diff --git a/contrib/bash_compl/_rgbgfx.bash b/contrib/bash_compl/_rgbgfx.bash index 7729b39b8..d1b1e1651 100755 --- a/contrib/bash_compl/_rgbgfx.bash +++ b/contrib/bash_compl/_rgbgfx.bash @@ -21,6 +21,7 @@ _rgbgfx_completions() { [b]="base-tiles:unk" [c]="colors:unk" [d]="depth:unk" + [o]="input-tileset:glob-*.2bpp" [L]="slice:unk" [N]="nb-tiles:unk" [n]="nb-palettes:unk" diff --git a/contrib/zsh_compl/_rgbgfx b/contrib/zsh_compl/_rgbgfx index f009502c8..b64aa7af9 100644 --- a/contrib/zsh_compl/_rgbgfx +++ b/contrib/zsh_compl/_rgbgfx @@ -30,6 +30,7 @@ local args=( '(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:' '(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:' '(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths' + '(-i --input-tileset)'{-i,--input-tileset}'+[Use specific tiles]:tileset file:_files -g "*.2bpp"' '(-L --slice)'{-L,--slice}'+[Only process a portion of the image]:input slice:' '(-N --nb-tiles)'{-N,--nb-tiles}'+[Limit number of tiles]:tile count:' '(-n --nb-palettes)'{-n,--nb-palettes}'+[Limit number of palettes]:palette count:' diff --git a/include/gfx/main.hpp b/include/gfx/main.hpp index 9716d4619..d7ae6dcbd 100644 --- a/include/gfx/main.hpp +++ b/include/gfx/main.hpp @@ -28,7 +28,8 @@ struct Options { EMBEDDED, } palSpecType = NO_SPEC; // -c std::vector, 4>> palSpec{}; - uint8_t bitDepth = 2; // -d + uint8_t bitDepth = 2; // -d + std::string inputTileset{}; // -i struct { uint16_t left; uint16_t top; diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index fa65b916d..81a4590ad 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -16,6 +16,7 @@ .Op Fl b Ar base_ids .Op Fl c Ar pal_spec .Op Fl d Ar depth +.Op Fl i Ar input_tiles .Op Fl L Ar slice .Op Fl N Ar nb_tiles .Op Fl n Ar nb_pals @@ -164,6 +165,37 @@ for a list of formats and their descriptions. .It Fl d Ar depth , Fl \-depth Ar depth Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or 2 (the default). This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively). +.It Fl i Ar input_tiles , Fl \-input-tileset Ar input_tiles +Read tiles that will be used to convert this image, and that will always be given the first IDs. +.Ar input_tiles +must contain tile data in +.Dq raw +format, as generated through the +.Fl o +option. +.Pp +If used together with +.Fl o , +then the image can contain tiles not in +.Ar input_tiles . +Otherwise, the image must be able to be generated using +.Em only +the tiles from +.Ar input_tiles , +and thus generate different tile data. +The former is more useful if you want several images to share a given set of tiles, such as different levels sharing a single tileset; the latter, if you want to control more precisely the numeric IDs of specific tiles. +.Pp +If more than one color palette is in use, it is also +.Sy strongly +advised to dump it together with the tile data, and to pass it using +.Fl c Cm gbc: Ns Ar input_palette . +This is because +.Nm +may not pack the palettes the same way that it did when generating +.Ar input_tiles . +See +.Sx EXAMPLES +for an example of how to use this. .It Fl L Ar slice , Fl \-slice Ar slice Only process a given rectangle of the image. This is useful for example if the input image is a sheet of some sort, and you want to convert each cel individually. @@ -637,7 +669,13 @@ without needing an input image. .Pp .Dl $ rgbgfx -c '#fff,#ff0,#f80,#000' -p colors.pal .Pp -TODO: more examples. +The following will convert two levels using the same tileset, and error out of any of the level images contain tiles not in the tileset. +.Pp +.Bd -literal -offset Ds +$ rgbgfx tileset.png -o tileset.2bpp -O -P +$ rgbgfx -i tileset.2bpp -c gbc:tileset.pal level1.png -t level1.tilemap -a level1.attrmap +$ rgbgfx -i tileset.2bpp -c gbc:tileset.pal level2.png -t level2.tilemap -a level2.attrmap +.Ed .Sh BUGS Please report bugs and mistakes in this man page on .Lk https://github.com/gbdev/rgbds/issues GitHub . diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index a4464e88a..7322a8fc2 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -427,6 +427,11 @@ static char *parseArgv(int argc, char *argv[]) { options.bitDepth = 2; } break; + case 'i': + if (!options.inputTileset.empty()) + warning("Overriding input tileset file %s", options.inputTileset.c_str()); + options.inputTileset = musl_optarg; + break; case 'L': options.inputSlice.left = parseNumber(arg, "Input slice left coordinate"); if (options.inputSlice.left > INT16_MAX) { diff --git a/src/gfx/process.cpp b/src/gfx/process.cpp index aa6c28356..98b2931d9 100644 --- a/src/gfx/process.cpp +++ b/src/gfx/process.cpp @@ -692,7 +692,7 @@ static void outputPalettes(std::vector const &palettes) { if (!options.palettes.empty()) { File output; if (!output.open(options.palettes, std::ios_base::out | std::ios_base::binary)) { - fatal("Failed to open \"%s\": %s", output.c_str(options.palettes), strerror(errno)); + fatal("Failed to create \"%s\": %s", output.c_str(options.palettes), strerror(errno)); } for (Palette const &palette : palettes) { @@ -736,6 +736,20 @@ class TileData { return row; } + TileData(std::array &&raw) : _data(raw), _hash(0) { + for (uint8_t y = 0; y < 8; ++y) { + uint16_t bitplanes = _data[y * 2] | _data[y * 2 + 1] << 8; + + _hash ^= bitplanes; + if (options.allowMirroring) { + // Count the line itself as mirrorred; vertical mirroring is + // already taken care of because the symmetric line will be XOR'd + // the same way. (...which is a problem, but probably benign.) + _hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF]; + } + } + } + TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) { size_t writeIndex = 0; for (uint32_t y = 0; y < 8; ++y) { @@ -836,7 +850,7 @@ static void outputTileData( ) { File output; if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) { - fatal("Failed to open \"%s\": %s", output.c_str(options.output), strerror(errno)); + fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno)); } uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : png.getWidth() / 8; @@ -875,7 +889,7 @@ static void outputMaps( if (!path.empty()) { file.emplace(); if (!file->open(path, std::ios_base::out | std::ios_base::binary)) { - fatal("Failed to open \"%s\": %s", file->c_str(options.tilemap), strerror(errno)); + fatal("Failed to create \"%s\": %s", file->c_str(options.tilemap), strerror(errno)); } } }; @@ -923,12 +937,10 @@ struct UniqueTiles { /* * Adds a tile to the collection, and returns its ID */ - std::tuple - addTile(Png::TilesVisitor::Tile const &tile, Palette const &palette) { - TileData newTile(tile, palette); + std::tuple addTile(TileData newTile) { auto [tileData, inserted] = tileset.insert(newTile); - TileData::MatchType matchType = TileData::EXACT; + TileData::MatchType matchType = TileData::NOPE; if (inserted) { // Give the new tile the next available unique ID tileData->tileID = static_cast(tiles.size()); @@ -963,8 +975,56 @@ static UniqueTiles dedupTiles( // by caching the full tile data anyway, so we might as well.) UniqueTiles tiles; + if (!options.inputTileset.empty()) { + File inputTileset; + if (!inputTileset.open(options.inputTileset, std::ios::in | std::ios::binary)) { + fatal("Failed to open \"%s\": %s", options.inputTileset.c_str(), strerror(errno)); + } + + std::array tile; + size_t const tileSize = options.bitDepth * 8; + for (;;) { + // It's okay to cast between character types. + size_t len = inputTileset->sgetn(reinterpret_cast(tile.data()), tileSize); + if (len == 0) { // EOF! + break; + } else if (len != tileSize) { + fatal( + "\"%s\" does not contain a multiple of %zu bytes; is it actually tile data?", + options.inputTileset.c_str(), + tileSize + ); + } else if (len == 8) { + // Expand the tile data to 2bpp. + for (size_t i = 8; i--;) { + tile[i * 2 + 1] = 0; + tile[i * 2] = tile[i]; + } + } + + auto [tileID, matchType] = tiles.addTile(std::move(tile)); + switch (matchType) { + case TileData::NOPE: + break; + case TileData::HFLIP: + case TileData::VFLIP: + case TileData::VHFLIP: + if (!options.allowMirroring) { + break; + } + [[fallthrough]]; + case TileData::EXACT: + error("The input tileset contains tiles that were deduplicated; please check that your deduplication flags (`-u`, `-m`) are consistent with what was used to generate the input tileset"); + } + } + } + for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { - auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[attr.protoPaletteID]]); + auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]}); + + if (matchType == TileData::NOPE && options.output.empty()) { + error("Tile at (%" PRIu32 ", %" PRIu32 ") is not within the input tileset, and `-o` was not given!", tile.x, tile.y); + } attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP; attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP; @@ -1186,6 +1246,12 @@ continue_visiting_tiles:; ); } + // I currently cannot figure out useful semantics for this combination of flags. + if (!options.inputTileset.empty()) { + fatal("Input tilesets are not supported without `-u`\nPlease consider explaining your " + "use case to RGBDS' developers!"); + } + if (!options.output.empty()) { options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n"); unoptimized::outputTileData(png, attrmap, palettes, mappings); diff --git a/src/gfx/reverse.cpp b/src/gfx/reverse.cpp index a622ba4c1..be9a934ce 100644 --- a/src/gfx/reverse.cpp +++ b/src/gfx/reverse.cpp @@ -136,6 +136,10 @@ void reverse() { ); } + if (!options.inputTileset.empty()) { + // TODO: check that the tile data is contained within the tileset + } + // By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles size_t const nbTiles = tiles.size() / tileSize; options.verbosePrint(Options::VERB_INTERM, "Read %zu tiles.\n", nbTiles);