10#include "absl/status/status.h"
11#include "absl/strings/match.h"
12#include "absl/strings/numbers.h"
13#include "absl/strings/str_format.h"
14#include "absl/strings/str_join.h"
15#include "absl/strings/str_split.h"
16#include "absl/strings/strip.h"
19#include "nlohmann/json.hpp"
34absl::StatusOr<int>
ParseNumber(absl::string_view text,
const char* what) {
35 text = absl::StripAsciiWhitespace(text);
38 const bool hex = absl::StartsWith(text,
"0x") ||
39 absl::StartsWith(text,
"0X") || absl::StartsWith(text,
"$");
42 : absl::SimpleAtoi(text, &value);
44 return absl::InvalidArgumentError(
45 absl::StrFormat(
"%s '%s' is not a number", what, std::string(text)));
52 if (!text.has_value()) {
53 return absl::InvalidArgumentError(
"--sheet is required");
57 return absl::InvalidArgumentError(
"--sheet must be 0-222");
61 return absl::UnimplementedError(
62 "2bpp sheets (113, 114, 218+) are not supported");
64 return static_cast<uint16_t
>(sheet);
68 const char* name,
int fallback) {
70 if (!text.has_value()) {
78 std::optional<zelda3::RoomSheetPaletteContext>* context =
nullptr) {
79 const std::string spec = parser.
GetString(
"palette").value_or(
"gray");
83 if (absl::StartsWith(spec,
"rgb:")) {
84 std::vector<std::string> parts = absl::StrSplit(spec.substr(4),
',');
85 if (parts.size() != 8) {
86 return absl::InvalidArgumentError(
"rgb: palettes need 8 colors");
89 for (
int i = 0; i < 8; ++i) {
90 absl::string_view hex = absl::StripAsciiWhitespace(parts[i]);
91 absl::ConsumePrefix(&hex,
"#");
93 if (hex.size() != 6 ||
94 !std::all_of(hex.begin(), hex.end(),
95 [](
unsigned char c) { return std::isxdigit(c) != 0; }) ||
97 return absl::InvalidArgumentError(
"rgb: colors are #RRGGBB");
99 palette[i] = {
static_cast<uint8_t
>(rgb >> 16),
100 static_cast<uint8_t
>(rgb >> 8),
static_cast<uint8_t
>(rgb)};
104 if (absl::StartsWith(spec,
"room-slot:")) {
105 const std::vector<std::string> parts = absl::StrSplit(spec,
':');
106 if (parts.size() != 4 || sheet < 0) {
107 return absl::InvalidArgumentError(
108 "room-slot:<room>:<slot>:<row> requires a single --sheet; use "
109 "gfx-room-export --out-dir for per-slot room palettes");
120 rom, data, room, slot, row));
121 if (resolved.background.sheets[slot] != sheet) {
122 return absl::InvalidArgumentError(absl::StrFormat(
123 "Room 0x%03X slot %d loads sheet 0x%02X, not --sheet 0x%02X", room,
124 slot, resolved.background.sheets[slot], sheet));
126 if (context !=
nullptr)
128 return resolved.colors;
130 const bool background = absl::StartsWith(spec,
"room-bg:");
131 if (background || absl::StartsWith(spec,
"room-sprite:")) {
132 std::vector<std::string> parts = absl::StrSplit(spec,
':');
133 if (parts.size() != 3) {
134 return absl::InvalidArgumentError(
135 "Room palettes are room-bg:<room>:<row> or room-sprite:<room>:<row>");
147 return absl::InvalidArgumentError(
"Unknown --palette " + spec);
151 return absl::StrJoin(palette,
",", [](std::string* out,
const auto& color) {
152 out->append(absl::StrFormat(
"#%02X%02X%02X", color[0], color[1], color[2]));
159 const char* main_source =
"zero_fallback";
161 case Source::kExplicitPreview:
162 main_source =
"explicit_preview";
164 case Source::kDirectDefault:
165 main_source =
"direct_default";
167 case Source::kInferredDefault:
168 main_source =
"inferred_default";
170 case Source::kHeaderFallback:
171 main_source =
"header_fallback";
173 case Source::kZeroFallback:
177 const char* entrance_source =
"none";
179 case EntranceSource::kDirect:
180 entrance_source =
"direct";
182 case EntranceSource::kDungeonMap:
183 entrance_source =
"dungeon_map";
185 case EntranceSource::kNeighbour:
186 entrance_source =
"neighbour";
188 case EntranceSource::kNone:
192 {
"slot", context.
slot},
195 {
"main_source", main_source},
201 {
"entrance_source", entrance_source},
203 {
"palette_row", context.
row},
206 {
"colors_rgb", context.
colors},
207 {
"transparent_index", 0},
208 {
"animation_overlay_included",
false}};
214 const std::vector<std::string>& outputs) {
215 std::vector<std::string> seen;
218 for (
const auto& path : outputs) {
220 return absl::InvalidArgumentError(
"Empty export path");
222 const auto canonical = std::filesystem::weakly_canonical(path, ec);
224 return absl::InvalidArgumentError(
"Invalid export path: " + path);
225 for (
const auto& other : seen) {
226 const bool equivalent = std::filesystem::equivalent(path, other, ec);
228 const auto other_canonical = std::filesystem::weakly_canonical(other, ec);
229 if (equivalent || (!ec && canonical == other_canonical)) {
230 return absl::InvalidArgumentError(
231 "Export destinations must differ from each other and the input "
235 seen.push_back(path);
237 return absl::OkStatus();
240absl::Status
WriteJson(
const std::string& path,
const nlohmann::json& value) {
241 const std::string text = value.dump(2) +
"\n";
245std::string
RangeText(
const std::vector<std::pair<int, int>>& ranges) {
246 return absl::StrJoin(ranges,
" ", [](std::string* out,
const auto& range) {
248 absl::StrFormat(
"0x%03X-0x%03X", range.first, range.second - 1));
261 auto room_text = parser.
GetString(
"room");
262 if (!room_text.has_value()) {
263 return absl::InvalidArgumentError(
"--room is required");
267 return absl::InvalidArgumentError(
"--room must be 0x000-0x127");
276 const auto header = rooms.find(room);
277 if (header == rooms.end()) {
278 return absl::DataLossError(
279 absl::StrFormat(
"Room 0x%03X header is unreadable", room));
282 header->second.blockset);
283 if (parser.
GetString(
"palette").has_value()) {
294 const std::string& directory,
296 if (directory.empty())
297 return absl::InvalidArgumentError(
"Empty --out-dir");
300 std::vector<std::pair<std::string, std::vector<uint8_t>>> files;
301 nlohmann::json manifest = {{
"schema_version", 1},
303 {
"palette_row", context.
row},
305 {
"slots", nlohmann::json::array()}};
306 for (
int slot = 0; slot < 8; ++slot) {
308 const auto palette_context,
312 const uint16_t sheet = context.
set.
sheets[slot];
316 entry[
"status"] =
"unsupported_sheet_format";
321 const std::string name =
322 absl::StrFormat(
"slot-%d-sheet-%02X.png", slot, sheet);
323 entry[
"status"] =
"exported";
325 entry[
"palette_spec"] = absl::StrFormat(
326 "room-slot:0x%03X:%d:%d", context.
set.
room, slot, context.
row);
327 files.emplace_back(name, std::move(png));
329 manifest[
"slots"].push_back(std::move(entry));
332 if (!std::filesystem::create_directory(directory, ec)) {
333 return absl::FailedPreconditionError(
334 "--out-dir must be a new directory in an existing parent: " +
335 directory + (ec ?
" (" + ec.message() +
")" :
""));
337 auto write = [&]() -> absl::Status {
338 for (
const auto& [name, png] : files) {
340 (std::filesystem::path(directory) / name).
string(), png));
343 (std::filesystem::path(directory) /
"manifest.json").
string(),
346 const absl::Status status = write();
349 std::filesystem::remove_all(directory, ec);
352 formatter.
AddField(
"out_dir", directory);
354 return absl::OkStatus();
357std::string
HexIds(
const std::vector<int>& ids) {
358 return absl::StrJoin(ids,
" ", [](std::string* out,
int id) {
359 out->append(absl::StrFormat(
"%03X",
id));
367 return absl::FailedPreconditionError(
368 "This ROM uses the Japanese graphics pointer tables (header byte "
369 "$7FD9 = 0); the gfx commands support US-table ROMs only");
371 return absl::OkStatus();
387 ASSIGN_OR_RETURN(
const int count, ParseIntArg(parser,
"count", 16 - block));
388 std::optional<zelda3::RoomSheetPaletteContext> context;
390 ResolvePalette(*rom, parser, sheet, &context));
394 const std::string path = *parser.
GetString(
"png");
395 const auto json_path = parser.
GetString(
"json");
396 std::vector<std::string> outputs = {path};
397 if (json_path.has_value())
398 outputs.push_back(*json_path);
400 nlohmann::json metadata = {{
"sheet", sheet}, {
"png", path},
401 {
"first_block", block}, {
"block_count", count},
402 {
"colors_rgb", palette}, {
"transparent_index", 0}};
403 if (context.has_value())
404 metadata[
"room_context"] = PaletteContextJson(*context);
406 if (json_path.has_value())
409 formatter.
AddField(
"sheet", absl::StrFormat(
"0x%02X", sheet));
412 absl::StrFormat(
"%d-%d", block, block + count - 1));
413 formatter.
AddField(
"width", std::min(count, 8) * 16);
414 formatter.
AddField(
"height", (count + 7) / 8 * 16);
415 formatter.
AddField(
"palette", PaletteText(palette));
416 if (context.has_value()) {
417 formatter.
AddRawJsonField(
"room_context", metadata[
"room_context"].dump());
419 return absl::OkStatus();
425 const bool dry_run = parser.
HasFlag(
"dry-run");
426 const bool write = parser.
HasFlag(
"write");
427 if (dry_run == write) {
428 return absl::InvalidArgumentError(
"Pass exactly one of --dry-run, --write");
430 if (write && !parser.
GetString(
"out").has_value()) {
431 return absl::InvalidArgumentError(
432 "--write needs --out <rom file>; the input ROM is never overwritten");
434 return absl::OkStatus();
443 std::optional<zelda3::RoomSheetPaletteContext> context;
445 ResolvePalette(*rom, parser, sheet, &context));
446 if (context.has_value()) {
448 PaletteContextJson(*context).dump());
451 const std::string png_path = *parser.
GetString(
"png");
463 Rom simulated = *rom;
467 formatter.
AddField(
"sheet", absl::StrFormat(
"0x%02X", sheet));
468 formatter.
AddField(
"png", png_path);
469 formatter.
AddField(
"changed_blocks",
470 absl::StrJoin(imported.changed_blocks,
" "));
472 static_cast<int>(imported.changed_tiles.size()));
473 formatter.
AddField(
"sheet_byte_ranges", RangeText(imported.changed_ranges));
476 formatter.
AddField(
"write_status", std::string(written.status().message()));
477 if (parser.
HasFlag(
"dry-run")) {
478 formatter.
AddField(
"mode",
"dry-run");
479 return absl::OkStatus();
481 return written.status();
483 formatter.
AddField(
"write_status",
"ok");
484 const auto rom_diff =
490 formatter.
AddField(
"rom_pc", absl::StrFormat(
"0x%06X", written->new_pc));
492 absl::StrFormat(
"%zu -> %zu", written->old_stored_size,
493 written->new_stored_size));
494 formatter.
AddField(
"rom_bytes_changed",
495 static_cast<int>(rom_diff.total_bytes_changed));
498 absl::StrJoin(rom_diff.ranges,
" ",
499 [](std::string* out,
const auto& range) {
500 out->append(absl::StrFormat(
"0x%06X-0x%06X", range.first,
504 if (parser.
HasFlag(
"dry-run")) {
505 formatter.
AddField(
"mode",
"dry-run");
506 return absl::OkStatus();
508 const std::string out = *parser.
GetString(
"out");
511 std::filesystem::equivalent(out, rom->
filename(), ec)) {
512 return absl::InvalidArgumentError(
513 "--out is the input ROM; write to a new file");
516 formatter.
AddField(
"mode",
"write");
518 return absl::OkStatus();
524 const bool bundle = parser.
GetString(
"out-dir").has_value();
525 if (bundle == parser.
GetString(
"png").has_value()) {
526 return absl::InvalidArgumentError(
"Pass exactly one of --png or --out-dir");
528 if (bundle && (parser.
GetString(
"palette").has_value() ||
530 return absl::InvalidArgumentError(
531 "--out-dir resolves each slot's palette and writes manifest.json; "
532 "do not pass --palette or --json");
534 return absl::OkStatus();
542 if (
const auto directory = parser.
GetString(
"out-dir");
543 directory.has_value()) {
544 return ExportRoomBundle(*rom, context, *directory, formatter);
546 const auto& set = context.set;
549 const std::string png_path = *parser.
GetString(
"png");
550 std::vector<std::string> outputs = {png_path};
551 if (
const auto json_path = parser.
GetString(
"json"); json_path.has_value()) {
552 outputs.push_back(*json_path);
558 const auto all_sets =
560 std::vector<int> same_main, same_room_blockset;
561 for (
const auto& other : all_sets) {
562 if (other.main_blockset == set.main_blockset) {
563 same_main.push_back(other.room);
565 if (other.header_blockset == set.header_blockset) {
566 same_room_blockset.push_back(other.room);
569 nlohmann::json slots = nlohmann::json::array();
570 for (
int slot = 0; slot < 8; ++slot) {
571 std::vector<int> rooms;
572 for (
const auto& other : all_sets) {
573 if (std::find(other.sheets.begin(), other.sheets.end(),
574 set.sheets[slot]) != other.sheets.end()) {
575 rooms.push_back(other.room);
578 slots.push_back({{
"slot", slot},
579 {
"sheet", absl::StrFormat(
"0x%02X", set.sheets[slot])},
580 {
"source", set.from_room_blockset[slot] ?
"room_blockset"
582 {
"png_y", slot * 32},
583 {
"rooms_using_sheet", rooms}});
585 const std::string rom_path = rom->
filename();
586 const std::string import_command = absl::StrFormat(
587 "z3ed gfx-room-import --rom %s --room 0x%03X --png %s --row %d --dry-run",
588 rom_path, set.room, png_path, context.row);
589 nlohmann::json sidecar = {
590 {
"room", absl::StrFormat(
"0x%03X", set.room)},
591 {
"main_blockset", set.main_blockset},
592 {
"room_blockset", set.header_blockset},
593 {
"palette_row", context.row},
594 {
"palette", PaletteText(context.palette)},
595 {
"palette_mode",
"uniform_palette_all_slots"},
598 {
"rooms_with_same_main_blockset", same_main},
599 {
"rooms_with_same_room_blockset", same_room_blockset},
600 {
"reimport", import_command},
602 if (
auto json_path = parser.
GetString(
"json"); json_path.has_value()) {
603 const std::string text = sidecar.dump(1) +
"\n";
605 *json_path, std::vector<uint8_t>(text.begin(), text.end())));
607 for (
const auto& [key, value] : sidecar.items()) {
610 return absl::OkStatus();
616 const bool dry_run = parser.
HasFlag(
"dry-run");
617 const bool write = parser.
HasFlag(
"write");
618 if (dry_run == write) {
619 return absl::InvalidArgumentError(
"Pass exactly one of --dry-run, --write");
621 if (write && !parser.
GetString(
"out").has_value()) {
622 return absl::InvalidArgumentError(
623 "--write needs --out <rom file>; the input ROM is never overwritten");
625 return absl::OkStatus();
633 const std::string png_path = *parser.
GetString(
"png");
644 Rom simulated = *rom;
645 const auto all_sets =
647 nlohmann::json sheets = nlohmann::json::array();
649 std::vector<int> affected_rooms;
650 for (
const auto&
import : imports) {
652 import.result.snes_3bpp, policy);
653 nlohmann::json entry = {
654 {
"sheet", absl::StrFormat(
"0x%02X",
import.sheet)},
655 {
"slots",
import.slots},
656 {
"changed_blocks",
import.result.changed_blocks},
657 {
"changed_tiles",
import.result.changed_tiles.size()},
664 entry[
"stored_bytes"] = absl::StrFormat(
665 "%zu -> %zu", written->old_stored_size, written->new_stored_size);
667 entry[
"write_status"] = std::string(written.status().message());
668 if (refusal.empty()) {
669 refusal = entry[
"write_status"];
672 for (
const auto& other : all_sets) {
673 if (std::find(other.sheets.begin(), other.sheets.end(),
import.sheet) !=
674 other.sheets.end()) {
675 affected_rooms.push_back(other.room);
678 sheets.push_back(std::move(entry));
680 std::sort(affected_rooms.begin(), affected_rooms.end());
681 affected_rooms.erase(
682 std::unique(affected_rooms.begin(), affected_rooms.end()),
683 affected_rooms.end());
685 formatter.
AddField(
"room", absl::StrFormat(
"0x%03X", context.set.room));
687 formatter.
AddField(
"rooms_affected", HexIds(affected_rooms));
688 formatter.
AddField(
"write_status", refusal.empty() ?
"ok" : refusal);
689 const auto rom_diff =
691 formatter.
AddField(
"rom_bytes_changed",
692 static_cast<int>(rom_diff.total_bytes_changed));
693 if (parser.
HasFlag(
"dry-run")) {
694 formatter.
AddField(
"mode",
"dry-run");
695 return absl::OkStatus();
697 if (!refusal.empty()) {
698 return absl::FailedPreconditionError(refusal);
700 const std::string out = *parser.
GetString(
"out");
703 std::filesystem::equivalent(out, rom->
filename(), ec)) {
704 return absl::InvalidArgumentError(
705 "--out is the input ROM; write to a new file");
708 formatter.
AddField(
"mode",
"write");
710 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
project::YazeProject * project_
Which graphics sheets exist, which tables use them, and which 16x16 blocks are free.
3bpp graphics sheets to and from indexed PNGs.
Locates graphics sheets in the ROM and writes edited sheets back.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
std::string HexIds(const std::vector< int > &ids)
absl::Status ExportRoomBundle(Rom &rom, RoomContext &context, const std::string &directory, resources::OutputFormatter &formatter)
std::string RangeText(const std::vector< std::pair< int, int > > &ranges)
std::string PaletteText(const zelda3::SheetPalette &palette)
absl::StatusOr< zelda3::SheetPalette > ResolvePalette(Rom &rom, const resources::ArgumentParser &parser, int sheet=-1, std::optional< zelda3::RoomSheetPaletteContext > *context=nullptr)
absl::StatusOr< uint16_t > ParseSheet(const resources::ArgumentParser &parser)
absl::Status WriteJson(const std::string &path, const nlohmann::json &value)
nlohmann::json PaletteContextJson(const zelda3::RoomSheetPaletteContext &context)
absl::StatusOr< int > ParseIntArg(const resources::ArgumentParser &parser, const char *name, int fallback)
absl::StatusOr< RoomContext > LoadRoomContext(Rom &rom, const resources::ArgumentParser &parser)
absl::StatusOr< int > ParseNumber(absl::string_view text, const char *what)
absl::Status ValidateExportPaths(const Rom &rom, const std::vector< std::string > &outputs)
absl::Status RequireUsGfxTables(const Rom &rom)
absl::StatusOr< zelda3::GfxSheetWritePolicy > BuildGfxSheetWritePolicy(const HackManifest &manifest)
DiffSummary ComputeDiffRanges(const std::vector< uint8_t > &before, const std::vector< uint8_t > &after)
absl::Status WriteBinaryFile(const std::string &path, const std::vector< uint8_t > &bytes)
absl::StatusOr< std::vector< uint8_t > > ReadBinaryFile(const std::string &path)
absl::StatusOr< PngImage > DecodePng(const std::vector< uint8_t > &bytes)
bool ParseHexString(absl::string_view str, uint64_t *out)
constexpr int kNumDungeonRooms
std::vector< RoomBackgroundSet > ResolveAllRoomBackgroundSets(const Rom &rom, const GameData &data)
All 296 rooms' background sets (header bytes read from the ROM).
absl::StatusOr< RoomSheetPaletteContext > ResolveRoomSheetPalette(Rom &rom, GameData &data, int room, int slot, int row)
absl::StatusOr< std::vector< uint8_t > > ExportRoomBackgroundPng(const Rom &rom, const RoomBackgroundSet &set, const SheetPalette &palette)
absl::StatusOr< SheetImportResult > ImportSheetBlocksPng(const std::vector< uint8_t > &snes_3bpp, const util::PngImage &png, int first_block, const SheetPalette &palette)
Places a PNG's 16x16 blocks over a sheet from first_block.
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
RoomBackgroundSet ResolveRoomBackgroundSet(const GameData &data, int room, uint8_t header_blockset)
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
absl::StatusOr< std::vector< uint8_t > > ReadGfxSheetData(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Returns the sheet's decoded bytes (SNES planar data).
absl::StatusOr< GfxSheetWriteResult > WriteGfxSheet(Rom &rom, uint16_t sheet_id, const std::vector< uint8_t > &snes_3bpp, const GfxSheetWritePolicy &policy, const GfxSheetPointerTables &tables)
Writes one edited 3bpp sheet to the ROM buffer.
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
constexpr size_t kGfxSheetCount
bool UsesJapaneseGfxSheetTables(const Rom &rom)
std::array< SheetColor, 8 > SheetPalette
Colors for 3bpp indices 0-7. Index 0 is exported as transparent.
absl::StatusOr< SheetPalette > RoomSpriteSheetPalette(Rom &rom, GameData &data, int room, int row)
Sprite palette row (OAM palette 0-7, CGRAM row 8 + row) of room room.
absl::StatusOr< std::vector< uint8_t > > ExportSheetBlocksPng(const std::vector< uint8_t > &snes_3bpp, const SheetPalette &palette, int first_block, int block_count)
SheetPalette GrayscaleSheetPalette()
absl::StatusOr< SheetPalette > RoomBackgroundSheetPalette(Rom &rom, GameData &data, int room, int row)
absl::StatusOr< std::vector< RoomSheetImport > > ImportRoomBackgroundPng(const Rom &rom, const RoomBackgroundSet &set, const util::PngImage &png, const SheetPalette &palette)
#define RETURN_IF_ERROR(expr)
zelda3::SheetPalette palette
zelda3::RoomBackgroundSet set
Rules a sheet write must follow.
std::array< uint8_t, 8 > sheets
std::array< bool, 8 > from_room_blockset
RoomEntranceSource source
RoomMainBlocksetSource source
std::array< uint8_t, 8 > cgram_indices
RoomDefaultEntrance entrance
RoomMainBlocksetResolution main
RoomBackgroundSet background