Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GRF Sprite Extractor

Extract real sprites from Transport Tycoon / OpenTTD NewGRF (.grf) files to PNG, with optional renaming from in-GRF aircraft names (Action4 → Action3 → Action2 → Action1).

Verified on Real_Aircrafts_Beta.grf (container v2): 2407 PNGs, 58 aircraft names, 2407/2407 renamed.

Requirements

  • Python 3.10+
  • Pillow
pip install pillow

Quick start

GUI (recommended)

python grf_extract.py
# or double-click:
.\Open GUI.bat
Control Purpose
GRF file Input .grf
Output folder Where PNGs are written (default <stem>_sprites/)
Rename checkbox Map sprites via Action4 aircraft names
Extract Runs on a background thread
Open output folder Opens Explorer on the result dir

CLI

python grf_extract.py path\to\file.grf
python grf_extract.py path\to\file.grf D:\out\sprites
python grf_extract.py path\to\file.grf --no-rename
python grf_extract.py --gui

Architecture

flowchart LR
  GRF[".grf file"] --> DET{Container?}
  DET -->|v2 magic| V2[Data section + Sprite section]
  DET -->|else| V1[Flat sprite stream]
  V2 --> MAP[AircraftNameMapper<br/>Action 8/4/3/2/1]
  V2 --> DEC[decompress_sprite LZ77]
  V1 --> DEC
  DEC --> IMG[sprite_to_image → PNG]
  MAP --> NAME["{vid}_{name}__id{sid}_...png"]
  IMG --> NAME
Loading

Container v2 layout (grf.txt)

[10-byte magic] [DWORD sprite_offs] [BYTE data_compr]
[ data section: pseudo FF + FD sprite refs … ] [DWORD 0]
[ sprite section: id, size, image payloads … ] [DWORD 0]

Magic:

CONTAINER_V2_MAGIC = bytes([0x00, 0x00, 0x47, 0x52, 0x46, 0x82, 0x0D, 0x0A, 0x1A, 0x0A])

Data-section entry:

size = u32()          # payload length (does NOT include info byte)
info = u8()           # 0xFF = pseudo (Action*), 0xFD = sprite-section ref
payload = read(size)  # if FD: DWORD sprite_id

Decompression (GRF LZ77)

Same algorithm as OpenTTD DecodeSingleSprite():

def decompress_sprite(cur, target_size: int) -> bytearray:
    out = bytearray(target_size)
    pos = 0
    while pos < target_size:
        code = cur.s8()  # signed
        if code >= 0:
            # Verbatim: length = code, or 128 if code == 0
            size = 0x80 if code == 0 else code
            out[pos:pos + size] = cur.read(size)
            pos += size
        else:
            # Back-reference (may overlap → copy byte-by-byte)
            lofs = cur.u8()
            offset = ((code & 7) << 8) | lofs
            length = -(code >> 3)  # arithmetic shift
            src = pos - offset
            for i in range(length):
                out[pos + i] = out[src + i]
            pos += length
    return out
code Meaning
>= 0 Copy next code bytes (or 128 if code == 0)
< 0 Copy length = -(code >> 3) bytes from pos - offset

Aircraft name mapping (GRFSpecs)

Action Feature Role
Action8 GRF title / GRFID
Action4 03 aircraft vehicle_id → name
Action3 03 vehicle_id → Action2 set-id
Action2 / VA2 03 set-id → spriteset indices / chain
Action1 03 spriteset → following FD sprite IDs

Load semantics (OpenTTD-compatible snapshots)

Action2 IDs are reused in NewGRFs. OpenTTD stores a group pointer when Action3 is parsed; later redefinitions of the same ID do not rewrite earlier vehicles.

This tool mirrors that by freezing sprite-id lists when Action2 is defined:

class AircraftNameMapper:
    def __init__(self):
        self.spritesets = {}      # set_index -> {ids, nent}
        self.a2_sprites = {}      # action2_id -> frozen [sprite_id, ...]
        self.names = {}           # vehicle_id -> sanitized name
        self.sprite_owners = defaultdict(set)

    def _snapshot_basic(self, idxs):
        out = []
        for idx in idxs:
            block = self.spritesets.get(idx)
            if block:
                out.extend(block["ids"])
        return out

    def _snapshot_var(self, payload):
        # Variational Action2: union of referenced Action2 snapshots
        out, seen = [], set()
        for ref in _extract_va2_refs(payload):
            if ref not in seen:
                seen.add(ref)
                out.extend(self.a2_sprites.get(ref, []))
        return out

On Action3, copy the current snapshot onto the vehicle. Small Action1 blocks (≤64 sprites) right after an Action4 are also attributed locally (covers purchase/views defined after Action3).

Output filename pattern

{vid:03d}_{Aircraft Name}__id{sprite_id}_{zoom}_{depth}_{W}x{H}.png

Examples:

041_Bell 206B JetRanger II__id55_normal_32bpp_50x40.png
055_Airbus A320-200__id593_4xin_32bpp_200x120.png
098_Lockheed L-1649A Starliner__id2965_normal_32bpp_96x64.png

Also written: aircraft_sprite_map.json (vehicle ↔ sprite-id map).

Programmatic use

from pathlib import Path
from grf_extract import detect_and_extract, build_aircraft_mapper

grf = Path("Real_Aircrafts_Beta.grf")
outdir = Path("out_sprites")
outdir.mkdir(exist_ok=True)

def on_progress(done, total, msg):
    print(msg)

n = detect_and_extract(grf, outdir, rename=True, progress=on_progress)
print(f"Extracted {n} PNGs")

mapper = build_aircraft_mapper(grf)
print(mapper.summary()["aircraft_count"], "aircraft")
print(list(mapper.sprite_id_to_label().items())[:3])

GUI extract worker (thread + queue)

def worker():
    def progress(done, total, msg):
        msg_queue.put(("progress", msg))
    n = detect_and_extract(grf, out, rename=rename, progress=progress)
    msg_queue.put(("done", n, str(out)))

threading.Thread(target=worker, daemon=True).start()

Project layout

GRF extract/
├── grf_extract.py          # Core: decompress, container v1/v2, name mapper, CLI
├── grf_extract_gui.py      # Tkinter GUI
├── Open GUI.bat            # Windows launcher
├── requirements.txt
├── README.md               # This file
├── docs/
│   ├── EXTRACT_Real_Aircrafts_Beta.md
│   └── GUI.md
└── .gitignore

Large binaries (.grf, extracted PNG trees, *.bak_*) are not committed.


Checkpoint: Real_Aircrafts_Beta.grf

Metric Value
Format Container v2
GRF title Real Aircrafts set 32 BPP 4X Extrazoom - Version beta
Aircraft (Action4) 58
PNGs 2407
Renamed 2407 / 2407

More detail: docs/EXTRACT_Real_Aircrafts_Beta.md, docs/GUI.md.

References

About

Extract OpenTTD/NewGRF sprites to PNG with aircraft name mapping and GUI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages