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 0000000..b7c466c Binary files /dev/null and b/level/intro.png differ diff --git a/level/intro_coll.png b/level/intro_coll.png new file mode 100644 index 0000000..2701022 Binary files /dev/null and b/level/intro_coll.png differ 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