How Romhacks Are Extracted and Loaded as Mods in Lighthouse
Lighthouse converts romhacks into .o2r overlay files through a multi-stage extraction pipeline that generates slim mod overlays, places them in mods/~romhacks/, enables exactly one romhack at a time, and requires a restart to apply the modified aGameConfig.
Romhacking in HarbourMasters/Lighthouse—the PC port of Banjo-Kazooie—works differently than traditional patching. Instead of distributing modified ROM files, Lighthouse treats romhacks as specialized mod overlays that override the base game's configuration blob. This article traces the complete extraction and loading pipeline from the UI button press through to the exclusive enablement mechanism.
The Romhack-as-Mod Architecture
A Lighthouse romhack is fundamentally an .o2r overlay containing a modified aGameConfig—the serialized game configuration that dictates level data, object behaviors, and other core parameters. Unlike standard mods that add assets alongside the base game, romhack overlays replace critical game state.
This design offers two advantages:
- Isolation: Each romhack lives in its own overlay file, preventing file conflicts.
- Exclusivity: The engine enforces that exactly one romhack loads at any time, avoiding configuration collisions.
User-Initiated Extraction Flow
Step 1: UI Registration and ROM Selection
The entry point resides in src/port/UI/LighthouseModMenuWindow.cpp. The "Generate Romhack from ROM" button registers with Lighthouse's widget system at lines 605–622:
// LighthouseModMenuWindow.cpp (lines 605–622)
generateRomhackWidget = {
.name = "Generate Romhack from ROM",
.type = WidgetType::WIDGET_BUTTON
};
generateRomhackWidget.Callback([](WidgetInfo&) {
LighthouseGui::RegisterPopup(
"Generate Romhack from ROM",
"Select a romhack ROM to extract as a mod overlay. Torch will generate a slim mod o2r in your mods/~romhacks/ folder...",
"Select ROM", "Cancel",
[]() { RequestInlineModExtraction(); }, nullptr);
});
LighthouseGui::mLighthouseMenu->AddSearchWidget({
generateRomhackWidget, "Settings", "Romhack Menu", "Top",
"generate romhack rom extract overlay"
});
When confirmed, the popup callback invokes RequestInlineModExtraction() (lines 714–718), which opens a file picker for the source N64 ROM.
Step 2: Launching the Extraction Worker
RequestInlineModExtraction() delegates to StartInlineRomExtraction(false), creating a GameExtractor instance and spawning a detached worker thread:
// LighthouseModMenuWindow.cpp (lines 889–898)
static void BeginInlineExtraction(std::shared_ptr<GameExtractor> extractor, bool langPack) {
sInlineFile = extractor->GetRomPath();
sInlineExtracting = true;
std::thread([ex = std::move(extractor)]() mutable {
const bool ok = ex->GenerateOTR(sInlineCount, sInlineTotal, "bk");
sInlineResult = ok ? 1 : 2;
sInlineExtracting = false;
}).detach();
}
The GameExtractor::GenerateOTR() method—declared in src/port/Extractor/GameExtractor.h—performs the actual heavy lifting:
- Reads and validates the N64 ROM header
- Parses assets using the Torch extraction pipeline
- Generates a slim overlay (.o2r) containing base assets plus the romhack's
aGameConfig
Progress propagates through atomic counters:
sInlineCount— assets processedsInlineTotal— total assets to processsPhase— current extraction phasesInlineResult— completion status (0=running,1=success,2=failure)
Post-Extraction Processing and Overlay Placement
Step 3: Moving the Generated Overlay
DrawInlineModExtraction() polls sInlineResult each frame. On success (result == 1), the code relocates the overlay to the romhacks directory (lines 441–455):
// LighthouseModMenuWindow.cpp (post-extraction handling)
if (result == 1) {
std::filesystem::path produced(GameExtractor::sLastOutputPath);
std::filesystem::path romhacksDir =
std::filesystem::path(Ship::Context::GetPathRelativeToAppDirectory("mods"))
/ ROMHACKS_DIR; // "~romhacks"
std::filesystem::path dest = romhacksDir / produced.filename();
std::filesystem::create_directories(romhacksDir);
std::filesystem::rename(produced, dest);
SetSoleEnabledRomhack(dest.stem().string()); // enable exclusively
// ... "Mod Installed" popup triggers GameEngine::RequestRelaunch()
}
The ROMHACKS_DIR macro expands to "~romhacks" (defined at line 86), establishing the canonical path mods/~romhacks/.
Exclusive Romhack Enablement
Step 4: Setting the Sole Active Romhack
The SetSoleEnabledRomhack() function (lines 57–89) implements Lighthouse's mutual exclusion guarantee:
// LighthouseModMenuWindow.cpp (lines 57–89)
void SetSoleEnabledRomhack(const std::string& romhackBasename) {
// 1. Verify base game compatibility first
if (!BaseGameSupportsRomhacks()) {
// Show warning: bk.o2r must be US v1.0
return;
}
// 2. Scan entire mods/ tree and adjust enable states
// 3. Enable only the specified romhack overlay
// 4. Update CVars directly (bypassing in-memory list)
SaveConsoleVariablesNextFrame();
}
Before enabling any overlay, BaseGameSupportsRomhacks() verifies that bk.o2r matches the US v1.0 release (lines 66–71). Mismatched base game versions trigger a warning popup and abort the enablement—romhack overlays often depend on specific memory layouts and asset offsets.
The function performs a complete mods tree scan, identifying overlays via IsRomhackOverlay() (lines 216–223), then updates EnabledMods and DisabledMods CVars directly rather than through the usual UI-mediated path. This raw CVar manipulation is necessary because the mod list hasn't been refreshed yet.
Romhack Detection and Compatibility Systems
SHA-1 Validation for Custom Code
Romhacks may include custom code patches. Lighthouse validates these against known hashes stored in src/port/Romhack/RomhackTable.h (lines 14–21):
// RomhackTable.h
struct RomhackEntry {
const char* name;
const char* sha1Hash; // Custom code blob hash
uint32_t configVersion; // aGameConfig format version
// ...
};
The RomhackConfig.cpp parser cross-references extracted overlays against this table, ensuring compatible custom-code handling and proper aGameConfig deserialization.
Runtime Detection Utilities
Two detection functions support the mod manager's filtering needs:
// LighthouseModMenuWindow.cpp (lines 216–223)
bool IsRomhackOverlay(const std::string& name);
bool IsInRomhacksFolder(const std::filesystem::path& path);
These enable UI differentiation between standard mods and romhack overlays throughout the interface.
Restart and Final Application
Step 5: Applying the Romhack
After SetSoleEnabledRomhack() succeeds, Lighthouse displays a "Mod Installed" popup with a single actionable outcome: GameEngine::RequestRelaunch(). Restart is mandatory because:
aGameConfigloads during early initialization- Overlay mounting occurs before the main loop begins
- CVars written to disk must be re-read from fresh process state
On relaunch, the enabled romhack overlay mounts over bk.o2r, its modified aGameConfig deserializes through RomhackConfig.cpp, and the custom code patches apply based on RomhackTable.h validation.
Summary
- Extraction:
GameExtractor::GenerateOTR()creates slim .o2r overlays from N64 ROMs via a background thread with atomic progress reporting. - Placement: Successful extractions move to
mods/~romhacks/(theROMHACKS_DIRlocation). - Exclusivity:
SetSoleEnabledRomhack()enforces exactly one active romhack by full-tree scanning and direct CVar manipulation. - Compatibility: US v1.0 base game verification and SHA-1 hash tables in
RomhackTable.hprevent invalid configurations. - Activation: Restart requirement ensures clean
aGameConfigloading in the next process instance.
Frequently Asked Questions
What file format do Lighthouse romhacks use?
Lighthouse romhacks use the .o2r (OTR overlay) format—identical to standard mods but with a modified aGameConfig blob that overrides base game parameters. These reside exclusively in mods/~romhacks/ rather than the general mods directory.
Can multiple romhacks be active simultaneously?
No. The SetSoleEnabledRomhack() function explicitly disables all other romhack overlays before enabling a new one. This mutual exclusion prevents aGameConfig collisions that would destabilize the engine. You must restart to switch romhacks.
Why does the base game need to be US v1.0?
Romhacks often depend on specific memory layouts, asset offsets, and code patterns present only in the US v1.0 release. The BaseGameSupportsRomhacks() check in LighthouseModMenuWindow.cpp (lines 66–71) validates your bk.o2r against this reference version before allowing any romhack enablement.
Where are romhack SHA-1 hashes defined?
Known romhack custom-code hashes are registered in src/port/Romhack/RomhackTable.h (lines 14–21). The RomhackConfig.cpp parser uses this table to identify compatible romhacks and apply appropriate deserialization logic for their aGameConfig structures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →