Architecture

Architecture

Project Structure

icarus-pets/
├── mount_editor.py          # Main application — launches GUI, orchestrates tabs
├── ue4_parser.py            # Binary → Python: reads UE4 serialized properties
├── ue4_serializer.py        # Python → Binary: writes properties back to byte arrays
├── talent_data.py           # Game data: genetics, lineages, talent trees (auto-generated)
├── variation_data.py        # Game data: phenotype variations per creature (auto-generated)
├── bestiary_data.json       # Intermediate bestiary data (77 creatures)
├── VERSION                  # Version file (semver)
├── README.md
├── build.py                 # Build script for Windows .exe
├── mount_editor.spec        # PyInstaller spec file
├── .gitignore
├── .gitlab-ci.yml           # CI/CD pipeline configuration
│
├── editor/                  # GUI module package
│   ├── __init__.py
│   ├── mount_model.py       # Data model wrapping parsed props with a clean API
│   ├── overview_tab.py      # Tab: identity, lineage, vital stats, talent summary
│   ├── genetics_tab.py      # Tab: 7 genetic stats with spinboxes
│   ├── talents_tab.py       # Tab: full talent tree with rank controls
│   ├── advanced_tab.py      # Tab: raw property tree for power users
│   ├── bug_report.py        # In-app bug reporting dialog + GitLab API
│   └── tooltip.py           # Reusable hover tooltip widget
├── tests/                   # Test files
│   ├── sample_mounts/       # Bundled test fixtures (gitignore exception)
│   │   ├── EN-US/Mounts.json   # UTF-8 encoded, 6 mounts
│   │   └── FR-FR/Mounts.json   # UTF-16 LE encoded, 8 mounts
│   ├── run_all.py           # Unified test runner: auto-discovers all tests
│   ├── test_roundtrip.py    # Roundtrip test: parse → serialize → compare
│   ├── test_ue4_parser.py   # UE4 binary parser tests (28 tests)
│   ├── test_ue4_serializer.py # UE4 binary serializer tests (28 tests)
│   ├── test_mount_model.py  # MountModel API tests (28 tests)
│   ├── test_species_swap.py # Species swap core tests (13 tests)
│   ├── test_species_swap_edge.py # Species swap edge cases (22 tests)
│   ├── test_save_backup.py  # Save/backup/restore tests (14 tests)
│   ├── test_packaging.py    # Frozen-app paths & version tests (13 tests)
│   ├── test_talent_data.py  # Talent data integrity tests (35 tests)
│   ├── test_variation.py    # Phenotype variation tests (36 tests)
│   └── test_refresh.py      # Refresh tool tests (7 tests)
├── scripts/                 # Extraction & generation scripts
│   ├── pak_extract.py       # PAK file reader: lists and extracts zlib chunks
│   ├── pak_talent_extract.py # Talent extractor: parses D_Talents from pak chunks
│   ├── generate_talent_data.py  # Code generator: JSON → talent_data.py module
│   ├── extract_bestiary.py  # Bestiary extractor: parses creature names & lore from pak
│   ├── add_bestiary_to_talent_data.py  # Injects BESTIARY_DATA into talent_data.py
│   ├── refresh_talent_data.py  # One-command talent data refresh tool
│   ├── pak_variation_extract.py # Variation extractor: parses IcarusMount DataTable
│   ├── generate_variation_data.py # Code generator: JSON → variation_data.py module
│   ├── phenotype_analysis.py   # Phenotype variation analysis script
│   ├── check_test_changes.py   # CI: warns when source changes lack test changes
│   └── mount-bin.py         # CLI utility for analyzing binary data
├── docs/                    # Research & reference docs
│   ├── mount_talent_reference.md  # Human-readable talent reference
│   └── species_swap_research.md   # Research notes on species field mapping
│
├── wiki/                    # Wiki repo (separate git)
├── pak-files/               # Extracted pak data (gitignored)
├── build/                   # Build output (gitignored)
└── CGitLab-Runner/          # Runner config

Data Flow

Mounts.json
    │
    ▼
┌─────────────────────┐
│  JSON parsing        │  json.load() → list of mount entries
│  (mount_editor.py)   │  Each entry has RecorderBlob.BinaryData
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  UE4 Binary Parser   │  BinaryReader + parse_properties()
│  (ue4_parser.py)     │  Byte array → list of property dicts
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  Mount Model         │  MountModel wraps props with .name, .genetics,
│  (mount_model.py)    │  .talents, .health, etc. — clean getter/setter API
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  GUI Tabs            │  Each tab calls model.load() to populate widgets
│  (overview, genetics,│  User edits values via spinboxes, dropdowns, etc.
│   talents, advanced) │  Each tab calls model.save() to push changes back
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  UE4 Serializer      │  props_to_binary_array()
│  (ue4_serializer.py) │  Property dicts → byte array (JSON-compatible int list)
└─────────┬───────────┘
          │
          ▼
    Mounts_edited.json     (new file — original never modified)

Pak Extraction Pipeline

Talent data is extracted directly from the game's pak files rather than manually entered:

data.pak (Icarus game file)
    │
    ▼
┌─────────────────────┐
│  pak_extract.py      │  Finds zlib streams, decompresses chunks
│                      │  Saves as pak-files/extracted_*.bin
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│  pak_talent_extract  │  Concatenates sequential chunks (offset 1420000–1570000)
│  .py                 │  Parses D_Talents JSON: Name, DisplayName, Description,
│                      │  TalentTree, Rewards (stat + values per rank)
└─────────┬───────────┘  → pak_talents_extracted.json (519 entries, 30 trees)
          │
          ▼
┌─────────────────────┐
│  generate_talent_   │  Maps pak tree names to editor types
│  data.py            │  Generates Python module with all definitions
└─────────┬───────────┘
          │
          ▼
    talent_data.py         (26 types, 457 rankable talents, 150KB)

To regenerate talent data after a game update:

python scripts/pak_extract.py              # Extract zlib chunks from data.pak
python scripts/pak_talent_extract.py       # Parse talent entries from chunks
python scripts/generate_talent_data.py     # Generate talent_data.py module

Bestiary Extraction Pipeline

Creature display names and lore are extracted from the game's bestiary DataTable:

data.pak (Icarus game file)
    │
    ▼
┌─────────────────────┐
│  extract_bestiary.py │  Scans pak chunks for D_Bestiary JSON
│                      │  Extracts CreatureName, Lore1, Lore2 per creature
└─────────┬───────────┘  → bestiary_data.json (77 creatures)
          │
          ▼
┌──────────────────────────┐
│  add_bestiary_to_talent  │  Maps 26 mount types to bestiary keys
│  _data.py                │  Injects BESTIARY_DATA dict into talent_data.py
└──────────────────────────┘

The bestiary provides: - Display names — "Hyena" for DesertWolf, "Mammoth" for WoollyMammoth, etc. - Lore1 — Primary bestiary description shown in the swap preview - Lore2 — Extended lore shown as mouseover tooltip

5 types have no game bestiary entry and use manually-crafted placeholders: Bull, Chew, Pig, Rooster, TundraMonkey.

Variation Extraction Pipeline

Phenotype variation data is extracted from the same pak file alongside talents:

data.pak (Icarus game file)
    │
    ▼
┌─────────────────────┐
│  pak_extract.py      │  (shared step — same chunks used for talents)
└─────────┬───────────┘
          │
          ▼
┌──────────────────────────┐
│  pak_variation_extract.py │  Parses IcarusMount DataTable for Variations arrays
│                           │  Extracts MeshMaterial, GFurMaterial, Weighting
└──────────┬───────────────┘  → pak_variations_extracted.json (3 types, 24 variants)
           │
           ▼
┌──────────────────────────┐
│  generate_variation_     │  Computes rarity tiers from weightings
│  data.py                 │  Generates Python module with public API
└──────────┬───────────────┘
           │
           ▼
    variation_data.py          (Buffalo/Moa/Wolf, 8 variations each)

To regenerate all game data after a game update (one command):

python scripts/refresh_talent_data.py    # Runs all 5 extraction steps

Bug Report Data Flow

User clicks "🐛 Report Bug" in toolbar
    │
    ▼
┌─────────────────────────┐
│  BugReportDialog         │  Modal dialog with structured form
│  (bug_report.py)         │  Collects title, description, steps, expected
└─────────┬───────────────┘
          │  User clicks Submit
          ▼
┌─────────────────────────┐
│  Background thread       │  1. Collect system info (version, OS, mount context)
│                          │  2. Capture window screenshot (optional, via PIL)
│                          │  3. Upload files to GitLab /uploads endpoint
│                          │  4. POST issue to icarus-bug-reports project
└─────────┬───────────────┘
          │
          ▼
    GitLab Issue created     (icarus-bug-reports project, ID=2)
    with labels + attachments

Species Swap Data Flow

User selects target species in swap dropdown
    │
    ▼
┌─────────────────────┐
│  Bestiary preview    │  BESTIARY_DATA provides display name, lore, lore2
│  (overview_tab.py)   │  get_talent_tree() computes compatibility stats
└─────────┬───────────┘
          │  User confirms
          ▼
┌─────────────────────┐
│  MountModel.swap_    │  Updates 6 identity fields from SPECIES_DATA
│  species()           │  Rewrites ObjectFName/ActorPathName patterns
│  (mount_model.py)    │  Calls remap_talents() for display-name matching
└─────────┬───────────┘
          │
          ▼
    All tabs reload from updated model

Testing

test_roundtrip.py — Binary Roundtrip

Parses every mount across all locale fixtures in tests/sample_mounts/, serializes back to binary, and compares byte-for-byte. Auto-discovers locale subdirectories (EN-US, FR-FR, etc.) and handles both UTF-8 and UTF-16 LE encoded save files.

Currently tests 14 mounts across 2 locales (6 EN-US + 8 FR-FR), including non-ASCII mount names with accented characters.

To add a new locale sample, just drop a Mounts.json into a new subdirectory (e.g., tests/sample_mounts/DE-DE/).

python tests/test_roundtrip.py

test_species_swap.py — Species Swap Core (13 tests)

Core logic (#20): - SPECIES_DATA completeness — all 26 types have actor_class, ai_setup, mount_type - get_swap_targets() — same-category filtering, self-exclusion, unknown type handling - remap_talents() — display-name matching, rank transfer, lost point counting, rank clamping - MountModel.swap_species() — identity field updates, path rewriting, talent remapping - Cross-category rejection — ValueError on mount→combatpet - Wolf→Boar swap — combatpet-to-combatpet with known talent overlap

Additional cases (#22): - Buffalo→Horse — mount-to-mount with HorseStandard talent suffix, 3 shared / 5 lost - Full roundtrip after swap — serialize→parse→serialize produces identical binary (639 bytes) - Exhaustive cross-category check — all 26 types verified: no self-targets, no cross-category leaks

python tests/test_species_swap.py

test_species_swap_edge.py — Species Swap Edge Cases (22 tests)

Full matrix (#47): - Every type → every valid target (244 swaps across all 26 types) - Swap target count validation per category

Double swap: - A→B→A preserves shared talents (lossless roundtrip) - A→B→A with type-specific talents (lossy roundtrip, correct accounting)

Max-rank and overflow: - All talents at max rank for every type, swap to first target, verify point accounting - Rank exceeding new tree's max_rank → clamped, overflow refunded

Edge cases: - Same-type swap (no-op), unknown type (ValueError), empty talents, zero-rank, negative rank - Nonexistent talent names refunded, corrupted talent data resilience - Cross-category: mount→farm, combatpet→farm, farm→mount all rejected

Specific pairs: Cat→Dog, Chicken→Sheep, Mammoth→Zebra, field preservation, JSON consistency

python tests/test_species_swap_edge.py

tests/run_all.py — Unified Test Runner (#51)

Auto-discovers and runs all test_*.py files with timing and summary:

python tests/run_all.py           # run all tests
python tests/run_all.py --list    # list discovered tests

Key Design Decisions

Modular Tab Architecture

Each tab is a self-contained ttk.Frame subclass with load(model) and save() methods. This makes it easy to add new tabs without touching existing code.

MountModel as Abstraction Layer

The model provides a clean Pythonic API (model.name, model.get_genetics(), model.set_talents({...})) so tabs never need to understand the raw property dict structure. Changes flow through the model back to the same property dicts the serializer reads.

In-Place Property Editing

The Advanced tab edits property dicts directly (by reference). The Overview/Genetics/Talents tabs work through the MountModel which also modifies the same dicts. This means all edits converge on a single data source — no sync issues.

Binary-Perfect Roundtrip

The parser and serializer are designed so that serialize(parse(data)) == data for all tested save files. This ensures edits don't corrupt unrelated data.

Auto-Generated Talent Data

Rather than manually entering talent definitions, we extract them directly from the game's pak files. This ensures accuracy and makes it trivial to update after game patches — just re-run the extraction pipeline.

No External Dependencies

The entire project uses only Python 3 standard library (tkinter, json, struct, zlib). No pip install required.


See also: GUI Guide · Binary Format · Species & Types · Talent & Genetics Data

Back to Docs