Coordinating Overlay Permissions

stonedtrollstonedtroll
3 min read

In my project, I’ve got overlays—things like movement paths, elevation indicators, and token’s boundary—that need to appear only when the viewer has permission to see them. To manage this, I’ve built a system called the OverlayPermissionCoordinator.

It bridges Foundry VTT’s infrastructure with my domain logic, keeping permissions clean, isolated, and easy to reason about.

Here’s a breakdown of how the whole thing works.

Entry Points: Where the Flow Begins

Overlay view permission checks kick off in a few key methods:

graph TD
    A[UI/Rendering System] --> B{Which Method?}
    B --> C[canViewOverlayOnToken - Single Check]
    B --> D[canViewOverlaysOnToken - Batch Check]
    B --> E[getViewableOverlaysForToken - Get All Viewable]

Single Overlay Check: canViewOverlayOnToken

This is the core function that checks one overlay on one token. Here's what it does under the hood:

// Check if a single overlay is viewable by the user
canViewOverlayOnToken(
    overlayId: string,
    targetToken: Token,
    userId: string,
    isGM: boolean,
    viewerTokens: Token[] = []
): boolean {
    const config = this.registry.get(overlayId);

    if (!config) {
        this.logger.warn(`Overlay "${overlayId}" isn’t registered.`);
        return false;
    }

    // Quick exit if the overlay doesn't require permission checks
    if (!config.usePermissionSystem) {
        return true;
    }

    // Gather the necessary data for a proper check
    const targetData = this.getCachedTokenData(targetToken);
    const viewerData = this.getCachedTokensData(viewerTokens);
    const allVisibleTokens = this.getAllVisibleTokensData();

    // Let the domain service handle the actual decision
    return this.permissionService.canViewOverlaysOnToken(
        targetData,
        userId,
        isGM,
        viewerData,
        allVisibleTokens
    );
}

Step-by-step:

  1. Registry Lookup

    • Pulls the overlay config from the registry

    • If not found, returns false

  2. Fast Path Check

    • If usePermissionSystem === false, returns true immediately

    • This skips heavy logic for overlays that don’t need it

  3. Data Conversion (Cached)

    • Converts targetToken and viewerTokens[] into TokenSightData

    • Fetches all visible tokens if needed

  4. Domain Permission Check

    • Calls permissionService.canViewOverlays()

    • This service lives in the pure domain layer—no Foundry types involved

  5. Return Result

    • Comes back with a boolean based on the domain logic

Caching Strategy: Keep It Fast, Keep It Simple

💡
I've got a sneaking feeling this is just a case of good old-fashioned gold plating — but we’ve chosen the path, so let’s follow it and see if there’s a leprechaun at the end.

To avoid re-converting tokens constantly, I’ve built a lightweight caching layer:

// Token Data Cache Flow
    ┌─────────────────┐
    │  Foundry Token  │
    └────────┬────────┘
             ↓
┌────────────────────────┐
│   Generate Cache Key   │ → `${token.id}-${cacheGeneration}`
└────────────┬───────────┘
             ↓
        ┌────┴────┐
        │ Cached? │
        └────┬────┘
             ↓
         No ─┴── Yes
         ↓        ↓
      Convert    Return
      & Store    Cached

The key includes a generation number so I can invalidate it whenever needed (e.g. when tokens move or vision updates).

Example Flow: Full Walkthrough

Let’s say a user hovers over a token. I want to know which overlays are visible to them:

getViewableOverlaysForToken(token, userId, isGM, viewerTokens)

Behind the scenes:

  1. Grab all overlays from the registry:
    ['movement-path', 'elevation-indicator', 'status-effects']

  2. Call the batch check:
    canViewOverlaysOnToken([...], token, userId, isGM, viewerTokens)

  3. For each overlay:

    • movement-path: needs permission → check domain

    • elevation-indicator: doesn’t need permission → allow

    • status-effects: needs permission → check domain

  4. Result:
    ['elevation-indicator', 'status-effects']
    (assuming movement-path was denied)

Why This Works for Me

This setup gives me:

  • Separation of concerns
    Foundry stays out of my domain logic. My domain doesn’t even know Foundry exists.

  • Performance
    The system avoids unnecessary work and reuses data wherever possible.

  • Flexibility
    I can add new permission types or overlay rules without touching Foundry code.

  • Testability
    Domain services are testable in isolation, no mocks needed.

  • Maintainability
    All Foundry-specific stuff lives in one place—the coordinator.

If you’re wrangling overlays in a similar way or building on Foundry and trying to decouple things, I think this structure makes it much easier to reason about and test.

0
Subscribe to my newsletter

Read articles from stonedtroll directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

stonedtroll
stonedtroll