Python pipeline for exporting and processing maps from Foxhole. Updated for U64.
0_make_release.py -> builds Exporter.exe from C# source
1_export.py -> Exporter.exe reads .pak → export/_json/, _meshes/, _heightmap/, _layers/
2_blend_all.py -> full-map Blender scenes → export/blend/<MapName>.blend
3_blend_spills.py -> per-region .blend with neighbor spill → export/blend_spill/<Region>.blend
4_render_spills.py -> top-down per-region bakes → export/{ao,heightmap_landscape,heightmap_water,
roads,beaches,id/<cat>,split_layers/<layer>,svg_layers/<layer>}/<Region>.png
5_finalize_exports.py -> stitches bakes into world PNGs and assembles final composites
→ export/_final/{technical,assembly,id,split_layers,svg_layers}/
6_breaker.py -> interactive: break any world PNG back into per-region tiles
→ <input_stem>/<Region>.png
- Foxhole
- Python 3.10–3.13 (no
bpyon 3.14) - numpy, opencv-python (steps 4 & 5), bpy (steps 2–4), cairosvg (step 4,
-svg) - Blender 5
- .NET 10 SDK (step 0 only)
- Cairo (the easiest way to install it - GTK for Windows Runtime Environment)
pip install numpy opencv-python bpy cairosvgClone with submodules (CUE4Parse is required):
git clone --recurse-submodules https://github.com/Tsekho/fh_map_exporter.gitCompiles Exporter/ and outputs Exporter.exe at the repo root. A pre-built binary is included.
python 0_make_release.pyExporter.exe is a self-contained win-x64 binary.
Exporter.exe -i <pak_path> -o <export_path> [-t] [-a <asset_path>]
| Argument | Description |
|---|---|
-i <pak_path> |
Path to the .pak file or its containing directory |
-o <export_path> |
Output folder (meshes/ subdirectory is created) |
-t, --texture |
Terrain layers and heightmaps |
-a <asset_path> |
Single asset, e.g. War/Content/Maps/HomeRegionC.umap (.umap optional). Omit to export all maps under War/Content/Maps. |
Exporter.exe -i "C:\...\War-WindowsNoEditor.pak" -o export -a War/Content/Maps/HomeRegionC
Exporter.exe -i "C:\...\Paks" -o export -tClears export/ and runs Exporter.exe. Writes:
| Directory | Contents |
|---|---|
export/_json/ |
Per-map JSON (symbols, groups, blueprints + transforms) |
export/_meshes/ |
Static/skeletal meshes as .pskx / .psk |
export/_heightmap/ |
16-bit grayscale heightmaps (2200×2200 px, 1 m/px) |
export/_layers/ |
Per-region terrain weightmap layers (8-bit grayscale) |
python 1_export.pypython 2_blend_all.py # interactive
python 2_blend_all.py OarbreakerHex
python 2_blend_all.py -a
python 2_blend_all.py -nt OarbreakerHex # exclude terrainOutput: export/blend/<MapName>.blend. JSONs whose stem isn't listed
in utils/region_centers.json (e.g. MainMenu.json) are skipped so
only real regions produce a .blend.
Per-region .blend with a 200 m spill from hexagonal neighbors. Spill
categories are defined in utils/catalogue.json (each key is a
category name with a list of mesh names). Only the focus region's
terrain and water are fully included; neighbor spill contributes only
non-water categories. Every category must have a color entry in
CATEGORY_COLORS (utils/config.py); the reserved names terrain
(built from the heightmap), water (cloned as deep_water occluders
at DEEP_WATER_DEPTH m), and deep_water are handled specially.
python 3_blend_spills.py # interactive
python 3_blend_spills.py OarbreakerHex
python 3_blend_spills.py -aOutput: export/blend_spill/<Region>.blend.
Opens each spill .blend and renders top-down 2048×2048 bakes. Without
any of -svg/-ao/-hm/-id/-r/-b/-sl, all bakes are produced.
The -svg pass reads export/_json/<region>.json directly and runs
before the .blend is opened; all others need Blender.
python 4_render_spills.py # interactive, all bakes
python 4_render_spills.py OarbreakerHex
python 4_render_spills.py -a # all regions
python 4_render_spills.py -a -ao -id # subsetOutputs (per-region PNGs):
ao/,heightmap_landscape/,heightmap_water/- grayscale bakes.heightmap_landscapepasses through water;heightmap_waterstops on the water surface.roads/,beaches/- RGBA spline coverage (SSAA, colored viaSPLINE_COLORS). Beaches alpha is masked toterrain × (not water).id/<category>/- 8-bit coverage per category (ID_SSAA).split_layers/<layer>/- RGBA Cycles AO per split layer; each category is tinted via its color fromSPLIT_LAYERS.svg_layers/<layer>/- cairosvg raster ofutils/svg/<cat>/<name>.svginstanced via<use>at every JSON transform. UE cm map to SVG px asx = x_cm * 1776 / 189000 + 1024.
Tunables in utils/config.py: TERRAIN_WHITELIST (categories that
participate in ao/hm/id), SPLIT_LAYERS, SVG_LAYERS, SPLINE_CATEGORIES,
SPLINE_COLORS, SPLINE_LAYER_SSAA, ID_SSAA.
Stitches every step-4 bake into world-sized PNGs, derives heightmap products, and assembles composites consumed by the map mod generator. All assembly work is in-memory; only the listed files are written.
python 5_finalize_exports.pyOutput layout (under export/_final/):
technical/ao.png- stitched AO (slope shading baked in via step 4).technical/heightmap_simple.png- 8-bit fromheightmap_water; shade 60 = z=0 m, 1 shade = 0.5 m.technical/contour.png- black RGBA lines wherehm // 250increments across a 4-neighbor boundary, masked to terrain.assembly/roads.png,assembly/beaches.png- stitched from step 4.assembly/fly_alert.png-utils/fly_alert_pattern.pngtiled, alpha ramped betweenFLY_ALERT_MIN_MandFLY_ALERT_MAX_M, gated byrocks_cov.assembly/dive_alert.png-DIVE_ALERT_COLORwhere landscape is below water surface; alpha fades 0..DEEP_WATER_DEPTH, gated byrocks_cov × water_cov.assembly/base_layer.png- single terrain composite:terrain_recolor(weighted blend ofID_RECOLORper non-water category, nearest-filled) +shades(terrain weightmaps withLAYER_COLORS,SHADES_BLUR_*) +highs × ground(add) +lows × ground(difference) +water_recolor(multiply) +ao(multiply); alpha = world hex mask.assembly/contours.png- contour blurred 3×3, alpha ×(0.5*water + ground).assembly/rdz.png-utils/rdz_pattern.pngtiled, alpha punched bysvg_layers/rdz_grace.assembly/ranges.png- alpha-over of range svg_layers:ranges_tap × ground,ranges_intel,ranges_ai × ground,ranges_mh,ranges_cg × water.assembly/bridges_aim.png-svg_layers/bridges_aimgated by water coverage eroded by 25 px.id/<cat>.png,split_layers/<layer>.png,svg_layers/<layer>.png- verbatim stitches of the per-region bakes.
Important: This exporter's output is intended for use with the map mod generator that allows to convert composed layers into a standalone map mod via a single function call. For any other use you'll most likely want to break it apart into separate regions - see step 6.
Interactive helper that slices any world-sized PNG back into per-region
tiles using utils/region_centers.json and utils/mask.png. Lists
*.png files in the current directory (or accepts a pasted path), then
asks whether to downscale to 1k.
python 6_breaker.pyFor each region, extracts a 2048x2048 crop centred on the region's
world-pixel coordinates, multiplies mask.png into the alpha channel,
then saves either:
- default: 2048x1776
- 1k mode: 1024x888 (default map textures size)
Output: <input_stem>/<Region>.png (created in the current directory).
Steps 2, 3, and 4 fan out to multiple subprocesses when more than one
item is queued. Worker count is controlled by NUM_WORKERS and NUM_WORKERS_SPILLS in
utils/config.py. Set it to 1 for serial execution.
The fan-out logic lives in utils/parallel.py.
.
├── 0_make_release.py # Builds Exporter.exe from C# source
├── 1_export.py # Runs Exporter.exe against game .pak
├── 2_blend_all.py # Full-map Blender scenes
├── 3_blend_spills.py # Per-region spill .blend
├── 4_render_spills.py # Top-down bakes per region
├── 5_finalize_exports.py # Stitches bakes + layers into world PNGs
├── 6_breaker.py # Breaks a world PNG back into per-region tiles
├── Exporter/ # C# exporter source (.NET 10, win-x64)
│ ├── Program.cs
│ ├── MapExporter.cs
│ ├── LandscapeStitcher.cs
│ ├── TransformMath.cs
│ ├── JsonOutput.cs
│ └── Constants.cs
├── utils/
│ ├── config.py # Shared constants and tunables
│ ├── png.py # 16/8-bit PNG read/write
│ ├── psk.py # PSK/PSKX parser + mesh cache
│ ├── blender.py # Materials, transforms, terrain, splines, GN instancing
│ ├── map.py # Map class (scene construction)
│ ├── bake.py # Top-down bakers (AO, heightmap, ID, coverage)
│ ├── svg_render.py # SVG layer builder + cairosvg rasterizer
│ ├── regions.py # Region geometry, deep water, spill builder
│ ├── parallel.py # Subprocess fan-out helper
│ ├── region_centers.json # World-space pixel coords of all regions
│ ├── mask.png # Per-region hex mask
│ ├── fly_alert_pattern.png # BGRA texture sampled by the fly_alert overlay
│ ├── rdz_pattern.png # BGRA texture punched by svg_layers/rdz_grace
│ ├── svg/<category>/<name>.svg # Source icons instanced into svg_layers
│ └── catalogue.json # Per-category mesh lists for step 3 spill
└── CUE4Parse/ # Git submodule - Unreal Engine asset reader
Every placed object in the exported JSON is a 9-element array:
[x, y, z, sx, sy, sz, pitch, yaw, roll]
Coordinates are UE world-space centimetres; rotations are degrees.
utils/blender.py converts these to Blender metres/radians.
Spline mesh components are exported as 23-element arrays containing the cubic hermite spline parameters needed to reconstruct the deformed mesh:
[0..2] world start position (x, y, z in cm)
[3..5] world start tangent (x, y, z in cm)
[6..8] world end position (x, y, z in cm)
[9..11] world end tangent (x, y, z in cm)
[12] start roll (radians)
[13..14] start offset (x, y, component-space 2D)
[15..16] start scale (x, y)
[17] end roll (radians)
[18..19] end offset (x, y, component-space 2D)
[20..21] end scale (x, y)
[22] forward axis (0=X, 1=Y, 2=Z)