Auto-Tokenize a World Actor Folder
Paste this into a Foundry Script macro (GM only). It collects every actor in a world Actor folder and runs them through Tokenizer 2's headless batch API - no editor windows, no clicking through actors one at a time.
Run it with FOLDER left empty to get a picker dialog, or set FOLDER to a folder name / id below to skip straight to the run.
Everything not overridden here follows your Tokenizer 2 settings (frame, ring mode, export size/format, save locations, naming templates).
Works on Tokenizer 2 v1.2.8 and later.
// ── Configuration ───────────────────────────────────────────────
// Leave FOLDER empty ("") to be prompted with a folder picker.
const FOLDER = "";
const OPTIONS = {
recursive: true, // include actors in subfolders
skipTokenized: true, // skip actors that already have a Tokenizer 2 token
skipWildcards: true, // skip wildcard-token actors
useActorImg: false, // source the portrait from actor.img instead of the token texture
dryRun: false, // list what would be processed, change nothing
};
// Optional per-run overrides passed straight to Tokenizer2.tokenize().
// Anything left null falls back to your module settings.
const TOKENIZE_OPTIONS = {
frameSrc: null, // e.g. "modules/tokenizer-2/img/frames/gold.png"
maskSrc: null, // e.g. "tokenizer/masks/circle.webp"
portraitFit: "contain",// "contain" | "cover"
// forceDynamicRing: true,
// forceBakedRing: true,
// exportSize: 2048,
// saveFolder: "tokenizer/npcs",
};
// ── Script ──────────────────────────────────────────────────────
const MODULE_ID = "tokenizer-2";
const api = game.modules.get(MODULE_ID)?.api;
if (!api) {
ui.notifications.error("Tokenizer 2 is not active in this world.");
return;
}
if (!game.user.isGM) {
ui.notifications.error("Auto-tokenize is GM only.");
return;
}
const escape = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => (
{ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]
));
/** Has this actor already been through Tokenizer? */
function isTokenized(actor) {
if (actor.getFlag?.(MODULE_ID, "layerStack")) return true;
const src = (actor.prototypeToken?.texture?.src ?? "") + "|" + (actor.img ?? "");
return src.toLowerCase().includes("tokenizer");
}
/** Mirrors the module's own wildcard detection, including the asterisk setting. */
function isWildcard(actor) {
if (!actor.prototypeToken?.randomImg) return false;
if (!game.settings.get(MODULE_ID, "check-for-wildcard-asterisk")) return true;
return (actor.prototypeToken.texture?.src ?? "").includes("*");
}
/** Parent folder id, whether stored as a document or a raw id. */
function parentId(folder) {
const parent = folder?.folder;
if (!parent) return null;
return typeof parent === "string" ? parent : (parent.id ?? null);
}
/**
* All descendant Actor folders, depth-first. Walks game.folders by parent id
* rather than Folder#children / #getSubfolders, whose shape has moved around
* between Foundry versions.
*/
function getSubfolders(folder) {
const all = game.folders.filter((f) => f.type === "Actor");
const out = [];
const walk = (parent) => {
for (const candidate of all) {
if (parentId(candidate) !== parent.id) continue;
if (out.includes(candidate)) continue; // cycle guard on malformed data
out.push(candidate);
walk(candidate);
}
};
walk(folder);
return out;
}
/** Resolve a Folder document, id, or (Actor-folder) name to a Folder. */
function resolveActorFolder(ref) {
if (!ref) return null;
if (typeof ref !== "string") return ref;
const byId = game.folders.get(ref);
if (byId) return byId;
const wanted = ref.trim().toLowerCase();
return game.folders.find((f) => f.type === "Actor" && f.name?.toLowerCase() === wanted) ?? null;
}
/** The world actors inside a folder, filtered per OPTIONS and sorted by name. */
function collectFolderActors(ref, { recursive = true, skipTokenized = false, skipWildcards = false } = {}) {
const folder = resolveActorFolder(ref);
if (!folder) throw new Error(`Tokenizer 2: no Actor folder matching "${ref}"`);
if (folder.type !== "Actor") throw new Error(`Tokenizer 2: folder "${folder.name}" is not an Actor folder`);
const ids = new Set([folder.id]);
if (recursive) for (const sub of getSubfolders(folder)) ids.add(sub.id);
let selected = game.actors.filter((a) => ids.has(a.folder?.id ?? a.folder ?? null));
if (skipTokenized) selected = selected.filter((a) => !isTokenized(a));
if (skipWildcards) selected = selected.filter((a) => !isWildcard(a));
return selected.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
}
/** Prompt for a folder when FOLDER is not configured. */
async function pickFolder() {
const folders = game.folders.filter((f) => f.type === "Actor");
if (!folders.length) {
ui.notifications.warn("This world has no Actor folders.");
return null;
}
const options = folders
.sort((a, b) => a.name.localeCompare(b.name))
.map((f) => {
const count = collectFolderActors(f, OPTIONS).length;
return `<option value="${f.id}">${escape(f.name)} (${count})</option>`;
})
.join("");
try {
return await foundry.applications.api.DialogV2.prompt({
window: { title: "Tokenizer 2: Auto-Tokenize Folder" },
content: `
<p>Pick the Actor folder to tokenize. Counts reflect the filters set in the macro.</p>
<select name="folder" style="width: 100%;">${options}</select>
`,
ok: {
label: "Continue",
callback: (_event, button) => button.form.elements.folder.value,
},
rejectClose: false,
});
} catch {
return null; // dismissed
}
}
const folderRef = FOLDER || (await pickFolder());
if (!folderRef) return;
let actors;
try {
actors = collectFolderActors(folderRef, OPTIONS);
} catch (err) {
ui.notifications.error(err.message);
return;
}
const folder = resolveActorFolder(folderRef);
if (!actors.length) {
ui.notifications.warn(`Tokenizer 2: no matching actors in "${folder.name}".`);
return;
}
if (OPTIONS.dryRun) {
console.log(`Tokenizer 2 dry run - "${folder.name}" (${actors.length} actors):`, actors.map((a) => a.name));
ui.notifications.info(`Tokenizer 2 dry run: ${actors.length} actor(s) in "${folder.name}" - see the console.`);
return;
}
let confirmed = false;
try {
confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: "Tokenizer 2: Auto-Tokenize Folder" },
content: `<p>Tokenize <strong>${actors.length}</strong> actor(s) in <strong>${escape(folder.name)}</strong>?</p>
<p>This overwrites their prototype token images.</p>`,
});
} catch { /* dismissed */ }
if (!confirmed) return;
// Foundry progress notifications; degrade to plain info toasts if absent.
let progress = null;
try {
progress = ui.notifications.info(`Tokenizing "${folder.name}"...`, { progress: true, pct: 0 });
} catch { /* no progress notification support */ }
const results = await api.tokenizeBatch(actors, {
...TOKENIZE_OPTIONS,
useActorImg: OPTIONS.useActorImg,
onProgress: (current, total, actor) => {
progress?.update({ pct: current / total, message: `Tokenizing ${actor.name} (${current}/${total})` });
},
});
progress?.update({ pct: 1, message: `Tokenizing "${folder.name}" - done` });
const failures = results.filter((r) => r.error);
if (failures.length) {
for (const f of failures) console.error(`Tokenizer 2: "${f.actor.name}" failed -`, f.error);
ui.notifications.warn(`Tokenizer 2: ${results.length - failures.length} tokenized, ${failures.length} failed - see the console.`);
} else {
ui.notifications.info(`Tokenizer 2: tokenized ${results.length} actor(s) in "${folder.name}".`);
}