Skip to main content

Macro

GET /macros

Get all macros

Retrieves a list of all macros available in the Foundry world.

Parameters

NameTypeRequiredSourceDescription
clientIdstringqueryClient ID for the Foundry world
userIdstringquery, bodyFoundry user ID or username to scope permissions (omit for GM-level access)

Returns

array - An array of macros with details

Try It Out

Code Examples

const baseUrl = 'http://localhost:3010';
const path = '/macros';
const params = {
clientId: 'fvtt_099ad17ea199e7e3'
};
const queryString = new URLSearchParams(params).toString();
const url = `${baseUrl}${path}?${queryString}`;

const response = await fetch(url, {
method: 'GET',
headers: {
'x-api-key': 'your-api-key-here'
}
});
const data = await response.json();
console.log(data);

Response

Status: 200

{
"type": "macros-result",
"requestId": "macros_1776657989837",
"macros": [
0: {
"uuid": "Macro.a8GD8bNBNJQeVtd1",
"id": "a8GD8bNBNJQeVtd1",
"name": "s2s",
"type": "script",
"author": "Gamemaster",
"command": "// ============================================================================ // Server-to-Server Transfer Macro (Phase 1) // Transfers entities between Foundry VTT worlds via the REST API relay server. // Source = this server (local Foundry APIs). Target = remote server (REST API). // Requires: foundry-rest-api module installed and configured on both worlds. // ============================================================================ (async () => { "use strict"; // ========================= SECTION 1: CONFIG & HELPERS ========================= const MODULE_ID = "foundry-rest-api"; const ENTITY_TYPES = ["Actor", "Item", "Scene", "JournalEntry", "RollTable", "Cards", "Macro", "Playlist"]; const FILE_EXTENSIONS = /\.(png|jpg|jpeg|gif|webp|svg|avif|mp3|ogg|wav|flac|m4a|webm|mp4|pdf)$/i; const SKIP_PATH_PREFIXES = ["systems/", "modules/", "icons/", "ui/"]; // Map entity type names to game collections const COLLECTION_MAP = { Actor: game.actors, Item: game.items, Scene: game.scenes, JournalEntry: game.journal, RollTable: game.tables, Cards: game.cards, Macro: game.macros, Playlist: game.playlists, }; // Validate module is active const mod = game.modules.get(MODULE_ID); if (!mod?.active) { ui.notifications.error("Foundry REST API module is not installed or active."); return; } // Read settings const wsUrl = game.settings.get(MODULE_ID, "wsRelayUrl"); const apiKey = game.settings.get(MODULE_ID, "apiKey"); if (!wsUrl || !apiKey) { ui.notifications.error("REST API module is not configured. Please set the relay URL and API key."); return; } // Derive HTTP URL from WebSocket URL const relayUrl = wsUrl.replace("wss://", "https://").replace("ws://", "http://").replace(/\/+$/, ""); // ========================= SECTION 2: API CLIENT (target server only) ========================= async function relayFetch(endpoint, options = {}) { const url = new URL(endpoint, relayUrl); if (options.params) { for (const [k, v] of Object.entries(options.params)) { if (v !== undefined && v !== null) url.searchParams.set(k, String(v)); } } const headers = { "x-api-key": apiKey }; if (options.body) headers["Content-Type"] = "application/json"; const resp = await fetch(url.toString(), { method: options.method || "GET", headers, body: options.body ? JSON.stringify(options.body) : undefined, }); if (!resp.ok) { const text = await resp.text().catch(() => ""); throw new Error(`API ${resp.status}: ${text || resp.statusText}`); } return resp.json(); } // Remote API — only used for TARGET server operations const remote = { getClients: () => relayFetch("/clients"), createEntity: (entityType, data, clientId, folder, keepId = true) => relayFetch("/create", { method: "POST", params: { clientId }, body: { entityType, data, folder: folder || null, keepId }, }), uploadFile: (path, filename, fileData, clientId) => relayFetch("/upload", { method: "POST", params: { clientId }, body: { path, filename, fileData, source: "data", overwrite: true }, }), createFolder: (name, folderType, clientId, parentFolderId) => relayFetch("/create-folder", { method: "POST", params: { clientId, name, folderType, parentFolderId }, }), getStructure: (clientId, types, includeEntityData = false, recursive = true) => relayFetch("/structure", { params: { clientId, types: types.join(","), includeEntityData, recursive, recursiveDepth: 10 }, }), getUsers: (clientId) => relayFetch("/users", { params: { clientId } }), createUser: (name, role, password, clientId) => relayFetch("/user", { method: "POST", params: { clientId }, body: { name, role: role ?? 1, password: password || undefined }, }), }; // ========================= SECTION 3: LOCAL SOURCE HELPERS ========================= // Get folder chain (array of folder names from root to the entity's folder) function getFolderChain(entity) { const chain = []; let folder = entity.folder; while (folder) { chain.unshift(folder.name); folder = folder.folder; } return chain; } // Serialize an entity for transfer using Foundry's toObject function serializeEntity(entity) { return entity.toObject(true); } // Download a local file as a data URL async function downloadLocalFile(filePath) { const url = filePath.startsWith("http") ? filePath : foundry.utils.getRoute(filePath); const resp = await fetch(url); if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); const blob = await resp.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } // Find all tokens for an actor across all scenes function findActorTokens(actorId) { const tokens = []; for (const scene of game.scenes) { for (const token of scene.tokens) { if (token.actorId === actorId) { tokens.push({ scene, token }); } } } return tokens; } // Delete all tokens for an actor across all scenes async function deleteActorTokens(actorId, log) { const tokenEntries = findActorTokens(actorId); if (tokenEntries.length === 0) return 0; log(` Deleting ${tokenEntries.length} token(s) across ${new Set(tokenEntries.map(t => t.scene.id)).size} scene(s)`); // Group by scene for batch deletion const byScene = new Map(); for (const { scene, token } of tokenEntries) { if (!byScene.has(scene.id)) byScene.set(scene.id, { scene, tokenIds: [] }); byScene.get(scene.id).tokenIds.push(token.id); } let deleted = 0; for (const { scene, tokenIds } of byScene.values()) { try { await scene.deleteEmbeddedDocuments("Token", tokenIds); deleted += tokenIds.length; } catch (err) { log(` Warning: Failed to delete tokens in scene "${scene.name}": ${err.message}`); } } return deleted; } // ========================= SECTION 4: FILE TRANSFER ENGINE ========================= function extractFilePaths(data) { const paths = new Set(); function walk(obj) { if (!obj || typeof obj !== "object") { if (typeof obj === "string" && FILE_EXTENSIONS.test(obj)) { if (obj.startsWith("http://") || obj.startsWith("https://")) return; if (SKIP_PATH_PREFIXES.some((p) => obj.startsWith(p))) return; paths.add(obj); } return; } if (Array.isArray(obj)) { for (const item of obj) walk(item); } else { for (const val of Object.values(obj)) walk(val); } } walk(data); return [...paths]; } async function transferFiles(paths, targetClientId, log) { const pathMap = new Map(); let success = 0; let failed = 0; for (let i = 0; i < paths.length; i++) { const filePath = paths[i]; const parts = filePath.split("/"); const filename = parts.pop(); const dir = parts.join("/") || "."; log(` File ${i + 1}/${paths.length}: ${filePath}`); try { // Download from local Foundry server const fileData = await downloadLocalFile(filePath); // Upload to remote target await remote.uploadFile(dir, filename, fileData, targetClientId); pathMap.set(filePath, filePath); success++; } catch (err) { log(` Warning: Failed to transfer: ${err.message}`); failed++; } } log(` Files: ${success} transferred, ${failed} failed`); return pathMap; } function remapFilePaths(data, pathMap) { if (!pathMap.size) return data; function walk(obj) { if (!obj || typeof obj !== "object") { if (typeof obj === "string" && pathMap.has(obj)) return pathMap.get(obj); return obj; } if (Array.isArray(obj)) return obj.map(walk); const result = {}; for (const [k, v] of Object.entries(obj)) result[k] = walk(v); return result; } return walk(data); } // ========================= SECTION 5: ENTITY TRANSFER ENGINE ========================= // Cache for created folders on target const folderCache = new Map(); async function ensureFolderChain(folderChain, folderType, targetClientId, log) { if (!folderChain.length) return null; let parentFolderId = null; for (const folderName of folderChain) { const cacheKey = `${folderName}|${folderType}|${parentFolderId || "root"}`; if (folderCache.has(cacheKey)) { parentFolderId = folderCache.get(cacheKey); continue; } try { log(` Creating folder: ${folderName}`); const result = await remote.createFolder(folderName, folderType, targetClientId, parentFolderId); parentFolderId = result.data?.id || result.id; folderCache.set(cacheKey, parentFolderId); } catch (err) { // Folder might already exist — try to find it try { const structure = await remote.getStructure(targetClientId, [folderType], false, true); const folderId = findFolderInStructure(structure, folderName, parentFolderId); if (folderId) { parentFolderId = folderId; folderCache.set(cacheKey, parentFolderId); } else { log(` Warning: Could not create or find folder "${folderName}": ${err.message}`); return parentFolderId; } } catch { log(` Warning: Could not create folder "${folderName}": ${err.message}`); return parentFolderId; } } } return parentFolderId; } function findFolderInStructure(structure, name, parentId) { const folders = structure?.data?.folders || structure?.folders || {}; function search(obj) { for (const [key, val] of Object.entries(obj)) { if (!val || typeof val !== "object") continue; if (key === name && val.id) { if (parentId && val.parentFolder !== parentId) continue; return val.id; } const nested = search(val); if (nested) return nested; } return null; } return search(folders); } async function transferEntity(entity, entityType, targetClientId, options, log) { const { deleteFromSource, deleteTokens, transferFilesOpt, ownershipRemap } = options; const entityName = entity.name || entity.id; log(`Transferring ${entityType}: ${entityName}`); try { // Serialize from local Foundry const entityData = serializeEntity(entity); // Transfer files let pathMap = new Map(); if (transferFilesOpt) { const filePaths = extractFilePaths(entityData); if (filePaths.length > 0) { log(` Found ${filePaths.length} file(s) to transfer`); pathMap = await transferFiles(filePaths, targetClientId, log); } } // Clone and remap file paths let createData = JSON.parse(JSON.stringify(entityData)); createData = remapFilePaths(createData, pathMap); // Remap ownership if needed if (ownershipRemap && createData.ownership) { const { sourceUserId, targetUserId } = ownershipRemap; if (createData.ownership[sourceUserId] !== undefined) { const permLevel = createData.ownership[sourceUserId]; delete createData.ownership[sourceUserId]; createData.ownership[targetUserId] = permLevel; } } // Recreate folder hierarchy on target let targetFolderId = null; const folderChain = getFolderChain(entity); if (folderChain.length > 0) { targetFolderId = await ensureFolderChain(folderChain, entityType, targetClientId, log); } // Remove folder from data (we pass it separately to the API) delete createData.folder; // Create entity on target (preserving _id) const result = await remote.createEntity(entityType, createData, targetClientId, targetFolderId); const targetUuid = result.uuid || `${entityType}.${createData._id}`; log(` Created on target: ${targetUuid}`); // Delete from source if requested if (deleteFromSource) { try { // Delete associated tokens first if this is an actor if (deleteTokens && entityType === "Actor") { const count = await deleteActorTokens(entity.id, log); if (count > 0) log(` Deleted ${count} token(s) from source scenes`); } await entity.delete(); log(` Deleted from source`); } catch (err) { log(` Warning: Failed to delete from source: ${err.message}`); } } return { success: true, entityName, targetUuid }; } catch (err) { log(` ERROR: ${err.message}`); return { success: false, entityName, error: err.message }; } } // ========================= SECTION 6: USER TRANSFER ENGINE ========================= async function transferPlayer(user, targetClientId, options, log) { const { createAccount, password, role, transferFilesOpt, deleteFromSource, deleteTokens, deleteUser } = options; let targetUserId = null; let ownershipRemap = null; if (createAccount) { log(`Creating user account: ${user.name}`); // Check if user already exists on target const targetUsersResp = await remote.getUsers(targetClientId); const targetUsers = targetUsersResp.data || targetUsersResp; const existingUser = (Array.isArray(targetUsers) ? targetUsers : []).find( (u) => u.name.toLowerCase() === user.name.toLowerCase() ); if (existingUser) { targetUserId = existingUser.id; log(` User already exists on target (ID: ${targetUserId})`); } else { const created = await remote.createUser( user.name, role ?? user.role, password || undefined, targetClientId ); targetUserId = created.data?.id || created.id; log(` Created user on target (ID: ${targetUserId})`); } ownershipRemap = { sourceUserId: user.id, targetUserId }; } // Find all entities owned by this user (locally) log("Finding owned entities..."); const ownedEntities = []; for (const entityType of ENTITY_TYPES) { const collection = COLLECTION_MAP[entityType]; if (!collection) continue; for (const entity of collection) { const ownership = entity.ownership || {}; if (ownership[user.id] >= 3) { ownedEntities.push({ entity, entityType }); } } } log(`Found ${ownedEntities.length} owned entity(ies)`); // Transfer each entity const results = []; for (const { entity, entityType } of ownedEntities) { const result = await transferEntity(entity, entityType, targetClientId, { deleteFromSource, deleteTokens, transferFilesOpt, ownershipRemap, }, log); results.push(result); } // Delete user from source if requested if (deleteUser && deleteFromSource) { try { await user.delete(); log(`Deleted user "${user.name}" from source`); } catch (err) { log(`Warning: Failed to delete user from source: ${err.message}`); } } return results; } // ========================= SECTION 7: UI DIALOGS ========================= // Fetch connected clients to find target servers let clients; try { const resp = await remote.getClients(); clients = resp.clients || []; } catch (err) { ui.notifications.error(`Failed to connect to relay server: ${err.message}`); return; } if (clients.length < 2) { ui.notifications.warn("Need at least 2 connected servers. Ensure both worlds have the REST API module configured with the same API key."); return; } // Source is always the current world const currentWorldId = game.world.id; const currentClient = clients.find((c) => c.worldId === currentWorldId); if (!currentClient) { ui.notifications.error("Could not identify this server among connected clients. Check that the REST API module is connected."); return; } const sourceName = currentClient.customName || currentClient.worldTitle || currentClient.worldId; // Target candidates are all other connected servers const targetClients = clients.filter((c) => c.id !== currentClient.id); // Shared CSS for all transfer dialogs const S2S_CSS = ` <style> .s2s .form-group { margin: 4px 0; } .s2s .form-group label { margin-bottom: 2px; } .s2s .s2s-row { display: flex; gap: 8px; } .s2s .s2s-row > .form-group { flex: 1; } .s2s .s2s-header { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; padding: 6px 8px; background: rgba(0,0,0,0.1); border-radius: 4px; font-size: 13px; } .s2s .s2s-header .fas { opacity: 0.5; } .s2s .s2s-checks { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.1); } .s2s .s2s-checks label { display: flex; align-items: center; gap: 4px; white-space: nowrap; font-size: 12px; } .s2s .s2s-entity-list { max-height: 250px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.15); border-radius: 3px; padding: 4px; } .s2s .s2s-entity-list label { display: block; padding: 1px 2px; font-size: 12px; } .s2s .s2s-entity-list label:hover { background: rgba(255,255,255,0.05); } .s2s .s2s-select-all { display: block; padding: 2px; margin-bottom: 2px; border-bottom: 1px solid rgba(255,255,255,0.1); font-weight: bold; font-size: 12px; } .s2s .s2s-log { height: 200px; overflow-y: auto; background: #0a0a0a; color: #0f0; padding: 8px; font-size: 11px; white-space: pre-wrap; border-radius: 3px; border: 1px solid rgba(255,255,255,0.1); } .s2s .s2s-status { font-size: 12px; margin-bottom: 4px; opacity: 0.8; } </style> `; // --- Server Selection Dialog --- function showServerSelectDialog() { const targetOptions = targetClients .map((c) => `<option value="${c.id}">${c.customName || c.worldTitle || c.worldId} (${c.systemTitle || c.systemId})</option>`) .join(""); const content = ` ${S2S_CSS} <div class="s2s"> <div class="s2s-row"> <div class="form-group"> <label>Source</label> <input type="text" value="${sourceName}" disabled /> </div> <div class="form-group"> <label>Target</label> <select name="target">${targetOptions}</select> </div> </div> <div class="form-group"> <label>Transfer Mode</label> <select name="mode"> <option value="entities">Select Entities</option> <option value="player">Transfer Player</option> </select> </div> </div> `; new Dialog({ title: "Server-to-Server Transfer", content, buttons: { next: { icon: '<i class="fas fa-arrow-right"></i>', label: "Next", callback: (html) => { const targetId = html.find('[name="target"]').val(); const mode = html.find('[name="mode"]').val(); if (mode === "entities") showEntitySelectDialog(targetId); else showPlayerTransferDialog(targetId); }, }, cancel: { icon: '<i class="fas fa-times"></i>', label: "Cancel" }, }, default: "next", }, { width: 420 }).render(true); } // --- Entity Selection Dialog --- function showEntitySelectDialog(targetId) { const targetClient = clients.find((c) => c.id === targetId); const targetName = targetClient?.customName || targetClient?.worldTitle || targetId; const typeOptions = ENTITY_TYPES.map((t) => `<option value="${t}">${t}</option>`).join(""); const content = ` ${S2S_CSS} <div class="s2s"> <div class="s2s-header"> <span>${sourceName}</span> <i class="fas fa-arrow-right"></i> <span>${targetName}</span> </div> <div class="form-group"> <label>Entity Type</label> <select name="entityType" id="s2s-entity-type">${typeOptions}</select> </div> <div class="form-group"> <div id="s2s-entity-list" class="s2s-entity-list"> <p><em>Loading...</em></p> </div> </div> <div class="s2s-checks"> <label><input type="checkbox" name="transferFiles" checked /> Transfer files</label> <label><input type="checkbox" name="deleteSource" /> Delete from source</label> <label id="s2s-delete-tokens-group" style="display:none;"><input type="checkbox" name="deleteTokens" checked /> Delete tokens</label> </div> </div> `; // Map of id -> local entity reference let entityRefMap = new Map(); new Dialog({ title: "Select Entities to Transfer", content, buttons: { transfer: { icon: '<i class="fas fa-exchange-alt"></i>', label: "Transfer", callback: async (html) => { const checked = html.find('.s2s-entity-cb:checked'); const selectedIds = []; checked.each(function () { selectedIds.push(this.value); }); if (selectedIds.length === 0) { ui.notifications.warn("No entities selected."); return; } const entityType = html.find('[name="entityType"]').val(); const transferFilesOpt = html.find('[name="transferFiles"]').is(":checked"); const deleteFromSource = html.find('[name="deleteSource"]').is(":checked"); const deleteTokens = html.find('[name="deleteTokens"]').is(":checked"); // Resolve local entity references const entitiesToTransfer = []; for (const id of selectedIds) { const entity = entityRefMap.get(id); if (entity) entitiesToTransfer.push({ entity, entityType }); else ui.notifications.warn(`Entity ${id} not found locally.`); } showProgressDialog(entitiesToTransfer, targetId, { transferFilesOpt, deleteFromSource, deleteTokens }); }, }, cancel: { icon: '<i class="fas fa-times"></i>', label: "Cancel" }, }, default: "transfer", render: (html) => { const typeSelect = html.find("#s2s-entity-type"); const deleteSourceCb = html.find('[name="deleteSource"]'); const deleteTokensGroup = html.find("#s2s-delete-tokens-group"); // Show/hide "delete tokens" based on entity type and delete checkbox const updateTokensVisibility = () => { const isActor = typeSelect.val() === "Actor"; const isDelete = deleteSourceCb.is(":checked"); deleteTokensGroup.toggle(isActor && isDelete); }; typeSelect.on("change", updateTokensVisibility); deleteSourceCb.on("change", updateTokensVisibility); const loadEntities = () => { const entityType = typeSelect.val(); const listDiv = html.find("#s2s-entity-list"); entityRefMap.clear(); const collection = COLLECTION_MAP[entityType]; if (!collection || collection.size === 0) { listDiv.html("<p><em>No entities found</em></p>"); updateTokensVisibility(); return; } // Build list with folder paths const entries = []; for (const entity of collection) { const folderChain = getFolderChain(entity); const folderPath = folderChain.join("/"); entries.push({ id: entity.id, name: entity.name, folderPath, entity }); entityRefMap.set(entity.id, entity); } // Sort by folder path then name entries.sort((a, b) => { const pathCmp = a.folderPath.localeCompare(b.folderPath); if (pathCmp !== 0) return pathCmp; return a.name.localeCompare(b.name); }); const checkboxes = entries.map((e) => { const label = e.folderPath ? `${e.folderPath}/${e.name}` : e.name; return `<label><input type="checkbox" class="s2s-entity-cb" value="${e.id}" /> ${label}</label>`; }).join(""); const selectAll = `<label class="s2s-select-all"><input type="checkbox" id="s2s-select-all" /> Select All (${entries.length})</label>`; listDiv.html(selectAll + checkboxes); listDiv.find("#s2s-select-all").on("change", function () { listDiv.find(".s2s-entity-cb").prop("checked", this.checked); }); updateTokensVisibility(); }; typeSelect.on("change", loadEntities); loadEntities(); }, }, { width: 480 }).render(true); } // --- Player Transfer Dialog --- function showPlayerTransferDialog(targetId) { const targetClient = clients.find((c) => c.id === targetId); const targetName = targetClient?.customName || targetClient?.worldTitle || targetId; // Get non-GM users from local game const playerUsers = game.users.filter((u) => u.role < 4); if (playerUsers.length === 0) { ui.notifications.warn("No player accounts found."); return; } const userOptions = playerUsers .map((u) => `<option value="${u.id}">${u.name} (Role: ${u.role})</option>`) .join(""); const content = ` ${S2S_CSS} <div class="s2s"> <div class="s2s-header"> <span>${sourceName}</span> <i class="fas fa-arrow-right"></i> <span>${targetName}</span> </div> <div class="form-group"> <label>Player</label> <select name="userId">${userOptions}</select> </div> <div class="s2s-row"> <div class="form-group"> <label><input type="checkbox" name="createAccount" checked /> Create account on target</label> </div> <div class="form-group"> <label>Password</label> <input type="text" name="password" placeholder="Optional" /> </div> </div> <div class="s2s-checks"> <label><input type="checkbox" name="transferFiles" checked /> Transfer files</label> <label><input type="checkbox" name="deleteSource" /> Delete from source</label> <label><input type="checkbox" name="deleteTokens" checked /> Delete tokens</label> <label><input type="checkbox" name="deleteUser" /> Delete user account</label> </div> </div> `; new Dialog({ title: "Transfer Player", content, buttons: { transfer: { icon: '<i class="fas fa-exchange-alt"></i>', label: "Transfer", callback: async (html) => { const userId = html.find('[name="userId"]').val(); const createAccount = html.find('[name="createAccount"]').is(":checked"); const password = html.find('[name="password"]').val(); const transferFilesOpt = html.find('[name="transferFiles"]').is(":checked"); const deleteFromSource = html.find('[name="deleteSource"]').is(":checked"); const deleteTokens = html.find('[name="deleteTokens"]').is(":checked"); const deleteUser = html.find('[name="deleteUser"]').is(":checked"); const user = game.users.get(userId); if (!user) { ui.notifications.error("User not found."); return; } showPlayerProgressDialog(user, targetId, { createAccount, password, transferFilesOpt, deleteFromSource, deleteTokens, deleteUser, }); }, }, cancel: { icon: '<i class="fas fa-times"></i>', label: "Cancel" }, }, default: "transfer", }, { width: 450 }).render(true); } // --- Progress Dialog (entity transfer) --- function showProgressDialog(entitiesToTransfer, targetId, options) { const logLines = []; const log = (msg) => { logLines.push(msg); const logEl = document.getElementById("s2s-log"); if (logEl) { logEl.textContent = logLines.join("\n"); logEl.scrollTop = logEl.scrollHeight; } const progEl = document.getElementById("s2s-progress"); if (progEl) progEl.textContent = msg; }; const content = ` ${S2S_CSS} <div class="s2s"> <p class="s2s-status" id="s2s-progress">Starting transfer...</p> <pre class="s2s-log" id="s2s-log"></pre> </div> `; new Dialog({ title: "Transfer in Progress", content, buttons: { close: { icon: '<i class="fas fa-check"></i>', label: "Close" }, }, default: "close", render: async () => { const results = []; for (let i = 0; i < entitiesToTransfer.length; i++) { const { entity, entityType } = entitiesToTransfer[i]; log(`--- Entity ${i + 1}/${entitiesToTransfer.length} ---`); const result = await transferEntity(entity, entityType, targetId, options, log); results.push(result); } const succeeded = results.filter((r) => r.success).length; const failed = results.filter((r) => !r.success).length; log("\n========== TRANSFER COMPLETE =========="); log(`Entities: ${succeeded} succeeded, ${failed} failed`); if (failed > 0) { log("\nFailed entities:"); for (const r of results.filter((r) => !r.success)) { log(` - ${r.entityName}: ${r.error}`); } } ui.notifications.info(`Transfer complete: ${succeeded} succeeded, ${failed} failed`); }, }, { width: 520 }).render(true); } // --- Progress Dialog (player transfer) --- function showPlayerProgressDialog(user, targetId, options) { const logLines = []; const log = (msg) => { logLines.push(msg); const logEl = document.getElementById("s2s-log"); if (logEl) { logEl.textContent = logLines.join("\n"); logEl.scrollTop = logEl.scrollHeight; } }; const content = ` ${S2S_CSS} <div class="s2s"> <p class="s2s-status" id="s2s-progress">Starting player transfer...</p> <pre class="s2s-log" id="s2s-log"></pre> </div> `; new Dialog({ title: "Player Transfer in Progress", content, buttons: { close: { icon: '<i class="fas fa-check"></i>', label: "Close" }, }, default: "close", render: async () => { try { const results = await transferPlayer(user, targetId, options, log); const succeeded = results.filter((r) => r.success).length; const failed = results.filter((r) => !r.success).length; log("\n========== TRANSFER COMPLETE =========="); log(`Entities: ${succeeded} succeeded, ${failed} failed`); if (failed > 0) { log("\nFailed entities:"); for (const r of results.filter((r) => !r.success)) { log(` - ${r.entityName}: ${r.error}`); } } ui.notifications.info(`Player transfer complete: ${succeeded} succeeded, ${failed} failed`); } catch (err) { log(`\nFATAL ERROR: ${err.message}`); ui.notifications.error(`Player transfer failed: ${err.message}`); } }, }, { width: 520 }).render(true); } // ========================= SECTION 8: MAIN ENTRY ========================= showServerSelectDialog(); })();",
"img": "icons/svg/dice-target.svg",
"scope": "global",
"canExecute": true
},
1: {
"uuid": "Macro.j7NHQIWtXA4AG8Jt",
"id": "j7NHQIWtXA4AG8Jt",
"name": "Nuke",
"type": "script",
"author": "Gamemaster",
"command": "async function cleanSlate() { // List of document types to wipe const collections = [ game.scenes, game.actors, game.items, game.journal, game.tables, game.playlists, game.cards, // game.macros // Uncomment this line if you want to delete all macros too ]; for (let collection of collections) { const ids = collection.map(doc => doc.id); if (ids.length > 0) { console.log(`Deleting ${ids.length} documents from ${collection.name}...`); await collection.documentClass.deleteDocuments(ids); } } ui.notifications.info("World cleanup complete. A fresh start awaits!"); } // Confirmation Dialog new Dialog({ title: "Nuclear Option: Clear World Data", content: ` <div style="text-align: center;"> <p><i class="fas fa-exclamation-triangle fa-3x" style="color: #ff6b6b;"></i></p> <p>This will <strong>permanently delete</strong> all Scenes, Actors, Items, Journals, and more.</p> <p><em>Are you absolutely sure?</em></p> </div>`, buttons: { confirm: { icon: '<i class="fas fa-trash"></i>', label: "Delete Everything", callback: () => cleanSlate() }, cancel: { icon: '<i class="fas fa-times"></i>', label: "Cancel" } }, default: "cancel" }).render(true);",
"img": "icons/svg/poison.svg",
"scope": "global",
"canExecute": true
},
2: {
"uuid": "Macro.AkcLmoRwvkrPvjyA",
"id": "AkcLmoRwvkrPvjyA",
"name": "test-macro",
"type": "script",
"author": "tester",
"command": "// Example macro that uses parameters function myMacro(args) { const targetName = args.targetName || "Target"; const damage = args.damage || 0; const effect = args.effect || "none"; // Use the parameters console.log(`Attacking ${targetName} for ${damage} ${effect} damage`); // Return a value (can be any data type) return { success: true, damageDealt: damage, target: targetName }; } // Don't forget to return the result of your function return myMacro(args);",
"img": "icons/svg/dice-target.svg",
"scope": "global",
"canExecute": true
}
]
}