How .o2r and .otr Asset Files Work Internally in Lighthouse: A Deep Dive
Lighthouse uses .o2r files as ZIP-based asset archives validated by an aGameConfig marker, and .otr files as lightweight texture containers loaded on-demand through the resource manager.
The HarbourMasters/Lighthouse project implements a modular asset system that allows mods, language packs, and custom textures without modifying the base game. Understanding how .o2r and .otr files function internally reveals the engine's clean separation between archive management and resource streaming.
What Are .o2r and .otr Files?
Both formats serve distinct purposes in Lighthouse's modding ecosystem:
| Format | Purpose | Internal Representation |
|---|---|---|
| .o2r | Complete asset archives (mods, language packs, game data) | ZIP archive with mandatory assets/aGameConfig entry |
| .otr | Individual texture resources | Compressed image data loaded as Fast::Texture |
The engine treats .o2r as bundle archives registered globally, while .otr files are discrete resources fetched on demand.
How .o2r Archives Work: Discovery to Registration
Step 1: File Discovery and Validation
When Lighthouse scans for mods, it validates each .o2r before accepting it. The check happens in LighthouseModMenuWindow.cpp:
bool IsValidExtension(std::string extension) {
return StringHelper::IEquals(extension, ".o2r");
}
static bool ArchiveHasGameConfig(const std::filesystem::path& archivePath) {
int err = 0;
zip_t* z = zip_open(archivePath.string().c_str(), ZIP_RDONLY, &err);
if (z == nullptr) return false;
bool found = zip_name_locate(z, "assets/aGameConfig", 0) >= 0;
zip_close(z);
return found;
}
The zip_name_locate call confirms the archive contains the required metadata file. Without assets/aGameConfig, the archive is rejected as invalid.
Step 2: Archive Registration
Valid archives are handed to the ArchiveManager via Engine.cpp. This code loads all language pack archives automatically:
for (auto& p : std::filesystem::directory_iterator(langPath)) {
if (p.is_regular_file() && p.path().extension() == ".o2r") {
Ship::Context::GetRawInstance()
->GetResourceManager()
->GetArchiveManager()
->AddArchive(p.path().generic_string());
}
}
The Ship::O2rArchive class (underlying AddArchive) wraps libzip and exposes standard archive operations: HasFile, GetFile, GetFileLength, and stream-based reading.
Step 3: Runtime Asset Resolution
Once registered, any game system requesting a file path searches across all loaded .o2r archives transparently. The ResourceManager coordinates this lookup without callers knowing which archive contains the data.
How .otr Texture Files Work
While .o2r provides bulk storage, .otr handles individual texture resources efficiently. The pattern appears in AltBoldFont.cpp:
std::shared_ptr<Fast::Texture> loadTexture(const char* otrPath) {
if (otrPath == nullptr) return nullptr;
return Ship::Context::GetRawInstance()
->GetResourceManager()
->LoadResourceProcess(otrPath, false);
}
Key characteristics of .otr handling:
- Direct loading — No archive registration required; paths resolve immediately
- Texture specialization — Returns
Fast::Textureobjects ready for GPU upload - ImGui integration — Loaded textures bind directly to ImGui rendering via OpenGL/D3D handles
The LoadResourceProcess call with false indicates non-blocking (synchronous) loading suitable for UI elements that must appear immediately.
Language Packs: A Specialized .o2r Use Case
According to the project's LANGUAGE PACKS.md documentation, language packs follow the same .o2r structure but include:
- Region-specific assets (translated text, localized graphics)
- A
langinfometadata entry identifying the language - The standard
assets/aGameConfigfor compatibility validation
When users switch languages, Lighthouse unloads the current pack and registers the new .o2r through the identical ArchiveManager flow.
ArchiveManager: The Central Hub
Both file types ultimately route through Ship::ArchiveManager, defined in the libultraship dependency. This singleton maintains:
- A searchable list of all registered
.o2rarchives - Priority ordering (later archives can override earlier ones)
- File existence caching to minimize ZIP operations
The manager's interface enables the engine to treat multiple physical archives as a single virtual filesystem.
Practical Code Examples
Loading a Custom Mod Archive at Runtime
std::string modPath = "mods/my_custom_mod.o2r";
auto& archiveMgr = Ship::Context::GetRawInstance()
->GetResourceManager()
->GetArchiveManager();
if (archiveMgr->AddArchive(modPath) != nullptr) {
SPDLOG_INFO("Successfully registered mod archive: {}", modPath);
// Assets are now accessible through normal resource paths
if (archiveMgr->HasFile("textures/custom/sprite.png")) {
auto tex = loadTexture("textures/custom/sprite.png");
// Use texture...
}
} else {
SPDLOGE("Failed to load mod archive: {}", modPath);
}
Rendering an .otr Texture in ImGui
const char* otrPath = "mods/ui_elements/hud_icons.otr";
auto texture = loadTexture(otrPath);
if (texture) {
ImGui::Image(
reinterpret_cast<void*>(texture->GetGLHandle()),
ImVec2(256, 256)
);
}
Key Source Locations
| File | Responsibility |
|---|---|
src/port/UI/LighthouseModMenuWindow.cpp |
.o2r discovery, ArchiveHasGameConfig() validation |
src/port/Engine.cpp (lines 333-348) |
Bulk loading of language pack archives |
src/port/Resource/Alt/AltBoldFont.cpp |
.otr texture loading via loadTexture() |
docs/modding/LANGUAGE PACKS.md |
Language pack structure and requirements |
libultraship (via Ship::O2rArchive) |
Underlying ZIP archive implementation |
Summary
.o2rfiles are validated ZIP archives — The engine checks forassets/aGameConfigbefore registering them withArchiveManager, enabling modular game content..otrfiles are streaming texture sources — Loaded individually throughResourceManager::LoadResourceProcess()for immediate GPU use.- Both leverage libultraship's archive abstraction — The
Ship::O2rArchiveclass wraps libzip operations, providing uniform file access across multiple backing archives. - Language packs reuse the
.o2rformat — Same validation, same registration flow, with additionallanginfometadata. - Runtime mod loading is fully dynamic — Drop files into
mods/(or subdirectories like~lang/) and the engine discovers and registers them automatically.
Frequently Asked Questions
What happens if an .o2r file lacks the aGameConfig entry?
Lighthouse rejects the archive. The ArchiveHasGameConfig() function in LighthouseModMenuWindow.cpp returns false, preventing AddArchive() from being called. The mod will not appear in the UI or be accessible to the game.
Can .o2r archives override base game assets?
Yes. The ArchiveManager searches registered archives in registration order, and later archives take precedence. This allows mods to replace textures, models, or configuration files without modifying original data.
Are .otr files required to be inside .o2r archives?
No. .otr files operate independently and can reside anywhere in the filesystem or mods directory. The loadTexture() function accepts absolute or relative paths and resolves them through the standard resource loader.
How does Lighthouse handle .o2r file conflicts?
When multiple archives contain the same file path, the ArchiveManager returns the version from the most recently registered archive. This prioritization enables clean mod stacking where later mods override earlier ones.
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 →