Claude Code Plugins

Community-maintained marketplace

Feedback

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name fvtt-sockets
description 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.

Foundry VTT Sockets & Multiplayer

Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-04

Overview

Foundry VTT uses Socket.io for real-time communication between server and clients. Understanding socket patterns is essential for multiplayer-safe code.

When to Use This Skill

  • Broadcasting events to other connected clients
  • Implementing GM-delegated operations for players
  • Synchronizing non-document state across clients
  • Creating animations/effects visible to all players
  • Avoiding duplicate execution in hooks

Socket Setup

Manifest Configuration

Request socket access in your manifest:

{
  "id": "my-module",
  "socket": true
}

Event Naming

Each package gets ONE event namespace:

  • Modules: module.{module-id}
  • Systems: 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" }
});

Registration Timing

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;
  }
}

Basic Socket Patterns

Emit to All Other Clients

function broadcastAnimation(tokenId, effect) {
  game.socket.emit("module.my-module", {
    type: "playAnimation",
    tokenId,
    effect
  });
}

Critical: Emitting client does NOT receive its own broadcast.

Self-Invoke Pattern

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);
  });
});

ExecuteAsGM Pattern

Players often need GM-authorized operations (damage enemies, modify world data).

Native Socket Approach

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:

  • No return value
  • Manual GM check required
  • Fails silently if no GM connected

Socketlib Approach (Recommended)

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");
  }
}

Socketlib Methods

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

ExecuteForEveryone Example

// Trigger animation on ALL clients
function playGlobalEffect(effectData) {
  socket.executeForEveryone("renderEffect", effectData);
}

// Registered function
function renderEffect(data) {
  canvas.effects.playEffect(data);
}

ExecuteAsUser Example

// 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);
  });
}

Data Synchronization

Document Updates (Automatic)

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;

Non-Document State

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);
}

Ownership Considerations

Only owners can update documents:

// Player cannot update enemy
await enemyActor.update({ ... }); // Permission denied!

// Must delegate to GM
await socket.executeAsGM("updateEnemy", enemyId, changes);

Common Pitfalls

1. Emitter Doesn't Receive Broadcast

// 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" });

2. Duplicate Execution in Hooks

// 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 });
});

3. Race Conditions with Multiple GMs

// 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);

4. No Permission Check on Handlers

// 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 });
});

5. No GM Connected

// 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");
}

6. Update Storms

// 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() });
});

Best Practices

1. Use Structured Events

// Good - clear, maintainable
game.socket.emit(SOCKET_NAME, {
  type: "applyEffect",
  targetId: token.id,
  effectType: "fire",
  duration: 3000
});

2. Batch Updates

// 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"
});

3. Skip No-Op Updates

const newHp = calculateHp(actor);
if (actor.system.hp.value === newHp) return;
await actor.update({ "system.hp.value": newHp });

4. Document Socket Messages

/**
 * Socket: module.my-module
 *
 * @event applyDamage
 * @param {string} actorId - Target actor
 * @param {number} damage - Damage amount
 * @param {string} type - Damage type (fire, cold, etc.)
 */

5. Prefer Socketlib for Complex Operations

Native sockets for simple broadcasts. Socketlib when you need:

  • Return values
  • Multiple GM handling
  • Permission-based execution
  • Error handling

Implementation Checklist

  • Add "socket": true to manifest
  • Use correct namespace (module.X or system.X)
  • Register listeners in init hook
  • Use structured event data with type field
  • Call handler locally when emitting (self-invoke pattern)
  • Check ownership in document operation hooks
  • Use socketlib for GM-delegated operations
  • Handle "no GM connected" errors
  • Batch related updates
  • Skip no-op updates
  • Test with multiple connected clients

References


Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset