You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
4.6 KiB

  1. import argparse
  2. import pathlib
  3. import subprocess
  4. import sys
  5. import tempfile
  6. from typing import NoReturn, Tuple
  7. from PIL import Image
  8. GREEN = (0, 255, 0)
  9. RED = (255, 0, 0)
  10. BLUE = (0, 0, 255)
  11. YELLOW = (255, 255, 0)
  12. def abort(msg: str) -> NoReturn:
  13. print(msg, file=sys.stderr)
  14. sys.exit(1)
  15. Point = Tuple[int, int]
  16. def generate_coll_map(
  17. in_path: pathlib.Path, width: int, height: int, compress: bool = False
  18. ) -> Tuple[str, Point, Point]:
  19. png = Image.open(in_path).convert("RGB")
  20. if png.width % 8 != 0 or png.height % 8 != 0:
  21. abort(f"file '{in_path}' has invalid dimensions (should be multiple of 8)")
  22. if png.width // 8 != width or png.height // 8 != height:
  23. abort(f"file '{in_path}' has different size from map")
  24. out_bytes = []
  25. bits = []
  26. spawn = None
  27. camera = (0, 0)
  28. for y in range(png.height // 8):
  29. for x in range(png.width // 8):
  30. pixel = png.getpixel((x * 8, y * 8))
  31. bit = None
  32. if pixel == RED:
  33. bit = 0
  34. elif pixel == GREEN:
  35. bit = 1
  36. elif pixel == BLUE:
  37. bit = 1
  38. spawn = (x, y)
  39. elif pixel == YELLOW:
  40. bit = 1
  41. camera = (x, y)
  42. else:
  43. abort(f"unsupported pixel in collision map: {pixel}")
  44. if compress:
  45. bits.append(bit)
  46. if len(bits) == 8:
  47. byte = sum([bit << i for i, bit in enumerate(bits)])
  48. out_bytes.append(byte)
  49. bits = []
  50. else:
  51. out_bytes.append(bit)
  52. png.close()
  53. if spawn is None:
  54. abort(f"no spawn point located")
  55. return format_bytes(out_bytes, width=width), spawn, camera
  56. def format_bytes(data: bytes, width: int = 16) -> str:
  57. lines = []
  58. for line_no in range(0, len(data), width):
  59. line = data[line_no : line_no + width]
  60. lines.append(" DB " + ", ".join(["$%02X" % b for b in line]))
  61. return "\n".join(lines)
  62. def generate_map(pngfile: str, compress: bool = False) -> None:
  63. pngpath = pathlib.Path(pngfile).resolve()
  64. incpath = pngpath.parent / pngpath.name.replace(".png", ".inc")
  65. spath = pngpath.parent / pngpath.name.replace(".png", ".s")
  66. png = Image.open(pngpath)
  67. if png.width % 8 != 0 or png.height % 8 != 0:
  68. abort(f"file '{pngfile}' has invalid dimensions (should be multiple of 8)")
  69. width = png.width // 8
  70. height = png.height // 8
  71. if width > 127 or height > 127:
  72. abort(
  73. f"file '{pngfile}' has invalid dimensions (width/height greater than 127 tiles)"
  74. )
  75. png.close()
  76. with tempfile.NamedTemporaryFile() as tilef, tempfile.NamedTemporaryFile() as mapf:
  77. subprocess.run(
  78. [
  79. "rgbgfx",
  80. "-u",
  81. "-t",
  82. tilef.name,
  83. "-o",
  84. mapf.name,
  85. pngfile,
  86. ]
  87. )
  88. map_data = format_bytes(tilef.read(), width=width)
  89. tile_data = format_bytes(mapf.read())
  90. section = pngpath.name.replace(".png", "")
  91. collpath = pngpath.parent / pngpath.name.replace(".png", "_coll.png")
  92. coll_map, spawn, camera = generate_coll_map(
  93. collpath, width, height, compress=compress
  94. )
  95. with open(incpath, "w") as outf:
  96. outf.write(
  97. f"""DEF {section}_WIDTH EQU {width}
  98. DEF {section}_HEIGHT EQU {height}
  99. DEF {section}_NUM_TILES EQUS "({section}_TILES_end - {section}_TILES)"
  100. DEF {section}_MAP_SIZE EQUS "({section}_MAP_end - {section}_MAP)"
  101. ASSERT {section}_MAP_SIZE == {section}_WIDTH * {section}_HEIGHT
  102. """
  103. )
  104. with open(spath, "w") as outf:
  105. outf.write(
  106. f"""SECTION "MAP - {section}", ROM0
  107. {section}_Data::
  108. {section}_TILE_PTR: DW {section}_TILES
  109. {section}_TILE_SIZE: DB ({section}_TILES_end - {section}_TILES)
  110. {section}_MAP_PTR: DW {section}_MAP
  111. {section}_MAP_COLLISION: DW {section}_COLLISION
  112. {section}_MAP_WIDTH: DB {width}
  113. {section}_MAP_HEIGHT: DB {height}
  114. {section}_SPAWN_X: DB {spawn[0]}
  115. {section}_SPAWN_Y: DB {spawn[1]}
  116. {section}_CAMERA_X: DB {camera[0]}
  117. {section}_CAMERA_Y: DB {camera[1]}
  118. {section}_MAP::
  119. {map_data}
  120. {section}_MAP_end::
  121. {section}_TILES::
  122. {tile_data}
  123. {section}_TILES_end::
  124. {section}_COLLISION::
  125. {coll_map}
  126. {section}_COLLISION_end::
  127. """
  128. )
  129. def main() -> None:
  130. parser = argparse.ArgumentParser("generate_map")
  131. parser.add_argument("-c", "--compress", default=False)
  132. parser.add_argument("png")
  133. args = parser.parse_args()
  134. generate_map(args.png, compress=(not not args.compress))
  135. if __name__ == "__main__":
  136. main()