ddw3/tools/stitch-map.py

158 lines
3.6 KiB
Python

"""
Stitches a map file into one big image.
"""
import argparse
from dataclasses import dataclass
from pathlib import Path
import toml
import util
from PIL import Image
@dataclass
class LayerObj:
"""
Layer object
"""
world_offset: tuple[int, int]
img_offset: tuple[int, int]
size: tuple[int, int]
@dataclass
class Layer:
"""
Layer
"""
objs: list[LayerObj]
@dataclass
class Tile:
"""
Tile
"""
img: Path | None
layers: list[Layer]
def parse_entry(entry_path: str) -> Tile:
"""
Parses an map entry
"""
entry_path: Path = util.process_path(entry_path, ".")
# If it's built from an `rlen`, wrap it
if str(entry_path).startswith("build/rlen"):
rlen_config_path = entry_path.relative_to("build/").with_suffix(".toml")
rlen_config = toml.load(open(rlen_config_path, encoding="utf-8"))
entry_path = util.process_path(rlen_config["src"], rlen_config_path.parent)
# If it's `map-tile/empty.bin` return an empty tile
if str(entry_path) == "map-tile/empty.bin":
return Tile(img=None, layers=[])
# Then, if it isn't built in `map-tile`, stop
if not str(entry_path).startswith("build/map-tile"):
raise RuntimeError(f"Entry {entry_path} wasn't built in `build/map-tile`")
map_tile_config_path = entry_path.relative_to("build/").with_suffix(".toml")
map_tile_config = toml.load(open(map_tile_config_path, encoding="utf-8"))
# If the map image isn't a `tim`, stop
map_tim_path = util.process_path(
map_tile_config["img"], map_tile_config_path.parent
)
if not str(map_tim_path).startswith("build/tim"):
raise RuntimeError(f"Entry {entry_path}'s image was not built in `build/tim`")
map_tim_config_path = map_tim_path.relative_to("build/").with_suffix(".toml")
map_tim_config = toml.load(open(map_tim_config_path, encoding="utf-8"))
map_img_path = util.process_path(
map_tim_config["img"]["path"], map_tim_config_path.parent
)
return Tile(
img=map_img_path,
layers=[
Layer(
objs=[
LayerObj(
world_offset=layer_obj["world_offset"],
img_offset=layer_obj["img_offset"],
size=layer_obj["size"],
)
for layer_obj in layer["objs"]
]
)
for layer in map_tile_config["layers"]
],
)
def main(args):
"""
Main function
"""
map_config = toml.load(open(args.input_toml, encoding="utf-8"))
map_width: int = map_config["width"]
map_height: int = map_config["height"]
print(f"Map: {map_width}x{map_height}")
entries = map_config["entries"]
entries = map(parse_entry, entries)
tiles = [[next(entries) for _ in range(map_width)] for _ in range(map_height)]
output_width = 128 * map_width
output_height = 128 * map_height
output_img = Image.new("RGBA", (output_width, output_height))
for row_idx, row in enumerate(tiles):
offset_y = int(output_height * row_idx // map_height)
for tile_idx, tile in enumerate(row):
offset_x = int(output_width * tile_idx // map_width)
offset = (offset_x, offset_y)
# If the tile doesn't have an image, skip
if tile.img is None:
continue
tile_img = Image.open(tile.img)
for layer in tile.layers:
for obj in layer.objs:
obj_img = tile_img.crop(
(
obj.img_offset[0],
obj.img_offset[1],
obj.img_offset[0] + obj.size[0],
obj.img_offset[1] + obj.size[1],
)
)
output_img.alpha_composite(
obj_img,
(
offset[0] + obj.world_offset[0],
offset[1] + obj.world_offset[1],
),
)
output_img.save(args.output)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Map stitcher")
parser.add_argument("input_toml", type=str)
parser.add_argument("-o", dest="output", type=str, required=True)
args = parser.parse_args()
main(args)