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:
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:
Example#
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#
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:
When debug is on, the engine logs which override key matched each message lookup ([bmg] applyStoredText ..., ROUTE HIT flow=... msgId=...).