xiaohongshu
xiaohongshu
fvtt-sockets
This skill should be used when implementing multiplayer synchronization, using game.socket.emit/on, creating executeAsGM patterns for privileged operations, broadcasting events between clients, or avoiding common pitfalls like race conditions and duplicate execution.
Full skill instructions
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-04
Foundry VTT uses Socket.io for real-time communication between server and clients. Understanding socket patterns is essential for multiplayer-safe code.
Request socket access in your manifest:
{
"id": "my-module",
"socket": true
}
Each package gets ONE event namespace:
module.{module-id}system.{system-id}Multiplex event types with structured data:
const SOCKET_NAME = "module.my-module";
game.socket.emit(SOCKET_NAME, {
type: "playAnimation",
payload: { tokenId: "abc123", effect: "fire" }
});
Register listeners after game.socket is available:
Hooks.once("init", () => {
game.socket.on("module.my-module", handleSocketMessage);
});
function handleSocketMessage(data) {
switch (data.type) {
case "playAnimation":
playTokenAnimation(data.payload);
break;
case "syncState":
updateLocalState(data.payload);
break;
}
}
function broadcastAnimation(tokenId, effect) {
game.socket.emit("module.my-module", {
type: "playAnimation",
tokenId,
effect
});
}
Critical: Emitting client does NOT receive its own broadcast.
Always call handler locally when emitting:
function triggerEffect(tokenId, effect) {
const data = { type: "effect", tokenId, effect };
// Execute locally
handleEffect(data);
// Broadcast to others
game.socket.emit("module.my-module", data);
}
function handleEffect(data) {
const token = canvas.tokens.get(data.tokenId);
token?.animate({ alpha: 0.5 }, { duration: 500 });
}
// Socket listener (for other clients)
Hooks.once("init", () => {
game.socket.on("module.my-module", (data) => {
if (data.type === "effect") handleEffect(data);
});
});
Players often need GM-authorized operations (damage enemies, modify world data).
const SOCKET_NAME = "module.my-module";
Hooks.once("init", () => {
game.socket.on(SOCKET_NAME, async (data) => {
// Only active GM handles this
if (game.user !== game.users.activeGM) return;
if (data.type === "damageActor") {
const actor = game.actors.get(data.actorId);
if (actor) {
const newHp = actor.system.hp.value - data.damage;
await actor.update({ "system.hp.value": Math.max(0, newHp) });
}
}
});
});
// Player calls this
function requestDamage(actorId, damage) {
game.socket.emit(SOCKET_NAME, {
type: "damageActor",
actorId,
damage
});
}
Limitations:
Socketlib handles multiple GMs, return values, and error cases.
Dependency (module.json):
{
"relationships": {
"requires": [{
"id": "socketlib",
"type": "module"
}]
}
}
Registration:
let socket;
Hooks.once("socketlib.ready", () => {
socket = socketlib.registerModule("my-module");
// Register callable functions
socket.register("damageActor", damageActor);
socket.register("getActorData", getActorData);
});
async function damageActor(actorId, damage) {
const actor = game.actors.get(actorId);
if (!actor) return { success: false, error: "Actor not found" };
const newHp = Math.max(0, actor.system.hp.value - damage);
await actor.update({ "system.hp.value": newHp });
return { success: true, newHp };
}
function getActorData(actorId) {
return game.actors.get(actorId)?.toObject() ?? null;
}
Usage:
// Execute on GM client, get return value
async function applyDamage(actorId, damage) {
try {
const result = await socket.executeAsGM("damageActor", actorId, damage);
if (result.success) {
ui.notifications.info(`Damage applied. HP now: ${result.newHp}`);
}
} catch (error) {
ui.notifications.error("No GM connected to process damage");
}
}
| Method | Target | Awaitable | Use Case |
|---|---|---|---|
executeAsGM(fn, ...args) | One GM | Yes | Privileged operations |
executeAsUser(fn, userId, ...args) | Specific user | Yes | Player-specific actions |
executeForEveryone(fn, ...args) | All clients | No | Broadcast effects |
executeForOthers(fn, ...args) | All except self | No | Sync without local call |
executeForAllGMs(fn, ...args) | All GMs | No | GM notifications |
executeForUsers(fn, ids[], ...args) | Listed users | No | Targeted messages |
// Trigger animation on ALL clients
function playGlobalEffect(effectData) {
socket.executeForEveryone("renderEffect", effectData);
}
// Registered function
function renderEffect(data) {
canvas.effects.playEffect(data);
}
// Ask specific player for input
async function promptPlayer(userId, question) {
try {
return await socket.executeAsUser("showDialog", userId, question);
} catch {
return null; // Player disconnected
}
}
// Registered function
async function showDialog(question) {
return new Promise(resolve => {
new Dialog({
title: question,
buttons: {
yes: { label: "Yes", callback: () => resolve(true) },
no: { label: "No", callback: () => resolve(false) }
}
}).render(true);
});
}
Foundry syncs document updates automatically:
// Syncs to all clients
await actor.update({ "system.hp.value": 50 });
// Does NOT sync (in-memory only)
actor.system.hp.value = 50;
Use sockets for custom state:
let combatState = {};
Hooks.once("socketlib.ready", () => {
socket.register("syncCombatState", (state) => {
combatState = state;
Hooks.callAll("combatStateChanged", state);
});
});
function updateCombatState(newState) {
combatState = newState;
socket.executeForEveryone("syncCombatState", newState);
}
Only owners can update documents:
// Player cannot update enemy
await enemyActor.update({ ... }); // Permission denied!
// Must delegate to GM
await socket.executeAsGM("updateEnemy", enemyId, changes);
// WRONG - emitter never sees this
game.socket.on("module.my-module", playSound);
game.socket.emit("module.my-module", { sound: "bell.wav" });
// Sound plays for others, NOT for emitter!
// CORRECT - call locally AND emit
playSound({ sound: "bell.wav" });
game.socket.emit("module.my-module", { sound: "bell.wav" });
// WRONG - runs on ALL clients
Hooks.on("deleteItem", (item) => {
item.parent.update({ "system.count": item.parent.items.length });
});
// CORRECT - only owner executes
Hooks.on("deleteItem", (item) => {
if (!item.parent?.isOwner) return;
item.parent.update({ "system.count": item.parent.items.length });
});
// RISKY - activeGM can change during async
game.socket.on(name, async (data) => {
if (game.user !== game.users.activeGM) return;
await actor.update({ ... }); // Another GM might be active now!
});
// SAFE - socketlib guarantees atomic execution
await socket.executeAsGM("updateActor", actorId, data);
// VULNERABLE - any player can trigger
game.socket.on(name, (data) => {
game.actors.get(data.id).update({ "system.hp": 9999 });
});
// SAFE - validate permissions
game.socket.on(name, (data) => {
const actor = game.actors.get(data.id);
if (!actor?.isOwner && !game.user.isGM) return;
actor.update({ "system.hp": data.hp });
});
// WRONG - silent failure
socket.executeAsGM("doThing", data);
// CORRECT - handle error
try {
await socket.executeAsGM("doThing", data);
} catch {
ui.notifications.warn("A GM must be connected for this action");
}
// WRONG - N clients = N updates
Hooks.on("updateActor", (actor, changes) => {
actor.update({ "system.modified": Date.now() });
});
// CORRECT - only owner updates
Hooks.on("updateActor", (actor, changes) => {
if (!actor.isOwner) return;
if (changes.system?.modified) return; // Prevent loop
actor.update({ "system.modified": Date.now() });
});
// Good - clear, maintainable
game.socket.emit(SOCKET_NAME, {
type: "applyEffect",
targetId: token.id,
effectType: "fire",
duration: 3000
});
// Bad - 3 updates
await actor.update({ "system.hp": 10 });
await actor.update({ "system.mp": 5 });
await actor.update({ "system.status": "hurt" });
// Good - 1 update
await actor.update({
"system.hp": 10,
"system.mp": 5,
"system.status": "hurt"
});
const newHp = calculateHp(actor);
if (actor.system.hp.value === newHp) return;
await actor.update({ "system.hp.value": newHp });
/**
* Socket: module.my-module
*
* @event applyDamage
* @param {string} actorId - Target actor
* @param {number} damage - Damage amount
* @param {string} type - Damage type (fire, cold, etc.)
*/
Native sockets for simple broadcasts. Socketlib when you need:
"socket": true to manifestmodule.X or system.X)init hooktype fieldLast Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience