The swap does not carry players' explored terrain across. FTB Chunks stores no rendered map (its region zips hold chunks.dat plus five data PNGs, one of which packs height into an image channel), and Xaero's region format is closed and now version 6 with per-pixel NBT blobs, so the community converters no longer work. Rather than convert pixels, prune a copy of the world save down to exactly the chunks a player revealed and let Xaero's own generator map it. Validated against real data: extracting the 167-region / 98,170-chunk mask cross-checks clean against FTB's own pixel data, and Anvil pruning round-trips byte-identically (15.7 MB -> 471 KB on a live region file, slot-to-chunk mapping confirmed against 1024 chunk NBT headers). Deferred for now: needs the world save and one singleplayer pass per player. See analysis/FTB-XAERO-MAP-MIGRATION.md.
89 lines
3.1 KiB
Python
Executable File
89 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Drop Xaero's map regions that a player never actually revealed.
|
|
|
|
After a player opens their pruned singleplayer world, Xaero's maps it - but Minecraft
|
|
also *regenerates* the chunks around wherever that player logged in, so a little
|
|
terrain they had never seen can sneak onto the map. This compares the harvested map
|
|
against the reveal mask and removes the strays.
|
|
|
|
It works on file names only (`<rx>_<rz>.zip` is one 512x512-block region, exactly the
|
|
same geometry as an FTB Chunks region file), so it needs no knowledge of Xaero's
|
|
region format.
|
|
|
|
Usage:
|
|
filter-xaero-regions.py <xaero-map-dir> <mask.json> [--dimension <id>] [--delete]
|
|
|
|
Without --delete it only reports. Run it on one dimension folder at a time, e.g.
|
|
~/.minecraft/xaero/world-map/Multiplayer_advilplug.top/null/
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
REGION_RE = re.compile(r"^(-?\d+)_(-?\d+)\.zip$")
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
ap.add_argument("map_dir", help="Xaero's map instance directory holding <rx>_<rz>.zip")
|
|
ap.add_argument("mask", help="mask JSON from ftbchunks-reveal.py")
|
|
ap.add_argument("--dimension", default=None,
|
|
help="dimension to check (default: the mask's only dimension)")
|
|
ap.add_argument("--delete", action="store_true", help="actually remove the strays")
|
|
args = ap.parse_args()
|
|
|
|
with open(args.mask) as fh:
|
|
mask = json.load(fh)
|
|
dims = mask["dimensions"]
|
|
dim = args.dimension
|
|
if dim is None:
|
|
if len(dims) != 1:
|
|
print(f"mask has {len(dims)} dimensions; pass --dimension", file=sys.stderr)
|
|
return 2
|
|
dim = next(iter(dims))
|
|
if dim not in dims:
|
|
print(f"no dimension '{dim}' in mask; have {list(dims)}", file=sys.stderr)
|
|
return 2
|
|
|
|
revealed = {tuple(int(v) for v in key.split(",")) for key in dims[dim]["regions"]}
|
|
|
|
if not os.path.isdir(args.map_dir):
|
|
print(f"not a directory: {args.map_dir}", file=sys.stderr)
|
|
return 2
|
|
|
|
present, strays = [], []
|
|
for name in sorted(os.listdir(args.map_dir)):
|
|
m = REGION_RE.match(name)
|
|
if not m:
|
|
continue
|
|
coords = (int(m.group(1)), int(m.group(2)))
|
|
(present if coords in revealed else strays).append((name, coords))
|
|
|
|
print(f"dimension {dim}")
|
|
print(f" map regions present : {len(present)} (of {len(revealed)} revealed)")
|
|
print(f" not in the mask : {len(strays)}")
|
|
for name, _ in strays:
|
|
print(f" {name}")
|
|
|
|
missing = sorted(revealed - {c for _, c in present})
|
|
if missing:
|
|
print(f" revealed but absent from the map: {len(missing)} "
|
|
f"(e.g. {missing[:4]}) - regenerate these, do not delete anything")
|
|
|
|
if args.delete and strays:
|
|
for name, _ in strays:
|
|
os.remove(os.path.join(args.map_dir, name))
|
|
print(f"\nremoved {len(strays)} stray region file(s)")
|
|
elif strays:
|
|
print("\nre-run with --delete to remove them")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|