Macro
GET /macros
Get all macros
Retrieves a list of all macros available in the Foundry world.
Parameters
| Name | Type | Required | Source | Description |
|---|---|---|---|---|
| clientId | string | query | Client ID for the Foundry world | |
| userId | string | query, body | Foundry 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
- JavaScript
- cURL
- Python
- TypeScript
- Emojicode
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);
curl -X GET 'http://localhost:3010/macros?clientId=fvtt_099ad17ea199e7e3' \
-H "x-api-key: your-api-key-here"
import requests
base_url = 'http://localhost:3010'
path = '/macros'
params = {
'clientId': 'fvtt_099ad17ea199e7e3'
}
url = f'{base_url}{path}'
response = requests.get(
url,
params=params,
headers={
'x-api-key': 'your-api-key-here'
}
)
data = response.json()
print(data)
import axios from 'axios';
(async () => {
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 axios({
method: 'get',
headers: {
'x-api-key': 'your-api-key-here'
},
url
});
const data = response.data;
console.log(data);
})();
📦 sockets 🏠
💭 Emojicode HTTP Client
💭 Compile: emojicodec example.🍇 -o example
💭 Run: ./example
🏁 🍇
💭 Connection settings
🔤localhost🔤 ➡️ host
3010 ➡️ port
🔤/macros🔤 ➡️ path
💭 Query parameters
🔤clientId=fvtt_099ad17ea199e7e3🔤 ➡️ clientId
🔤?🧲clientId🧲🔤 ➡️ queryString
💭 Build HTTP request
🔤GET /macros🧲queryString🧲 HTTP/1.1❌r❌nHost: localhost:3010❌r❌nx-api-key: your-api-key-here❌r❌n❌r❌n🔤 ➡️ request
💭 Connect and send
🍺 🆕📞 host port❗ ➡️ socket
🍺 💬 socket 📇 request❗❗
💭 Read and print response
🍺 👂 socket 4096❗ ➡️ data
😀 🍺 🔡 data❗❗
💭 Close socket
🚪 socket❗
🍉
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 } ]}