Skip to content

api core

github-actions[bot] edited this page Aug 31, 2026 · 13 revisions

Blacksmith Core API

Audience: Developers of other FoundryVTT modules integrating with Coffee Pub Blacksmith. If you are working on Blacksmith itself, see ../architecture/architecture-blacksmith.md.

This is the entry point. It covers how to reach the API, when each part becomes available, how to register your module, and what the surface contains. Each subsystem has its own reference (Pins, Toolbar, Menubar, Sockets, and so on) linked at the end.

Two ways in

  • game.modules.get('coffee-pub-blacksmith')?.api — the module API object. Use this for registration and for every namespaced surface (pins, chatCards, campaign, ...).
  • BlacksmithAPI — a bridge with async helpers that wait for Blacksmith to finish loading:
import { BlacksmithAPI } from '/modules/coffee-pub-blacksmith/api/blacksmith-api.js';

const blacksmith = await BlacksmithAPI.get();   // resolves once Blacksmith is fully ready

There are also convenience globals (BlacksmithUtils, BlacksmithConstants, BlacksmithHookManager, BlacksmithModuleManager, BlacksmithStats, BlacksmithCompendiums, BlacksmithEffects). They are assigned late — see timing below.

Availability and timing

This is the part that causes most integration bugs, so it is worth reading once.

  • module.api is assigned synchronously at the start of Blacksmith's init, before any await in that hook. So by the time your ready handler runs, game.modules.get('coffee-pub-blacksmith')?.api is non-null and registerModule, utils, version, BLACKSMITH, and stats are usable.
  • Some members start as null placeholders and are filled in later during init, when their dynamic imports complete: the toolbar, window, menubar, notification, and secondary-bar functions. Call these from ready (or after await BlacksmithAPI.waitForReady()), not from init.
  • api.compendiums and api.campaign bind before the first await in Blacksmith's ready, alongside the compendium, roll table and macro choice caches. This matters because module scripts load alphabetically: a sibling whose id sorts before coffee-pub-blacksmith registers its ready handler first and therefore runs first. Anything assigned later in the hook is undefined for that window.
  • Asset-backed data finishes during Blacksmith's ready: assetLookup and the merged keys on BLACKSMITH. api.BLACKSMITH is the same object reference throughout — it gains keys as merges run, so a reference captured early stays valid.
  • The window.Blacksmith* globals are attached when markReadyForConsumers() runs, near the end of Blacksmith's ready — not at the first line of it. BlacksmithAPI.waitForReady() resolves after that point.

Rule of thumb: register and read simple API members in ready; await BlacksmithAPI.waitForReady() first if you need asset-backed constants or the globals.

One known gap: window.BlacksmithCanvasLayer is effectively never set. Use BlacksmithAPI.getCanvasLayer() instead — see api-canvas.md.

Awaiting readiness tells you ready ran, not that it succeeded

BlacksmithAPI.waitForReady() is only ever resolved, never rejected. If a stage of Blacksmith's ready throws, the failure handler still calls markReadyForConsumers() — deliberately, so that a consumer gets a degraded API and fails visibly rather than awaiting a promise that never settles. The consequence for you is that awaiting and then dereferencing is not safe: the await proves the hook finished, not that what you need exists.

Check for what you actually use, after the await:

await BlacksmithAPI.waitForReady();
const api = game.modules.get('coffee-pub-blacksmith')?.api;
if (!api?.compendiums) { /* decide: degrade, or refuse to start */ }

Then match your failure mode to whether recovery is possible. Degrading is right when a later attempt can succeed — a panel that renders empty now and repopulates on the next open costs a user nothing. Degrading is wrong when the failed operation cannot be retried, because the "degraded" state is permanent and silent rather than loud.

Settings registration is the case where it cannot. game.settings.register runs once per key and refuses re-registration, so a dropdown built from empty API-derived choices is not a temporary degradation that a reload repairs — it is a dead control for the life of that world. A consumer in that position should refuse to initialise and say why, rather than register settings it knows are wrong. That is what coffee-pub-artificer does, and it is the correct reading of this contract rather than an unusually strict one.

The general rule, since it applies well beyond settings: degrade where recovery is possible, refuse where it is not. A guard that silently supplies ?? {} to something unretryable converts a loud failure into a permanent one.

Quick start

1. Declare the dependency

{
  "relationships": {
    "requires": [
      {
        "id": "coffee-pub-blacksmith",
        "type": "module",
        "manifest": "https://github.com/Drowbe/coffee-pub-blacksmith/releases/latest/download/module.json"
      }
    ]
  }
}

2. Register your module

Registration tells Blacksmith your module exists and enables inter-module features.

Hooks.once('ready', () => {
    const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
    if (!blacksmith) return;   // Blacksmith not installed or disabled

    blacksmith.registerModule('my-module-id', {
        name: 'MYMODULE',      // short name used in logs
        version: '1.0.0'
    });
});
  • my-module-id must match your module.json id exactly.
  • name is a short label that makes console output easy to filter.

Related: isModuleActive(moduleId) and getModuleFeatures(moduleId).

3. Guard every call

Blacksmith may be absent, disabled, or mid-load. Degrade gracefully:

const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
if (!blacksmith?.pins?.isAvailable?.()) return;

The API surface

Namespaced sub-APIs, each with its own reference doc:

Namespace What it does Reference
api.pins Canvas pins: CRUD, placement, taxonomy, events api-pins.md
api.chatCards Chat card themes and class names api-chatcards.md
api.campaign Normalized campaign context, party roster, leader api-campaign.md
api.compendiums Compendium mapping; resolve text to UUIDs api-compendiums.md
api.effects Read-only Active Effect normalization and classifiers api-effects.md
api.stats Combat and player statistics api-stats.md
api.sockets Cross-client messaging (SocketLib with native fallback) api-sockets.md
api.gmNotes GM-only notes on documents api-gmnotes.md
api.toast On-screen toast notifications api-toast.md
api.tags Shared tag vocabulary api-tags.md
api.uiContextMenu Context-menu building blocks this document

Function groups on api directly:

Group Functions Reference
Module registration registerModule, isModuleActive, getModuleFeatures this document
Toolbar registerToolbarTool, unregisterToolbarTool, getRegisteredTools, getToolsByModule, isToolRegistered, getToolbarSettings, setToolbarSettings api-toolbar.md
Windows registerWindow, unregisterWindow, openWindow, getRegisteredWindows, isWindowRegistered, getWindowBaseV2, BlacksmithWindowBaseV2, getToolWindowBaseV2, BlacksmithToolWindowBaseV2, windowStyles, toolTitlebars api-window.md
Menubar registerMenubarTool, unregisterMenubarTool, getRegisteredMenubarTools, getMenubarToolsByModule, getMenubarToolsByZone, isMenubarToolRegistered, renderMenubar, updateMenubarToolActive api-menubar.md
Menubar notifications addNotification, updateNotification, removeNotification, clearNotificationsByModule, clearAllNotifications, getActiveNotifications, getNotificationIdsByModule api-menubar.md
Secondary bars registerSecondaryBarType, registerSecondaryBarItem, registerSecondaryBarTool, unregisterSecondaryBarItem, updateSecondaryBarItemActive, updateSecondaryBarItemInfo, getSecondaryBarItems, openSecondaryBar, closeSecondaryBar, toggleSecondaryBar api-menubar.md
Canvas CanvasLayer, getCanvasLayer api-canvas.md
Rolls openRequestRollDialog, rolls api-requestroll.md, api-rolls.md
Encounters getPartyCR, getMonsterCR, parseCR, formatCR, calculateEncounterDifficulty, getCombatAssessment, deployMonsters this document
Party getPartyHealthSummary, getPartyActorHp, deployParty, clearPartyFromCanvas, isCurrentUserPartyLeader, getPartyReputation, setPartyReputation, getReputationScaleEntry, postCurrentReputationCard, postNewReputationCard this document
Hooks BlacksmithHookManager (global) api-hookmanager.md
Utilities api.utils below
Constants api.BLACKSMITH, BlacksmithConstants (global) below

The test* members on the API (testMenubarAPI, testNotificationSystem, and similar) are Blacksmith's own development helpers. They are not a supported contract; do not build against them.

Party actions

Member Signature Notes
deployParty ({ pattern?, prompt?, includeOwnedNpcs? }) => Promise<Token[]> Places the party's player characters on the current scene, skipping any that already have a token there. With no arguments it asks which formation to use (grid, circle, line, scatter, sequential) and whether to include player-owned NPCs, remembering both answers as the defaults. The NPC checkbox appears only when there are owned NPCs not already placed. Pass pattern to skip the picker, includeOwnedNpcs to decide the roster explicitly, or prompt: false to use the saved defaults silently -- a scripted caller is never blocked on a dialog. Dismissing the picker returns an empty array. GM only: a non-GM gets a logged warning and an empty array.
clearPartyFromCanvas () => Promise<number> Removes party tokens from the current scene and returns how many went. GM only, same shape as above.
isCurrentUserPartyLeader () => boolean True when the stored party leader is this user, or this user owns the leader's character. This is the predicate behind leaderOnly: true in the menubar registration API; use it to gate a control the same way the menubar does, rather than reading the partyLeader setting.

Party reputation

Reputation is a number from -100 to +100, stored per scene in the world setting blacksmithPartyData under scenes[sceneId].reputation. resources/reputation.json divides that range into eleven named bands with a label and a description.

Blacksmith owns the score and the bands. It does not own what the score means for a consumer's mechanics -- prices, attitude checks, what information an NPC will part with. A consumer reads the value and applies its own policy.

Member Signature Notes
getPartyReputation (scene?) => number The value for a scene. Returns 0 when there is no scene or nothing stored.
setPartyReputation (value, scene?) => Promise<boolean> Clamped to -100..+100. GM only; returns false for a non-GM or no scene.
getReputationScaleEntry (value) => Promise<{key, label, min, max, description, effects?}|null> The band containing a value.
postCurrentReputationCard () => Promise<void> Posts a Current Reputation chat card: scene, value, band label and description.
postNewReputationCard (change, previousValue, newValue) => Promise<void> Posts a New Reputation card. Call after changing the value.

Two properties worth knowing before building on these:

  • getPartyReputation defaults to the reading client's canvas scene. A window opened against a token must pass that token's scene explicitly, or a GM standing on another map reads a different value than the players standing in the scene the window is about.
  • getReputationScaleEntry is asynchronous because the first call fetches the JSON; the parsed file is cached for the session after that. Resolve the band once per render rather than once per row when pricing or labelling a list.

blacksmith.partyReputationChanged fires on every client whenever party reputation changes, carrying { sceneId, reputation }. It is emitted from Foundry's updateSetting, not from setPartyReputation, so it arrives however the value was changed and on every client rather than only the one that made the change.

Utilities (api.utils)

Also available as the BlacksmithUtils global once Blacksmith is ready.

Function Signature Notes
postConsoleAndNotification (moduleId, message, result, debug, notification) Console logging; debug: true logs only when Blacksmith debug is on, notification: true also shows a UI toast
playSound (sound, volume?, loop?, broadcast?, duration?) See below
getSettingSafely (moduleId, settingKey, defaultValue) Never throws on an unregistered setting
setSettingSafely (moduleId, settingKey, value) Async
formatTime (ms, format) format defaults to "colon"
generateFormattedDate (format) format is an enum, not a pattern: 'date', 'time', or anything else for both. Values are not zero-padded
trimString (str, maxLength)
toSentenceCase (str)
markdownToHtml (text) Subset only, see below
htmlToMarkdown (html) Subset only, see below
getActorId (actorName)
getTokenId (tokenName)
getTokenImage (tokenDoc)
getPortraitImage (actor)
objectToString (obj)
stringToObject (str) Parses key=value|other=data
convertSecondsToRounds (numSeconds)
convertSecondsToString (numSeconds) e.g. "1 HR (600 ROUNDS)"
clamp (value, min, max)
rollCoffeePubDice (roll) Async
resetModuleSettings (moduleId)
isPlayerCharacter (entity) Requires both a character sheet type and player ownership, so summons and player-owned NPCs are false. Accepts a combatant, token, token document, actor, id, or name
ownsAnyCharacter (user) The user-side counterpart: does this user have OWNER on at least one character actor? Owning only a group actor, a summon, or a familiar is false
resolveActorFrom (entity) Resolves any of those shapes to an Actor, or null
const utils = game.modules.get('coffee-pub-blacksmith')?.api?.utils;

utils.postConsoleAndNotification('my-module-id', 'Registered', { count: 3 }, false, false);

const theme = utils.getSettingSafely('my-module-id', 'cardTheme', 'theme-default');

playSound(sound, volume?, loop?, broadcast?, duration?)

  • sound — a path, or a constant such as BlacksmithConstants.SOUNDNOTIFICATION01. 'none' / 'sound-none' does nothing.
  • volume — 0 to 1, default 0.7.
  • loop — default false.
  • broadcast — default true (all clients); false plays locally only.
  • duration — optional seconds. If set, the sound loops for that long then stops, on all clients when broadcasting.

playSoundLocalWithDuration is exported from scripts/api-core.js but is not on api.utils, so utils.playSoundLocalWithDuration is undefined. For local playback with a timed stop, call playSound(sound, volume, false, false, duration) — that is what it does internally.

Markdown subset

markdownToHtml / htmlToMarkdown handle a deliberately small subset; anything else is treated as plain text: #/##/### headings, --- rules, **bold**, *italic*, -/* unordered lists, 1. ordered lists, and > blockquotes.

Handlebars helpers

Blacksmith registers a set of Handlebars helpers globally, so any module's templates can use them once Blacksmith is active. They are registered during init, before anything renders, and unconditionally -- no Blacksmith setting or feature switch affects whether they exist.

Helper Signature Returns
or (...values) True if any argument is truthy
and (a, b) a && b
eq (a, b) Strict equality
gt (a, b) a > b
add (a, b) a + b
subtract (a, b) a - b
multiply (a, b) a * b
divide (a, b) a / b
round (number) Math.round(number)
formatDamage (amount) The number as a string, or '0' when not a number
formatTime (ms) A duration such as 45s or 2m 30s
isImageUrl (string) True for a path-like prefix or an image extension

Foundry provides others (localize, numberFormat, lookup, ne, lt, gte, not and more); those are not Blacksmith's and are documented by Foundry.

Two cautions. formatTime is context-sensitive: inside a template whose data carries planningDuration or turnDuration it may return SKIPPED or EXPIRED rather than a duration, which is intended for the combat timer templates. And these helpers exist only while Blacksmith is enabled -- a template using them needs Blacksmith as a dependency, which any Coffee Pub module already has.

Implementation: scripts/utility-handlebars.js.

Constants

Read constants from the BlacksmithConstants global (or api.BLACKSMITH). COFFEEPUB is an internal export of scripts/api-core.js and is not exposed to other modules — do not reference it.

  • Sounds — BlacksmithConstants.SOUNDNOTIFICATION01, SOUNDBUTTON01, and similar.
  • Volumes — SOUNDVOLUMESOFT, SOUNDVOLUMENORMAL, SOUNDVOLUMELOUD, SOUNDVOLUMEMAX. These resolve to the numeric string the entry carries (e.g. "0.5"), which you can pass straight to playSound.
  • Icons — ICONNONE, ICONCHESSQUEEN, and others defined by resources/asset-defaults/assets-icons.json. Run BlacksmithAPIConstants() in the console for the current list rather than trusting a written example.
  • Choice maps — arrThemeChoices, arrSoundChoices, arrTableChoices, arrMacroChoices. Despite the arr prefix these are plain objects ({ value: label }) shaped for a Foundry choices map. .length is undefined; use Object.keys().

Chat card themes are not in BlacksmithConstants — use the chat cards API (api-chatcards.md).

Request Roll presentation assets (its cinematic overlay and sounds) are feature-local to Blacksmith, driven by themes/request-roll/theme-requestroll.json. They are not part of the shared constants contract.

For tag-based asset searching, reach the lookup through the API object:

const blacksmith = await BlacksmithAPI.get();
const sounds = blacksmith.assetLookup.searchByCriteria({ type: 'sound', tags: ['notification'] });

Patterns

Feature detection — check for what you use, not for a version:

const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
if (blacksmith?.registerToolbarTool) {
    blacksmith.registerToolbarTool('my-tool', { /* ... */ });
}

Waiting for asset-backed data:

import { BlacksmithAPI } from '/modules/coffee-pub-blacksmith/api/blacksmith-api.js';

Hooks.once('ready', async () => {
    await BlacksmithAPI.waitForReady();
    const sound = BlacksmithConstants.SOUNDNOTIFICATION01;   // globals are attached by now
});

Cleanup — Foundry has no module-unload event, and disabling a module reloads the world, so there is no teardown hook to register. Call any unregister methods from your own lifecycle if you turn a feature off at runtime. See api-hookmanager.md.

Console commands

Run these in the browser console to inspect an integration:

Command Shows
BlacksmithAPICheck() Overall integration check
BlacksmithAPIStatus() API readiness status
BlacksmithAPIVersion() Blacksmith API version
BlacksmithAPIModules() Registered modules
BlacksmithAPIFeatures() Registered features
BlacksmithAPIConstants() Current constants
BlacksmithAPIUtils() Available utilities
BlacksmithAPISettings() Settings snapshot
BlacksmithAPIAssetLookup() Asset lookup state
BlacksmithAPIDetails() Detailed API dump
BlacksmithAPIHooks() / BlacksmithAPIHookDetails() / BlacksmithAPIHookStats() / BlacksmithAPIHookExpandedDetails() Hook registrations (see api-hookmanager.md)
BlacksmithAPIManualReady() Force the ready signal (debugging only)

Troubleshooting

api is null. You are running in init before Blacksmith's init assigned it, or Blacksmith is not installed. Move the call to ready, and guard with ?..

A function is null rather than missing. The toolbar, window, menubar, notification, and secondary-bar members start as null placeholders and are filled during init. Call them from ready.

A constant is undefined. Asset-backed constants finish loading during Blacksmith's ready. await BlacksmithAPI.waitForReady() first, then read.

A global (BlacksmithUtils, BlacksmithConstants, ...) is undefined. Same cause — globals attach when markReadyForConsumers() runs late in ready. Await waitForReady().

.length on a choice array is undefined. The arr* choice maps are objects, not arrays. Use Object.keys().

Settings throw "not registered". Use getSettingSafely / setSettingSafely, and register settings that depend on Blacksmith API data inside ready (awaiting your own registration) rather than init.

Your module never appears in BlacksmithAPIModules(). registerModule was not called, ran before ready, or the id does not match your module.json.

Related documentation

  • Subsystem references: api-pins.md, api-toolbar.md, api-window.md, api-menubar.md, api-sockets.md, api-chatcards.md, api-stats.md, api-campaign.md, api-compendiums.md, api-requestroll.md, api-rolls.md, api-gmnotes.md, api-canvas.md, api-hookmanager.md, api-tags.md, api-toast.md
  • Internal architecture: ../architecture/architecture-blacksmith.md

Clone this wiki locally