Skip to content

Courage.Bmg#

require("Courage.Bmg") registers runtime replacements for in-game message text. Gossip signs, shop shelf labels, item pickup lines, and other strings that come from BMG archives can be swapped without repacking bmgres.

Courage keeps override strings in memory and patches JMessage::TControl after the game resolves a message id. Native hooks install automatically on the first set or loadOverrides call.

Message ids usually come from extracted .bmg data or community tools. The Debug Text tab covers debug strings, not full gossip tables.

API#

Function Description
Bmg.set(key, text) Register one override. See override keys below.
Bmg.clear() Remove all overrides.
Bmg.loadOverrides(table) → number Bulk load from a map of key → string. Returns how many entries were loaded. Skips empty values and invalid keys.
Bmg.color(name) → string Return a Twilight Princess color control-code prefix for name.
Bmg.setDebug(enabled) Enable or disable BMG routing logs in the engine console.
Bmg.onShop(handlers) Register shop shelf and purchase-dialog callbacks. See shop callbacks.

Override keys#

Bmg.set and loadOverrides accept several key formats. The prefix determines how Courage routes the override at runtime.

Key format Example Use for
Message id (number) 263, 0x0107 Direct BMG message index (shops, HUD strings, etc.).
Message id (string) "0x72b0" Same as above. Decimal strings also work ("29360").
Flow id "flow:0x232a" Gossip signs and other text keyed by message flow.
Flow node "flow:0x72b0:2" A specific INF1 node inside a message flow.
Stage archive "arc:2:0x0107" Per-stage BMG archive + message id. Use arc:255:... to match any stage archive.

Gossip hints in seed data typically use flow:0x... keys. Shop mods often register both a plain message id and an arc:255:... mirror so overrides hit every lookup path the game uses.

Colored text#

Bmg.color returns the binary control bytes the TP message renderer expects. Concatenate a color prefix, your visible text, and Bmg.color("white") to reset.

Use \n for hard line breaks. Word-wrap long lines yourself before registering text. Kanban signs auto-wrap around 35 characters and may split words if a line is too long.

Color palette#

Twilight Princess message colors use a 6-byte control tag followed by visible text:

1A 06 FF 00 00 NN

NN is the color index (shown below). Bmg.color(name) returns these bytes as a Luau string. The on-screen RGB values come from the retail message font palette (bmgFontCCColor in the PC port, same table as vanilla TP).

Preview Bmg.color name Index Control tag (hex) Display RGBA Display #RRGGBB
white 0x00 1A 06 FF 00 00 00 0xFFFFFFFF #FFFFFF
red 0x01 1A 06 FF 00 00 01 0xF07878FF #F07878
green 0x02 1A 06 FF 00 00 02 0xAADC8CFF #AADC8C
blue 0x03 1A 06 FF 00 00 03 0xA0B4DCFF #A0B4DC
yellow 0x04 1A 06 FF 00 00 04 0xDCDC82FF #DCDC82
light_blue 0x05 1A 06 FF 00 00 05 0xB4C8E6FF #B4C8E6
purple 0x06 1A 06 FF 00 00 06 0xC8A0DCFF #C8A0DC
orange 0x08 1A 06 FF 00 00 08 0xDCAA78FF #DCAA78
dark_green 0x09 1A 06 FF 00 00 09 0x4BBE4BFF #4BBE4B
sky_blue 0x0A 1A 06 FF 00 00 0A 0x4B96D7FF #4B96D7
silver 0x0B 1A 06 FF 00 00 0B 0xBFBFBFFF #BFBFBF

Index 0x07 exists in the retail palette (same white as 0x00) but is not exposed by Bmg.color.

Shop / menu suffix. Not a foreground color. Terminates shelf strings for the shop renderer:

Bmg.color name Control tag (hex)
end_menu 1A 05 00 00 20

In Luau you can build tags manually when JSON or bundling would eat escape sequences:

local green = string.char(0x1A, 0x06, 0xFF, 0x00, 0x00, 0x02) -- same as Bmg.color("green")
// Prefer Bmg.color("green") in TypeScript mods
import * as Bmg from "Courage.Bmg"
const green = Bmg.color("green")

Example#

local Bmg = require("Courage.Bmg")

local text = Bmg.color("green")
    .. "You do not need to complete any dungeons."
    .. Bmg.color("white")
    .. "\n"
    .. Bmg.color("purple")
    .. "Dungeon entrances have been shuffled."
    .. Bmg.color("white")

Bmg.set("flow:0x232a", text)
import * as Bmg from "Courage.Bmg"

const text =
    Bmg.color("green") +
    "You do not need to complete any dungeons." +
    Bmg.color("white") +
    "\n" +
    Bmg.color("purple") +
    "Dungeon entrances have been shuffled." +
    Bmg.color("white")

Bmg.set("flow:0x232a", text)

Seed JSON#

Mods can ship precomputed overrides in seed data:

{
  "hints": [
    { "spot": "Midna", "type": "RequiredDungeons", "lines": ["You do not need to complete any dungeons."] }
  ],
  "bmg": {
    "overrides": {
      "0x0107": "Bombs  30 Rupees",
      "flow:0x232a": "You do not need to complete any dungeons.\nDungeon entrances have been shuffled."
    }
  }
}

Keys may be decimal, 0x hex, flow:..., or arc:.... Values are display strings. Use newlines inside a value for multi-line text.

Call Bmg.clear() before reloading a new seed if your mod swaps override tables at runtime.

Registering an override#

local Bmg = require("Courage.Bmg")

function MyMod:init()
    Bmg.set(0x72b0, "They say that Courage mods can replace gossip text.")
    Bmg.set("flow:0x232a", "Colored gossip goes on flow keys.")
end
import * as Bmg from "Courage.Bmg"

declare function mod(name: string): any

const MyMod = mod("my-mod")

MyMod.init = function () {
    Bmg.set(0x72b0, "They say that Courage mods can replace gossip text.")
    Bmg.set("flow:0x232a", "Colored gossip goes on flow keys.")
}

Bulk load#

local Bmg = require("Courage.Bmg")

local overrides = {
    ["0x72b0"] = "They say that the woods hide a secret.",
    ["0x72e0"] = "They say that Kakariko has stories to tell.",
    ["flow:0x232a"] = "Required-dungeons hint on the Castle Town sign.",
}

local count = Bmg.loadOverrides(overrides)
print("loaded", count, "message overrides")
import * as Bmg from "Courage.Bmg"

const overrides: Record<string, string> = {
    "0x72b0": "They say that the woods hide a secret.",
    "0x72e0": "They say that Kakariko has stories to tell.",
    "flow:0x232a": "Required-dungeons hint on the Castle Town sign.",
}

const count = Bmg.loadOverrides(overrides)
print("loaded", count, "message overrides")

Shop callbacks#

Use Bmg.onShop to react when the game builds shop shelf text or resolves a purchase dialog. This is how randomized shop mods swap item names, prices, and confirm prompts.

Callback Arguments Return
onSlots itemNos, msgIndices n/a
tryFlow vanillaItem, msgId Custom prompt string, or nil / undefined to fall through
shouldSkipBottleCheck vanillaItem true to skip the bottle-space check for that item (optional)

onSlots runs when shelf slots are populated. Use Bmg.set (and optionally Bmg.set("arc:255:0x...", ...)) to push per-slot text. tryFlow runs when the player selects an item. Return a string to replace the purchase prompt.

local Bmg = require("Courage.Bmg")

Bmg.onShop({
    onSlots = function(itemNos, msgIndices)
        for i, vanilla in ipairs(itemNos) do
            if vanilla ~= 0xFF then
                local msgIdx = msgIndices[i]
                local text = "Special Item!\n30 Rupees"
                Bmg.set(msgIdx, text)
                Bmg.set(string.format("arc:255:0x%04x", msgIdx), text)
            end
        end
    end,
    tryFlow = function(vanillaItem, msgId)
        if vanillaItem == 0x05 then
            return "Would you like to buy this replacement item?"
        end
        return nil
    end,
    shouldSkipBottleCheck = function(vanillaItem)
        return vanillaItem == 0x61 -- skip bottle check for a specific shop item
    end,
})
import * as Bmg from "Courage.Bmg"

Bmg.onShop({
    onSlots: (itemNos: number[], msgIndices: number[]) => {
        for (let i = 0; i < itemNos.length; i++) {
            const vanilla = itemNos[i]
            if (vanilla !== 0xFF) {
                const msgIdx = msgIndices[i]
                const text = "Special Item!\n30 Rupees"
                Bmg.set(msgIdx, text)
                Bmg.set(`arc:255:0x${msgIdx.toString(16).padStart(4, "0")}`, text)
            }
        }
    },
    tryFlow: (vanillaItem: number, msgId: number) => {
        if (vanillaItem === 0x05) {
            return "Would you like to buy this replacement item?"
        }
        return undefined
    },
    shouldSkipBottleCheck: (vanillaItem: number) => vanillaItem === 0x61,
})

Debugging#

Enable routing logs while developing:

local Bmg = require("Courage.Bmg")
Bmg.setDebug(true)
import * as Bmg from "Courage.Bmg"
Bmg.setDebug(true)

When debug is on, the engine logs which override key matched each message lookup ([bmg] applyStoredText ..., ROUTE HIT flow=... msgId=...).