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::Texture objects 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 langinfo metadata entry identifying the language
  • The standard assets/aGameConfig for 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 .o2r archives
  • 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

  • .o2r files are validated ZIP archives — The engine checks for assets/aGameConfig before registering them with ArchiveManager, enabling modular game content.
  • .otr files are streaming texture sources — Loaded individually through ResourceManager::LoadResourceProcess() for immediate GPU use.
  • Both leverage libultraship's archive abstraction — The Ship::O2rArchive class wraps libzip operations, providing uniform file access across multiple backing archives.
  • Language packs reuse the .o2r format — Same validation, same registration flow, with additional langinfo metadata.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →