diff --git a/Makefile b/Makefile index 73b96b5..0af42b8 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ LIB = ./saturn SRC = main.o input.o SRC += gen/maps.o SRC += gen/map_objects.o +SRC += gen/sprites.o DEP = $(patsubst %.o,%.d,$(SRC)) res = $(subst pokered/,res/,$(patsubst %.$(1),%.$(1).o,$(wildcard $(2)*.$(1)))) @@ -13,9 +14,10 @@ res_png = $(subst pokered/,res/,$(patsubst %.png,%.$(1).o,$(wildcard $(2)*.png)) GFX_TILESETS = $(call res_png,2bpp,pokered/gfx/tilesets/) GFX_BLOCKSETS = $(call res,bst,pokered/gfx/blocksets/) +GFX_SPRITES = $(call res_png,2bpp,pokered/gfx/sprites/) MAPS_BLOCKS = $(call res,blk,pokered/maps/) -GENERATED = $(GFX_TILESETS) $(GFX_BLOCKSETS) $(MAPS_BLOCKS) +GENERATED = $(GFX_TILESETS) $(GFX_BLOCKSETS) $(GFX_SPRITES) $(MAPS_BLOCKS) OBJ = $(SRC) $(GENERATED) all: main.cue @@ -47,6 +49,10 @@ res/%.2bpp: pokered/%.png @mkdir -p $(dir $@) python tools/png_to_nbpp.py $< 2 $@ +res/gfx/sprites/%.2bpp: pokered/gfx/sprites/%.png + @mkdir -p $(dir $@) + python tools/png_to_nbpp_sprite.py $< 2 $@ + %.2bpp.h: $(BUILD_BINARY_H) diff --git a/main.cpp b/main.cpp index c6be5d0..58da84b 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,7 @@ #include #include "vdp2.h" +#include "vdp1.h" #include "scu.h" #include "smpc.h" #include "sh2.h" @@ -12,6 +13,7 @@ #include "input.hpp" #include "gen/maps.hpp" +#include "gen/sprites.hpp" #include "map_objects.hpp" constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b) @@ -21,19 +23,14 @@ constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b) void palette_data() { - vdp2.cram.u16[0] = rgb15( 0, 0, 0); - vdp2.cram.u16[1] = rgb15(10, 10, 10); - vdp2.cram.u16[2] = rgb15(21, 21, 21); - vdp2.cram.u16[3] = rgb15(31, 31, 31); + vdp2.cram.u16[3] = rgb15( 0, 0, 0); + vdp2.cram.u16[2] = rgb15(10, 10, 10); + vdp2.cram.u16[1] = rgb15(21, 21, 21); + vdp2.cram.u16[0] = rgb15(31, 31, 31); } -uint32_t cell_data(const start_size_t& buf, const uint32_t top) +static inline void _2bpp_4bpp_vram_copy(uint32_t * vram, const start_size_t& buf) { - // round to nearest multiple of 32 - const uint32_t table_size = ((buf.size * 2) + 0x20 - 1) & (-0x20); - const uint32_t base_address = top - table_size; // in bytes - - uint32_t * vram = &vdp2.vram.u32[(base_address / 4)]; for (uint32_t ix = 0; ix < buf.size / 4; ix += 1) { const uint32_t pixels = reinterpret_cast(buf.start)[ix]; const uint32_t px0 = pixels >> 16 & 0xffff; @@ -52,6 +49,28 @@ uint32_t cell_data(const start_size_t& buf, const uint32_t top) #undef lshift #undef rshift } +} + +uint32_t character_pattern_table(const start_size_t& buf, const uint32_t top) +{ + // round to nearest multiple of 32 + const uint32_t table_size = ((buf.size * 2) + 0x20 - 1) & (-0x20); + const uint32_t base_address = top - table_size; + + uint32_t * vram = &vdp1.vram.u32[(base_address / 4)]; + _2bpp_4bpp_vram_copy(vram, buf); + + return base_address; +} + +uint32_t cell_data(const start_size_t& buf, const uint32_t top) +{ + // round to nearest multiple of 32 + const uint32_t table_size = ((buf.size * 2) + 0x20 - 1) & (-0x20); + const uint32_t base_address = top - table_size; // in bytes + + uint32_t * vram = &vdp2.vram.u32[(base_address / 4)]; + _2bpp_4bpp_vram_copy(vram, buf); return base_address; } @@ -187,10 +206,74 @@ void smpc_int(void) intback::fsm(digital_callback, nullptr); } +void sprite() +{ + uint32_t top = (sizeof (union vdp1_vram)); + const spritesheet_t& spritesheet = spritesheets[spritesheet_t::oak]; + uint32_t character_address = top = character_pattern_table(spritesheet.spritesheet, top); + uint32_t color_address = 0; + + /* TVM settings must be performed from the second H-blank IN interrupt after the + V-blank IN interrupt to the H-blank IN interrupt immediately after the V-blank + OUT interrupt. */ + // "normal" display resolution, 16 bits per pixel, 512x256 framebuffer + vdp1.reg.TVMR = TVMR__TVM__NORMAL; + + // swap framebuffers every 1 cycle; non-interlace + vdp1.reg.FBCR = 0; + + // during a framebuffer erase cycle, write the color "black" to each pixel + constexpr uint16_t black = 0x0000; + vdp1.reg.EWDR = black; + + // the EWLR/EWRR macros use somewhat nontrivial math for the X coordinates + // erase upper-left coordinate + vdp1.reg.EWLR = EWLR__16BPP_X1(0) | EWLR__Y1(0); + + // erase lower-right coordinate + vdp1.reg.EWRR = EWRR__16BPP_X3(319) | EWRR__Y3(239); + + vdp1.vram.cmd[0].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__SYSTEM_CLIP_COORDINATES; + vdp1.vram.cmd[0].LINK = 0; + vdp1.vram.cmd[0].XC = 319; + vdp1.vram.cmd[0].YC = 239; + + vdp1.vram.cmd[1].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__LOCAL_COORDINATE; + vdp1.vram.cmd[1].LINK = 0; + vdp1.vram.cmd[1].XA = 0; + vdp1.vram.cmd[1].YA = 0; + + vdp1.vram.cmd[2].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__NORMAL_SPRITE; + vdp1.vram.cmd[2].LINK = 0; + // The "end code" is 0xf, which is being used in the mai sprite palette. If + // both transparency and end codes are enabled, it seems there are only 14 + // usable colors in the 4-bit color mode. + vdp1.vram.cmd[2].PMOD = PMOD__ECD | PMOD__COLOR_MODE__COLOR_BANK_16; + // It appears Kronos does not correctly calculate the color address in the + // VDP1 debugger. Kronos will report FFFC when the actual color table address + // in this example is 7FFE0. + vdp1.vram.cmd[2].COLR = color_address; // non-palettized (rgb15) color data + vdp1.vram.cmd[2].SRCA = character_address >> 3; + vdp1.vram.cmd[2].SIZE = SIZE__X(16) | SIZE__Y(16); + vdp1.vram.cmd[2].XA = 5 * 16; + vdp1.vram.cmd[2].YA = 5 * 16; + + vdp1.vram.cmd[3].CTRL = CTRL__END; + + // start drawing (execute the command list) on every frame + vdp1.reg.PTMR = PTMR__PTM__FRAME_CHANGE; +} + void main() { + state.map_ix = map_t::celadon_city; + v_blank_in(); + sprite(); + + vdp2.reg.PRISA = PRISA__S0PRIN(7); // Sprite register 0 PRIority Number + // DISP: Please make sure to change this bit from 0 to 1 during V blank. vdp2.reg.TVMD = ( TVMD__DISP | TVMD__LSMD__NON_INTERLACE | TVMD__VRESO__240 | TVMD__HRESO__NORMAL_320); @@ -199,7 +282,7 @@ void main() vdp2.reg.RAMCTL = RAMCTL__CRKTE | RAMCTL__CRMD__RGB_5BIT_1024 | RAMCTL__VRAMD | RAMCTL__VRBMD; /* enable display of NBG0 */ - vdp2.reg.BGON = BGON__N0ON; + vdp2.reg.BGON = BGON__N0ON | BGON__N0TPON; /* set character format for NBG0 to palettized 16 color set enable "cell format" for NBG0 @@ -216,10 +299,10 @@ void main() 2-word: value of bit 5-0 * 0x4000 */ constexpr int plane_a = 0; - constexpr int plane_a_offset = plane_a * 0x2000; + //constexpr int plane_a_offset = plane_a * 0x2000; - constexpr int page_size = 64 * 64 * 2; // N0PNB__1WORD (16-bit) - constexpr int plane_size = page_size * 1; + //constexpr int page_size = 64 * 64 * 2; // N0PNB__1WORD (16-bit) + //constexpr int plane_size = page_size * 1; vdp2.reg.MPOFN = MPOFN__N0MP(0); // bits 8~6 vdp2.reg.MPABN0 = MPABN0__N0MPB(0) | MPABN0__N0MPA(plane_a); // bits 5~0 @@ -247,6 +330,4 @@ void main() scu.reg.IST = 0; scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN); - - state.map_ix = map_t::celadon_city; } diff --git a/start_size.hpp b/start_size.hpp new file mode 100644 index 0000000..852c736 --- /dev/null +++ b/start_size.hpp @@ -0,0 +1,6 @@ +#pragma once + +struct start_size_t { + uint8_t const * const start; + uint32_t size; +}; diff --git a/tileset.hpp b/tileset.hpp deleted file mode 100644 index 6c0e177..0000000 --- a/tileset.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "res/gfx/tilesets/cavern.4bpp.h" -#include "res/gfx/tilesets/cemetery.4bpp.h" -#include "res/gfx/tilesets/club.4bpp.h" -#include "res/gfx/tilesets/facility.4bpp.h" -#include "res/gfx/tilesets/forest.4bpp.h" -#include "res/gfx/tilesets/gate.4bpp.h" -#include "res/gfx/tilesets/gym.4bpp.h" -#include "res/gfx/tilesets/house.4bpp.h" -#include "res/gfx/tilesets/interior.4bpp.h" -#include "res/gfx/tilesets/lab.4bpp.h" -#include "res/gfx/tilesets/lobby.4bpp.h" -#include "res/gfx/tilesets/mansion.4bpp.h" -#include "res/gfx/tilesets/overworld.4bpp.h" -#include "res/gfx/tilesets/plateau.4bpp.h" -#include "res/gfx/tilesets/pokecenter.4bpp.h" -#include "res/gfx/tilesets/reds_house.4bpp.h" -#include "res/gfx/tilesets/ship.4bpp.h" -#include "res/gfx/tilesets/ship_port.4bpp.h" -#include "res/gfx/tilesets/underground.4bpp.h" - -struct tileset { - enum { - cavern, - cemetery, - club, - facility, - forest, - gate, - gym, - house, - interior, - lab, - lobby, - mansion, - overworld, - plateau, - pokecenter, - reds_house, - ship, - ship_port, - underground, - }; - - static uint32_t pattern_name(uint32_t graphics_ix); - static uint32_t load(uint32_t top, uint32_t i); -}; - -struct buf_t { - uint32_t * buf; - uint32_t size; - uint32_t base_address; -}; - -extern uint32_t cell_data(const buf_t& buf, const uint32_t top); - -uint32_t tileset::pattern_name(uint32_t graphics_ix) -{ - return tilesets[graphics_ix].base_address / 32; -} - -uint32_t tileset::load(uint32_t top, uint32_t i) -{ - tilesets[i].base_address = top = cell_data(tilesets[i], top); - return top; -} diff --git a/tools/generate/__main__.py b/tools/generate/__main__.py index 8404744..fdba7ef 100644 --- a/tools/generate/__main__.py +++ b/tools/generate/__main__.py @@ -4,14 +4,17 @@ import sys from generate import maps from generate import map_objects +from generate import sprites + +files = [ + (maps.generate_maps_header, "maps.hpp"), + (maps.generate_maps_source, "maps.cpp"), + (map_objects.generate_map_objects_source, "map_objects.cpp"), + (sprites.generate_sprites_header, "sprites.hpp"), + (sprites.generate_sprites_source, "sprites.cpp"), +] def generate(base_path): - files = [ - (maps.generate_maps_header, "maps.hpp"), - (maps.generate_maps_source, "maps.cpp"), - (map_objects.generate_map_objects_source, "map_objects.cpp") - ] - for func, filename in files: path = base_path / filename with open(path, 'w') as f: diff --git a/tools/generate/binary.py b/tools/generate/binary.py new file mode 100644 index 0000000..6ee10ad --- /dev/null +++ b/tools/generate/binary.py @@ -0,0 +1,18 @@ +def binary_res(path, suffix): + # _binary_res_gfx_blocksets_overworld_bst_start + name = path.replace('/', '_').replace('.', '_') + return f"&_binary_res_{name}_{suffix}" + +def start_size_value(path): + if path is None: + return [ + "0,", + "0,", + ] + else: + start = binary_res(path, "start") + size = binary_res(path, "size") + return [ + f"reinterpret_cast({start}),", + f"reinterpret_cast({size}),", + ] diff --git a/tools/generate/maps.py b/tools/generate/maps.py index 627b13b..6f190d5 100644 --- a/tools/generate/maps.py +++ b/tools/generate/maps.py @@ -1,19 +1,51 @@ +""" +map_headers[0].tileset == 'OVERWORLD' +map_headers[0].name1 = 'PalletTown' +map_headers[0].name2 = 'PALLET_TOWN' +map_headers[0].blocks() == 'PalletTown_Blocks' +map_constants_list['PALLET_TOWN'] == MapConstant(10, 19) +maps_blocks_list['PalletTown_Blocks'] == 'maps/PalletTown.blk' +tileset_constants_list['OVERWORLD'] == 0 +tileset_headers_list[0].name == 'Overworld' +tileset_headers_list[0].blockset() == 'Overworld_Block' +tileset_headers_list[0].gfx() == 'Overworld_GFX' +gfx_tilesets_list['Overworld_Block'] == 'gfx/blocksets/overworld.bst' +gfx_tilesets_list['Overworld_GFX'] == 'gfx/tilesets/overworld.2bpp' +""" + from parse import parse +from generate.sort import default_sort +from generate.binary import binary_res, start_size_value from generate.generate import renderer +def sorted_map_constants_list(): + return sorted(parse.map_constants_list.items(), key=default_sort) + def sorted_map_headers(): + map_constants_list = sorted_map_constants_list() + map_headers_dict = dict((map_header.name2, map_header) for map_header in parse.map_headers) # hack to remove unused/duplicate underground_path_route_7 - map_headers = sorted(parse.map_headers, key=lambda m: m.name2) - return filter(lambda m: m.name1 != "UndergroundPathRoute7Copy", map_headers) + #map_headers = sorted(parse.map_headers, key=lambda m: m.name2) + #return filter(lambda m: m.name1 != "UndergroundPathRoute7Copy", map_headers) + return ( + map_headers_dict[map_name2] for map_name2, _ in map_constants_list + if map_name2 in map_headers_dict + # e.g CERULEAN_TRASHED_HOUSE_COPY has no map header + ) + +def sorted_tilesets_constants_list(): + return sorted(parse.tileset_constants_list.items(), key=default_sort) def includes_header(): yield "#pragma once" yield "" + yield '#include "../start_size.hpp"' + yield "" for map_header in sorted_map_headers(): block_path = parse.maps_blocks_list[map_header.blocks()] yield f'#include "../res/{block_path}.h"' - for tileset_name in sorted(parse.tileset_constants_list): + for tileset_name, _ in sorted_tilesets_constants_list(): tileset_index = parse.tileset_constants_list[tileset_name] tileset_header = parse.tileset_headers_list[tileset_index] @@ -24,18 +56,10 @@ def includes_header(): yield f'#include "../res/{gfx_path}.h"' yield "" -def struct_start_size_t(): - return [ - "struct start_size_t {", - "uint8_t const * const start;", - "uint32_t size;", - "};", - ] - def struct_tileset_t(): tileset_names = ( f"{name.lower()}," - for name in sorted(parse.tileset_constants_list) + for name, _ in sorted_tilesets_constants_list() ) return [ "struct tileset_t {", @@ -68,20 +92,6 @@ def struct_map_t(): "};", ] -def binary_res(path, suffix): - # _binary_res_gfx_blocksets_overworld_bst_start - name = path.replace('/', '_').replace('.', '_') - return f"_binary_res_{name}_{suffix}" - -def start_size_value(path): - start = binary_res(path, "start") - size = binary_res(path, "size") - - return [ - f"reinterpret_cast(&{start}),", - f"reinterpret_cast(&{size}),", - ] - def blockset_tileset(name: str): tileset_index = parse.tileset_constants_list[name] tileset_header = parse.tileset_headers_list[tileset_index] @@ -105,8 +115,8 @@ def tilesets_header(): def tilesets(): yield "const tileset_t tilesets[] = {" - for tileset in sorted(parse.tileset_constants_list): - yield from blockset_tileset(tileset) + for name, _ in sorted_tilesets_constants_list(): + yield from blockset_tileset(name) yield "};" def map(map_header): @@ -135,7 +145,6 @@ def maps(): def generate_maps_header(): render, out = renderer() render(includes_header()) - render(struct_start_size_t()) render(struct_tileset_t()) render(struct_map_t()) render(tilesets_header()); @@ -154,19 +163,3 @@ def generate_maps_source(): render(tilesets()) render(maps()) return out - - -""" -map_headers[0].tileset == 'OVERWORLD' -map_headers[0].name1 = 'PalletTown' -map_headers[0].name2 = 'PALLET_TOWN' -map_headers[0].blocks() == 'PalletTown_Blocks' -map_constants_list['PALLET_TOWN'] == MapConstant(10, 19) -maps_blocks_list['PalletTown_Blocks'] == 'maps/PalletTown.blk' -tileset_constants_list['OVERWORLD'] == 0 -tileset_headers_list[0].name == 'Overworld' -tileset_headers_list[0].blockset() == 'Overworld_Block' -tileset_headers_list[0].gfx() == 'Overworld_GFX' -gfx_tilesets_list['Overworld_Block'] == 'gfx/blocksets/overworld.bst' -gfx_tilesets_list['Overworld_GFX'] == 'gfx/tilesets/overworld.2bpp' -""" diff --git a/tools/generate/sort.py b/tools/generate/sort.py new file mode 100644 index 0000000..a494e22 --- /dev/null +++ b/tools/generate/sort.py @@ -0,0 +1,5 @@ +from operator import itemgetter + +by_name = itemgetter(0) +by_index = itemgetter(1) +default_sort = by_name diff --git a/tools/generate/sprites.py b/tools/generate/sprites.py new file mode 100644 index 0000000..c8cae72 --- /dev/null +++ b/tools/generate/sprites.py @@ -0,0 +1,98 @@ +""" +spritesheets_list[0] == Spritesheet('RedSprite', 3) +sprite_constants_list[0] == 'SPRITE_NONE' +gfx_sprites_list['RedSprite'] == 'gfx/sprites/red.2bpp' +""" +# insert a empty sprite for SPRITE_NONE +# remove SPRITE_ prefix + + +from parse import parse + +from generate.sort import default_sort +from generate.binary import binary_res, start_size_value +from generate.generate import renderer + +def sorted_sprite_constants_list(): + return sorted(parse.sprite_constants_list.items(), key=default_sort) + +def includes_header(): + yield '#pragma once' + yield '' + yield '#include "../start_size.hpp"' + yield '' + for name, index in sorted_sprite_constants_list(): + if name == 'SPRITE_NONE': + continue + assert index != 0, index + spritesheet = parse.spritesheets_list[index - 1] + sprite_path = parse.gfx_sprites_list[spritesheet.name] + yield f'#include "../res/{sprite_path}.h"' + +def includes_source(): + yield '#include ' + yield '' + yield '#include "sprites.hpp"' + yield '' + +def sprite_name(name): + assert name.startswith('SPRITE_'), name + return name.removeprefix('SPRITE_').lower() + +def struct_spritesheet_t(): + sprite_names = ( + f"{sprite_name(name)}," + for name, _ in sorted_sprite_constants_list() + ) + return [ + "struct spritesheet_t {", + "start_size_t spritesheet;", + "uint8_t sprite_count;", + "", + "enum sprite {", + *sprite_names, + "};", + "};", + ] + +def sprites_header(): + yield "extern const spritesheet_t spritesheets[];" + +def generate_sprites_header(): + render, out = renderer() + render(includes_header()) + render(struct_spritesheet_t()) + render(sprites_header()) + return out + +def sprite(name, index): + if name == 'SPRITE_NONE': + # special null sprite + sprite_path = None + sprite_count = 0 + else: + # spritesheets_list does not include SPRITE_NULL at index 0 + assert index != 0, index + spritesheet = parse.spritesheets_list[index - 1] + sprite_path = parse.gfx_sprites_list[spritesheet.name] + sprite_count = spritesheet.sprite_count + return [ + f"[spritesheet_t::{sprite_name(name)}] = {{", + ".spritesheet = {", + *start_size_value(sprite_path), + "},", + f".sprite_count = {sprite_count}", + "},", + ] + +def sprites(): + yield "const spritesheet_t spritesheets[] = {" + for name, index in sorted_sprite_constants_list(): + yield from sprite(name, index) + yield "};" + +def generate_sprites_source(): + render, out = renderer() + render(includes_source()); + render(sprites()) + return out diff --git a/tools/palette.py b/tools/palette.py new file mode 100644 index 0000000..199582b --- /dev/null +++ b/tools/palette.py @@ -0,0 +1,4 @@ +def intensity_to_index(px): + indices = {0x00: 3, 0x55: 2, 0xaa: 1, 0xff: 0} + assert px in indices, px + return indices[px] diff --git a/tools/parse/__main__.py b/tools/parse/__main__.py index 881f9ca..77213b9 100644 --- a/tools/parse/__main__.py +++ b/tools/parse/__main__.py @@ -1,5 +1,5 @@ from pprint import pprint from parse import parse -for i in parse.map_objects_list.items(): +for i in parse.sprite_constants_list: pprint(i) diff --git a/tools/parse/tilesets.py b/tools/parse/gfx_sprites.py similarity index 50% rename from tools/parse/tilesets.py rename to tools/parse/gfx_sprites.py index e70a059..a2df4ae 100644 --- a/tools/parse/tilesets.py +++ b/tools/parse/gfx_sprites.py @@ -2,15 +2,16 @@ from parse.maps_blocks import tokenize_block, flatten def tokenize_lines(lines): for line in lines: - if '_GFX:' in line or '_Block:' in line: + if '::' in line: yield tokenize_block(line, delim='::') def parse(prefix): - path = prefix / 'gfx/tilesets.asm' + path = prefix / 'gfx/sprites.asm' with open(path) as f: tokens = tokenize_lines(f.read().split('\n')) - return list( - flatten(tokens, - endings=['_GFX', '_Block'], - base_path='gfx/') - ) + l = list(flatten(tokens, + endings=['Sprite'], + base_path='gfx/')) + d = dict(l) + assert len(l) == len(d) + return d diff --git a/tools/parse/parse.py b/tools/parse/parse.py index 233e67f..a5126f5 100644 --- a/tools/parse/parse.py +++ b/tools/parse/parse.py @@ -12,6 +12,10 @@ from parse import map_objects from parse import hidden_objects from parse import map_constants +from parse import gfx_sprites +from parse import spritesheets +from parse import sprite_constants + prefix = Path(sys.argv[1]) map_headers = map_header.parse_all(prefix) @@ -30,5 +34,9 @@ map_constants_list = map_constants.parse(prefix) #ledge_tiles.asm #cut_tree_blocks.asm - # home/vcopy: animations + +# sprites +gfx_sprites_list = gfx_sprites.parse(prefix) +spritesheets_list = spritesheets.parse(prefix) +sprite_constants_list = sprite_constants.parse(prefix) diff --git a/tools/parse/sprite_constants.py b/tools/parse/sprite_constants.py new file mode 100644 index 0000000..ebd20cf --- /dev/null +++ b/tools/parse/sprite_constants.py @@ -0,0 +1,6 @@ +from parse.tileset_constants import flatten, tokenize_lines + +def parse(prefix): + path = prefix / 'constants/sprite_constants.asm' + with open(path) as f: + return dict(flatten(tokenize_lines(f.read().split('\n')))) diff --git a/tools/parse/spritesheets.py b/tools/parse/spritesheets.py new file mode 100644 index 0000000..2c39b0f --- /dev/null +++ b/tools/parse/spritesheets.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass + +from parse.map_header import tokenize_line +from parse import number + +def tokenize_lines(lines): + for line in lines: + if 'overworld_sprite ' in line: + yield tokenize_line(line) + +@dataclass +class Spritesheet: + name: str + sprite_count: int + +def translate_tile_count(tile_count): + # this is the number of non-animation sprites in the spritesheet + # the animation sprite is always at tile_index * 2 + # all animated sprites have 2 animation frames + return int(tile_count) // 4 + +def flatten(tokens): + for ts in tokens: + if ts[0] != 'overworld_sprite': + continue + _, (name, tile_count) = ts + + sprite_count = translate_tile_count(tile_count) + + yield Spritesheet( + name=name, + sprite_count=sprite_count, + ) + +def parse(prefix): + path = prefix / 'data/sprites/sprites.asm' + with open(path) as f: + tokens = tokenize_lines(f.read().split('\n')) + return list(flatten(tokens)) diff --git a/tools/png_to_nbpp.py b/tools/png_to_nbpp.py index a30c81c..00c804f 100644 --- a/tools/png_to_nbpp.py +++ b/tools/png_to_nbpp.py @@ -3,10 +3,7 @@ import sys from PIL import Image -def intensity_to_index(px): - indices = {0x00: 0, 0x55: 1, 0xaa: 2, 0xff: 3} - assert px in indices, px - return indices[px] +from palette import intensity_to_index def convert(image, bpp): assert bpp in {8, 4, 2}, bpp diff --git a/tools/png_to_nbpp_sprite.py b/tools/png_to_nbpp_sprite.py new file mode 100644 index 0000000..bbe25be --- /dev/null +++ b/tools/png_to_nbpp_sprite.py @@ -0,0 +1,68 @@ +import os +import sys + +from PIL import Image + +from palette import intensity_to_index + +cell_width = 16 +cell_height = 16 + +def convert(image, bpp): + assert bpp in {8, 4, 2}, bpp + bits_per_byte = 8 + px_per_byte = bits_per_byte // bpp + px_per_row = cell_width + bytes_per_row = (px_per_row // px_per_byte) + + assert image.mode == 'L', image.mode + width, height = image.size + + buf = bytearray(width * height // px_per_byte) + + for cell_y in range(height//cell_height): + cell_y_start = cell_y * bytes_per_row * cell_height + + for cell_x in range(width//cell_width): + cell_x_start = cell_y_start + bytes_per_row * cell_x + + for y in range(cell_height): + for x in range(cell_width): + px = im.getpixel((cell_x * cell_width + x, cell_y * cell_height + y)) + index = intensity_to_index(px) + buf_ix = cell_x_start + x//px_per_byte + y * bytes_per_row + + buf[buf_ix] |= (index << bpp * ((px_per_byte - 1) - (x % px_per_byte))) + return buf + +# (pixels/row) +# ------------ * +# (pixels/byte) + +def debug(buf, bpp): + assert bpp in {8, 4, 2}, bpp + bits_per_byte = 8 + px_per_byte = bits_per_byte // bpp + px_per_row = cell_width + bytes_per_row = (px_per_row // px_per_byte) + bit_mask = (2 ** bpp) - 1 + + for row in range(len(buf) // bytes_per_row): + for x in range(bytes_per_row): + px = buf[row * bytes_per_row + x] + for shift in reversed(range(px_per_byte)): + print((px >> (shift * bpp)) & bit_mask, end='') + print() + if (row % cell_height == (cell_height - 1)): + print() + +in_path = sys.argv[1] +bpp = int(sys.argv[2]) +out_path = sys.argv[3] + +im = Image.open(in_path) +buf = convert(im, bpp) +if 'NBPP_DEBUG' in os.environ: + debug(buf, bpp) +with open(out_path, 'wb') as f: + f.write(buf)