# How .o2r and .otr Asset Files Work Internally in Lighthouse: A Deep Dive

> Uncover the internal workings of .o2r and .otr asset files in Lighthouse. Learn how these ZIP archives and texture containers are managed by the resource manager.

- Repository: [Harbour Masters/Lighthouse](https://github.com/HarbourMasters/Lighthouse)
- Tags: deep-dive
- Published: 2026-08-04

---

**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`](https://github.com/HarbourMasters/Lighthouse/blob/main/LighthouseModMenuWindow.cpp):

```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`](https://github.com/HarbourMasters/Lighthouse/blob/main/Engine.cpp). This code loads all language pack archives automatically:

```cpp
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`](https://github.com/HarbourMasters/Lighthouse/blob/main/AltBoldFont.cpp):

```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

```cpp
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

```cpp
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`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/UI/LighthouseModMenuWindow.cpp) | `.o2r` discovery, `ArchiveHasGameConfig()` validation |
| [`src/port/Engine.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Engine.cpp) (lines 333-348) | Bulk loading of language pack archives |
| [`src/port/Resource/Alt/AltBoldFont.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/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`](https://github.com/HarbourMasters/Lighthouse/blob/main/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.