From 28f207cbe2f3db2ff74e8eb67385a9cbfa09c2c9 Mon Sep 17 00:00:00 2001 From: Forest Belton Date: Wed, 29 Sep 2021 06:09:38 -0400 Subject: [PATCH] Begin work on map loading --- .vscode/settings.json | 6 +- level/intro.png | Bin 0 -> 2449 bytes level/intro_coll.png | Bin 0 -> 723 bytes scripts/generate_map.py | 174 ++++++++++++++++++++++++++++++++++++++++ src/game.h | 22 ++++- src/level.c | 8 ++ src/map.c | 95 ++++++++++++++++++++++ src/map.h | 41 ++++++++++ 8 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 level/intro.png create mode 100644 level/intro_coll.png create mode 100644 scripts/generate_map.py create mode 100644 src/map.c create mode 100644 src/map.h diff --git a/.vscode/settings.json b/.vscode/settings.json index e9ea6b7..56f7515 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,8 @@ { "files.associations": { "hardware.h": "c", - "util.h": "c" + "util.h": "c", + "video.h": "c", + "vram.h": "c" } -} \ No newline at end of file +} diff --git a/level/intro.png b/level/intro.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c466c5d552a1132396292f7af84c93f87a6514 GIT binary patch literal 2449 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 N6`rnsF6*2UngHL?@#+8o literal 0 HcmV?d00001 diff --git a/scripts/generate_map.py b/scripts/generate_map.py new file mode 100644 index 0000000..935e239 --- /dev/null +++ b/scripts/generate_map.py @@ -0,0 +1,174 @@ +import argparse +import os +import pathlib +import re +import subprocess +import sys +from typing import NoReturn, Tuple + +from PIL import Image + +GREEN = (0, 255, 0) +RED = (255, 0, 0) +BLUE = (0, 0, 255) +YELLOW = (255, 255, 0) + + +def abort(msg: str) -> NoReturn: + print(msg, file=sys.stderr) + sys.exit(1) + + +Point = Tuple[int, int] + + +def generate_coll_map( + in_path: pathlib.Path, width: int, height: int, compress: bool = False +) -> Tuple[str, Point, Point]: + png = Image.open(in_path).convert("RGB") + if png.width % 8 != 0 or png.height % 8 != 0: + abort(f"file '{in_path}' has invalid dimensions (should be multiple of 8)") + + if png.width // 8 != width or png.height // 8 != height: + abort(f"file '{in_path}' has different size from map") + + out_bytes = [] + bits = [] + spawn = None + camera = (0, 0) + + for y in range(png.height // 8): + for x in range(png.width // 8): + pixel = png.getpixel((x * 8, y * 8)) + bit = None + + if pixel == RED: + bit = 0 + elif pixel == GREEN: + bit = 1 + elif pixel == BLUE: + bit = 1 + spawn = (x, y) + elif pixel == YELLOW: + bit = 1 + camera = (x, y) + else: + abort(f"unsupported pixel in collision map: {pixel}") + + if compress: + bits.append(bit) + if len(bits) == 8: + byte = sum([bit << i for i, bit in enumerate(bits)]) + out_bytes.append(byte) + bits = [] + else: + out_bytes.append(bit) + + png.close() + + if spawn is None: + abort(f"no spawn point located") + + return format_bytes(out_bytes, width=width), spawn, camera + + +def format_bytes(data: bytes, width: int = 16) -> str: + lines = [] + for line_no in range(0, len(data), width): + line = data[line_no : line_no + width] + lines.append(" " + ", ".join(["0x%02X" % b for b in line])) + return ",\n".join(lines) + + +def generate_map(pngfile: str, compress: bool = False) -> None: + pngpath = pathlib.Path(pngfile).resolve() + section = re.sub(r"[^a-z]", "_", pngpath.name.replace(".png", "").lower()) + + tilepath = pngpath.parent / f"{section}.tiles" + mappath = pngpath.parent / f"{section}.tilemap" + incpath = pngpath.parent / pngpath.name.replace(".png", ".h") + spath = pngpath.parent / pngpath.name.replace(".png", ".c") + + png = Image.open(pngpath) + if png.width % 8 != 0 or png.height % 8 != 0: + abort(f"file '{pngfile}' has invalid dimensions (should be multiple of 8)") + + width = png.width // 8 + height = png.height // 8 + + if width > 127 or height > 127: + abort( + f"file '{pngfile}' has invalid dimensions (width/height greater than 127 tiles)" + ) + + png.close() + subprocess.run( + [ + "rgbgfx", + "-u", + "-t", + tilepath, + "-o", + mappath, + pngfile, + ] + ) + + collpath = pngpath.parent / pngpath.name.replace(".png", "_coll.png") + coll_map, spawn, camera = generate_coll_map( + collpath, width, height, compress=compress + ) + + tiles_size = os.path.getsize(tilepath) + + with open(incpath, "w") as outf: + outf.write( + f""" +#ifndef IS_MAP_{section}_H_ +#define IS_MAP_{section}_H_ + +extern map_t map_{section}; + +#endif +""" + ) + + with open(spath, "w") as outf: + outf.write( + f""" +#include "map.h" + +MAP_ASSET({section}_tiles, "{section}.tiles"); +MAP_ASSET({section}_map, "{section}.tilemap"); +//MAP_ASSET({section}_collision, "{section}.collision"); + +const uint8_t {section}_collision[] = {{ +{coll_map} +}}; + +const map_t map_{section} = {{ + (uint16_t)&{section}_tiles[0], + {tiles_size} / 16, + (uint16_t)&{section}_map[0], + (uint16_t)&{section}_collision[0], + {width}, + {height}, + {spawn[0]}, + {spawn[1]}, + {camera[0]}, + {camera[1]} +}}; +""" + ) + + +def main() -> None: + parser = argparse.ArgumentParser("generate_map") + parser.add_argument("-c", "--compress", default=False) + parser.add_argument("png") + args = parser.parse_args() + generate_map(args.png, compress=(not not args.compress)) + + +if __name__ == "__main__": + main() diff --git a/src/game.h b/src/game.h index 1e58f67..137d931 100644 --- a/src/game.h +++ b/src/game.h @@ -16,11 +16,31 @@ #define SCRN_VX 256 #define SCRN_VY 256 #define SCRN_VX_B 32 -#define SCRN_VX_Y 32 +#define SCRN_VY_B 32 #define SCREEN_HEIGHT_TILES 18 #define SCREEN_WIDTH_TILES 20 +#define ALLOC_SIZE_FONT 39 +#define ALLOC_SIZE_PLAYER 52 +#define ALLOC_SIZE_BACKGROUND 32 +#define ALLOC_SIZE_ITEMS 20 +#define ALLOC_SIZE_MONSTERS 108 + +#define TILE_INDEX_FONT 0 +#define TILE_INDEX_PLAYER ALLOC_SIZE_FONT +#define TILE_INDEX_BACKGROUND (ALLOC_SIZE_FONT + ALLOC_SIZE_PLAYER) +#define TILE_INDEX_ITEMS \ + (ALLOC_SIZE_FONT + ALLOC_SIZE_PLAYER + ALLOC_SIZE_BACKGROUND) +#define TILE_INDEX_MONSTERS \ + (ALLOC_SIZE_FONT + ALLOC_SIZE_PLAYER + ALLOC_SIZE_BACKGROUND + \ + ALLOC_SIZE_ITEMS) + +#define TILE_SIZE 16 //!< Size of 1 tile in bytes +#define VRAM_TILE_PTR(idx) \ + ((void*)(_VRAM + \ + (idx)*TILE_SIZE)) //!< Compute VRAM pointer for tile section + typedef enum { GAME_STATE_TITLE, GAME_STATE_LEVEL, diff --git a/src/level.c b/src/level.c index 3c736be..ba34b2e 100644 --- a/src/level.c +++ b/src/level.c @@ -1,8 +1,16 @@ #include "game.h" +#include "map.h" #include "sdk/hardware.h" #include "sdk/joypad.h" +#include "sdk/video.h" void level(void) { + lcd_off(); + + // map_load(); + + rLCDC |= LCDC_ON; + while (1) { joypad_update(); diff --git a/src/map.c b/src/map.c new file mode 100644 index 0000000..9a9fd98 --- /dev/null +++ b/src/map.c @@ -0,0 +1,95 @@ +#include "map.h" + +#include + +#include "game.h" +#include "sdk/hardware.h" +#include "vram.h" + +#define INIT_SCX ((SCRN_VX - SCRN_X) / 2) +#define INIT_SCY ((SCRN_VY - SCRN_Y) / 2) + +map_t MAP; + +uint8_t pending_row[SCRN_VX_B]; +uint16_t pending_row_dest; + +uint8_t pending_col[SCRN_VY_B]; +uint16_t pending_col_dest; + +static void map_write_row(uint16_t map_ptr, int8_t x0, int8_t y0); + +static void map_write_col(uint16_t map_ptr, int8_t x0, int8_t y0); + +void map_load(map_t *map) { + MAP = *map; + + rSCX = INIT_SCX; + rSCY = INIT_SCY; + + // TODO: Update player state + + memcpy(VRAM_TILE_PTR(TILE_INDEX_BACKGROUND), (uint8_t *)MAP.tile_ptr, + MAP.tile_count); + + int8_t x0 = MAP.camera_x - INIT_SCX; + int8_t y0 = MAP.camera_y - INIT_SCY; + uint16_t map_ptr = _SCRN0; + + for (uint8_t i = 0; i < SCRN_VY_B; ++i) { + map_write_row(map_ptr, x0, y0); + map_ptr += SCRN_VX_B; + y0++; + } +} + +void map_update(void) { + if (pending_row_dest != 0) { + vram_enqueue_mem_xfer((uint8_t *)pending_row_dest, &pending_row[0], + sizeof pending_row); + pending_row_dest = 0; + } + + // TODO: Make it work + /* if (pending_col_dest != 0) { + vram_enqueue_mem_xfer(pending_col_dest, &pending_col[0], + sizeof pending_col); + pending_col_dest = 0; + } */ +} + +static void map_enqueue_row(uint16_t vram_ptr, int8_t x0, int8_t y0) { + pending_row_dest = vram_ptr; + + if (y0 < 0 || y0 > MAP.map_height) { + memset(&pending_row[0], 0, sizeof pending_row); + return; + } + + uint8_t *map_ptr = (uint8_t *)(MAP.map_ptr + y0 * MAP.map_width); + uint8_t left = sizeof pending_row; + uint8_t *row = &pending_row[0]; + + while (left > 0 && x0 < 0) { + *row++ = TILE_INDEX_BACKGROUND; + left--; + } + + while (left > 0 && x0 < MAP.map_height) { + *row++ = *map_ptr++; + left--; + } + + while (left > 0) { + *row++ = TILE_INDEX_BACKGROUND; + left--; + } +} + +static void map_write_row(uint16_t vram_ptr, int8_t x0, int8_t y0) { + map_enqueue_row(vram_ptr, x0, y0); + memcpy((uint8_t *)pending_row_dest, &pending_row[0], sizeof pending_row); + pending_row_dest = 0; +} + +static void map_write_col(uint16_t map_ptr, int8_t x0, int8_t y0) {} diff --git a/src/map.h b/src/map.h new file mode 100644 index 0000000..5314d96 --- /dev/null +++ b/src/map.h @@ -0,0 +1,41 @@ +#ifndef IS_MAP_H_ +#define IS_MAP_H_ + +#include + +#include "sdk/assets.h" + +typedef struct { + uint16_t tile_ptr; + uint8_t tile_count; + uint16_t map_ptr; + uint16_t collision_ptr; + int8_t map_width; + int8_t map_height; + uint8_t spawn_x; + uint8_t spawn_y; + uint8_t camera_x; + uint8_t camera_y; +} map_t; + +extern map_t MAP; //!< The current game map + +#define MAP_ASSET(var_name, filename) \ + void __##var_name##__() __naked { \ + __asm__("_" #var_name "::"); \ + __asm__(".incbin \"_build/level/" filename "\""); \ + __asm__("_" #var_name "_end::"); \ + } \ + EXTERN_ASSET(var_name) + +void map_load(map_t *map); + +void map_scroll_up(void); + +void map_scroll_down(void); + +void map_scroll_right(void); + +void map_scroll_left(void); + +#endif