How to Expand - thethiny/NRS-Asset-Manager GitHub Wiki

How to Expand: Adding a New Game

This guide explains how to add support for a new NRS game (e.g., MKX, MK1, IJ1). All NRS games share the same engine lineage, so the structure is consistent.

Prerequisites

You need:

  1. Sample .xxx files from the target game
  2. The correct Oodle DLL version (check the game's install directory)
  3. A hex editor to inspect headers and compare with known formats

Step 1: Create the Game Module

Create the directory structure mirroring existing games:

mk_utils/nrs/<game_code>/
├── __init__.py
├── enums.py              # Game-specific enums (compression, pixel formats, etc.)
├── ue3_common.py         # Structs: header, archive base, table entries
├── archive.py            # UE3Asset class (compressed .xxx parser)
├── midway.py             # MidwayAsset class (decompressed format parser)
├── ue3_properties.py     # UE3 property deserializer
└── class_handlers/
    ├── __init__.py       # Handler registration
    ├── database.py       # Database export → JSON
    └── texture2d.py      # Texture2D export → DDS/PNG

Step 2: Identify the Header

Open a .xxx file in a hex editor. The first 4 bytes should be C1 83 2A 9E (magic 0x9E2A83C1).

Read the FPackageFileSummary serialization order from the game or by comparing with known formats. Key fields to identify:

class NewGameAssetHeader(Struct):
    _fields_ = [
        ("magic", c_uint32),              # Always 0x9E2A83C1
        ("file_version", c_uint16),       # Identifies the engine version
        ("licensee_version", c_uint16),   # Usually 157
        ("total_header_size", c_uint32),  # Where export data starts
        ("midway_team_four_cc", c_char * 4),  # Game identifier ("MK11", "DCF2", etc.)
        # ... remaining fields vary by version
        ("compression_flag", c_uint32),   # Last field before compressed chunks
    ]

Critical: Compare field order and sizes with MK11 (104 bytes) and IJ2 (100 bytes). The header evolved between games — fields may be added, removed, or reordered.

Step 3: Define the Compressed Chunk

The FCompressedChunk struct defines how data blocks map between compressed and decompressed space. Verify the field sizes — IJ2 uses:

u64 uncompressed_offset, u32 uncompressed_size, u64 compressed_offset, u32 compressed_size

While MK11 uses all u64 fields with named sub-packages.

Step 4: Define Table Entry Structs

Export Table (FObjectExport)

This is the most version-sensitive struct. The serialization order from MK11:

ClassIndex(4), OuterIndex(4), ObjectName(FName=4+4), SuperIndex(4),
ObjectFlags(8), ObjectGuid(16), PackageName(4), Unk(4),
SerialSize(4), SerialOffset(8), Unk2(8), Unk3(4) = 76 bytes

IJ2:

ClassIndex(4), SuperIndex(4), OuterIndex(4), ObjectName(FName=4+4),
ArchetypeIndex(4), ReferencedObjects(4), ObjectFlags(8), ObjectGuid(16),
SerialSize(4), SerialOffset(8), ComponentMap(variable), ExportFlags(4) = 72+ bytes

Key differences to check:

  • Field ORDER (especially name/suffix, super/outer)
  • Whether FName is u32+u32 or u64
  • Variable-size fields (ComponentMap in IJ2)

Import Table (FObjectImport)

MK11 uses 20 bytes (5 x i32 with resolve_object). IJ2 uses 28 bytes (3 FNames + 1 i32). Check if your game's imports use name indices or resolve_object references.

Step 5: Implement the Archive Class

class NewGameUE3Asset(NewGameArchive):
    VERSION_RANGE = range(780, 810)  # Accepted file_version range
    
    def __init__(self, path: str, extra_path: str = ""):
        super().__init__(path, extra_path)

    def parse(self, skip_bulk: bool = False):
        self.header = self.parse_header()
        self.compression_mode = NewGameCompressionType(self.header.compression_flag)
        self.compressor = self.get_compressor(self.compression_mode)
        # Parse compressed chunks, filename, etc.

    def to_midway(self, skip_bulk: bool = False):
        buffer = self._MidwayBuilder.from_asset(self, skip_bulk)
        return NewGameMidwayAsset(buffer, self.psf_source)

Must implement: parse_header(), parse(), to_midway(), parse_all(), dump()

Step 6: Implement the Midway Class

class NewGameMidwayAsset(NewGameArchive):
    def parse(self, resolve=True, skip_bulk=False):
        self.parse_summary()
        self.file_name = self.parse_file_name()
        # Parse file tables if applicable
        self.name_table = list(self.parse_name_table())
        self.import_table = list(self.parse_uobject_table(...))
        self.export_table = list(self.parse_uobject_table(...))
        if resolve:
            self.resolve_table_info(self.import_table)
            self.resolve_table_info(self.export_table)

Must implement: parse_summary(), validate_file(), parse(), dump(), parse_and_save_export()

Step 7: Implement Properties and Handlers

Copy the UE3 property parser from the closest game version and adjust:

  • Property size field (u32 vs u64)
  • FName serialization (u32+u32 vs u64)
  • Known array/map key types

Create handlers following the ClassHandler base class:

class NewGameDatabaseHandler(ClassHandler):
    HANDLED_TYPES = {"somedatabasetype"}
    
    def parse(self):
        # Parse UE3 properties from self.mm
        
    def save(self, data, export, asset_name, save_dir, instance):
        # Write output file

Step 8: Create the Extractor Script

# mk_utils/scripts/newgame_extractors.py
def extract_all(files, output_dir="extracted", overwrite=False):
    for file_info in files:
        asset = NewGameUE3Asset(file_path, extra_path)
        midway = asset.parse_all(save_path=output_dir)
        for export in midway.export_table:
            handler = newgame_handlers.get(export.class_.name.lower())
            if handler:
                midway.parse_and_save_export(export, handler["handler_class"], output_dir, overwrite)

Step 9: Add to main.py Auto-Detection

Register the game in main.py's game registry:

GAME_REGISTRY = [
    (IJ2UE3Asset, "DCF2", range(700, 750)),
    (MK11UE3Asset, "MK11", range(760, 800)),
    (NewGameUE3Asset, "NEWG", range(800, 850)),
]

Step 10: Add Tests

Create test files following the existing pattern:

tests/<game_code>/
├── test_<game>_gamedata.py       # Test files in gamedata/<game>
├── test_<game>_pass_validation.py # Test with game install files
└── processors/
    ├── test_database.py
    └── test_texture2d.py

Checklist

  • Header struct matches hex dump
  • Compressed chunks decompress correctly
  • Export table entries parse with correct offsets (chain contiguously)
  • Final export table position matches total_header_size
  • Name table resolves correctly
  • Import/export resolution works (resolve_object)
  • UE3 properties parse without errors
  • Class handlers produce valid output
  • Tests pass for 3+ sample files
⚠️ **GitHub.com Fallback** ⚠️