yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_sheet_png_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <optional>
7#include <string>
8#include <vector>
9
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"
18#include "core/project.h"
19#include "nlohmann/json.hpp"
20#include "rom/rom.h"
21#include "rom/rom_diff.h"
22#include "util/hex.h"
23#include "util/indexed_png.h"
24#include "util/macro.h"
25#include "zelda3/game_data.h"
29
30namespace yaze {
31namespace cli {
32namespace {
33
34absl::StatusOr<int> ParseNumber(absl::string_view text, const char* what) {
35 text = absl::StripAsciiWhitespace(text);
36 // yaze::util::ParseHexString instead of absl::SimpleHexAtoi: Ubuntu 22.04's
37 // system Abseil (20210324) predates SimpleHexAtoi.
38 const bool hex = absl::StartsWith(text, "0x") ||
39 absl::StartsWith(text, "0X") || absl::StartsWith(text, "$");
40 int value = 0;
41 const bool ok = hex ? yaze::util::ParseHexString(text, &value)
42 : absl::SimpleAtoi(text, &value);
43 if (!ok) {
44 return absl::InvalidArgumentError(
45 absl::StrFormat("%s '%s' is not a number", what, std::string(text)));
46 }
47 return value;
48}
49
50absl::StatusOr<uint16_t> ParseSheet(const resources::ArgumentParser& parser) {
51 auto text = parser.GetString("sheet");
52 if (!text.has_value()) {
53 return absl::InvalidArgumentError("--sheet is required");
54 }
55 ASSIGN_OR_RETURN(int sheet, ParseNumber(*text, "--sheet"));
56 if (sheet < 0 || sheet >= static_cast<int>(zelda3::kGfxSheetCount)) {
57 return absl::InvalidArgumentError("--sheet must be 0-222");
58 }
59 if (zelda3::GetGfxSheetStorageKind(static_cast<uint16_t>(sheet)) ==
61 return absl::UnimplementedError(
62 "2bpp sheets (113, 114, 218+) are not supported");
63 }
64 return static_cast<uint16_t>(sheet);
65}
66
67absl::StatusOr<int> ParseIntArg(const resources::ArgumentParser& parser,
68 const char* name, int fallback) {
69 auto text = parser.GetString(name);
70 if (!text.has_value()) {
71 return fallback;
72 }
73 return ParseNumber(*text, name);
74}
75
76absl::StatusOr<zelda3::SheetPalette> ResolvePalette(
77 Rom& rom, const resources::ArgumentParser& parser, int sheet = -1,
78 std::optional<zelda3::RoomSheetPaletteContext>* context = nullptr) {
79 const std::string spec = parser.GetString("palette").value_or("gray");
80 if (spec == "gray") {
82 }
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");
87 }
89 for (int i = 0; i < 8; ++i) {
90 absl::string_view hex = absl::StripAsciiWhitespace(parts[i]);
91 absl::ConsumePrefix(&hex, "#");
92 uint32_t rgb = 0;
93 if (hex.size() != 6 ||
94 !std::all_of(hex.begin(), hex.end(),
95 [](unsigned char c) { return std::isxdigit(c) != 0; }) ||
96 !yaze::util::ParseHexString(hex, &rgb)) {
97 return absl::InvalidArgumentError("rgb: colors are #RRGGBB");
98 }
99 palette[i] = {static_cast<uint8_t>(rgb >> 16),
100 static_cast<uint8_t>(rgb >> 8), static_cast<uint8_t>(rgb)};
101 }
102 return palette;
103 }
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");
110 }
111 ASSIGN_OR_RETURN(const int room, ParseNumber(parts[1], "room"));
112 ASSIGN_OR_RETURN(const int slot, ParseNumber(parts[2], "slot"));
113 ASSIGN_OR_RETURN(const int row, ParseNumber(parts[3], "row"));
114 zelda3::GameData data;
115 zelda3::LoadOptions options;
116 options.load_graphics = false;
117 options.expand_rom = false;
118 RETURN_IF_ERROR(zelda3::LoadGameData(rom, data, options));
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));
125 }
126 if (context != nullptr)
127 *context = resolved;
128 return resolved.colors;
129 }
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>");
136 }
137 ASSIGN_OR_RETURN(int room, ParseNumber(parts[1], "room"));
138 ASSIGN_OR_RETURN(int row, ParseNumber(parts[2], "row"));
139 zelda3::GameData data;
140 zelda3::LoadOptions options;
141 options.load_graphics = false;
142 options.expand_rom = false;
143 RETURN_IF_ERROR(zelda3::LoadGameData(rom, data, options));
144 return background ? zelda3::RoomBackgroundSheetPalette(rom, data, room, row)
145 : zelda3::RoomSpriteSheetPalette(rom, data, room, row);
146 }
147 return absl::InvalidArgumentError("Unknown --palette " + spec);
148}
149
150std::string PaletteText(const zelda3::SheetPalette& palette) {
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]));
153 });
154}
155
156nlohmann::json PaletteContextJson(
157 const zelda3::RoomSheetPaletteContext& context) {
158 using Source = zelda3::RoomMainBlocksetSource;
159 const char* main_source = "zero_fallback";
160 switch (context.main.source) {
161 case Source::kExplicitPreview:
162 main_source = "explicit_preview";
163 break;
164 case Source::kDirectDefault:
165 main_source = "direct_default";
166 break;
167 case Source::kInferredDefault:
168 main_source = "inferred_default";
169 break;
170 case Source::kHeaderFallback:
171 main_source = "header_fallback";
172 break;
173 case Source::kZeroFallback:
174 break;
175 }
176 using EntranceSource = zelda3::RoomEntranceSource;
177 const char* entrance_source = "none";
178 switch (context.entrance.source) {
179 case EntranceSource::kDirect:
180 entrance_source = "direct";
181 break;
182 case EntranceSource::kDungeonMap:
183 entrance_source = "dungeon_map";
184 break;
185 case EntranceSource::kNeighbour:
186 entrance_source = "neighbour";
187 break;
188 case EntranceSource::kNone:
189 break;
190 }
191 return {{"room", context.background.room},
192 {"slot", context.slot},
193 {"sheet", context.background.sheets[context.slot]},
194 {"main_blockset", context.main.main_blockset},
195 {"main_source", main_source},
196 {"room_blockset", context.background.header_blockset},
197 {"sheet_source", context.background.from_room_blockset[context.slot]
198 ? "room_blockset"
199 : "main_blockset"},
200 {"entrance_id", context.entrance.entrance_id},
201 {"entrance_source", entrance_source},
202 {"entrance_via_room", context.entrance.via_room},
203 {"palette_row", context.row},
204 {"palette_half", context.palette_offset == 8 ? "right" : "left"},
205 {"raw_index_to_cgram", context.cgram_indices},
206 {"colors_rgb", context.colors},
207 {"transparent_index", 0},
208 {"animation_overlay_included", false}};
209}
210
211// Check all output aliases before the first write, including symlinks and hard
212// links. PNG/JSON destinations must not destroy each other or the source ROM.
213absl::Status ValidateExportPaths(const Rom& rom,
214 const std::vector<std::string>& outputs) {
215 std::vector<std::string> seen;
216 if (!rom.filename().empty())
217 seen.push_back(rom.filename());
218 for (const auto& path : outputs) {
219 if (path.empty())
220 return absl::InvalidArgumentError("Empty export path");
221 std::error_code ec;
222 const auto canonical = std::filesystem::weakly_canonical(path, ec);
223 if (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);
227 ec.clear();
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 "
232 "ROM");
233 }
234 }
235 seen.push_back(path);
236 }
237 return absl::OkStatus();
238}
239
240absl::Status WriteJson(const std::string& path, const nlohmann::json& value) {
241 const std::string text = value.dump(2) + "\n";
242 return util::WriteBinaryFile(path, {text.begin(), text.end()});
243}
244
245std::string RangeText(const std::vector<std::pair<int, int>>& ranges) {
246 return absl::StrJoin(ranges, " ", [](std::string* out, const auto& range) {
247 out->append(
248 absl::StrFormat("0x%03X-0x%03X", range.first, range.second - 1));
249 });
250}
251
258
259absl::StatusOr<RoomContext> LoadRoomContext(
260 Rom& rom, const resources::ArgumentParser& parser) {
261 auto room_text = parser.GetString("room");
262 if (!room_text.has_value()) {
263 return absl::InvalidArgumentError("--room is required");
264 }
265 ASSIGN_OR_RETURN(const int room, ParseNumber(*room_text, "--room"));
266 if (room < 0 || room >= zelda3::kNumDungeonRooms) {
267 return absl::InvalidArgumentError("--room must be 0x000-0x127");
268 }
269 RoomContext context;
270 ASSIGN_OR_RETURN(context.row, ParseIntArg(parser, "row", 2));
271 zelda3::LoadOptions options;
272 options.load_graphics = false;
273 options.expand_rom = false;
274 RETURN_IF_ERROR(zelda3::LoadGameData(rom, context.data, options));
275 const auto rooms = zelda3::CollectRoomGfx(rom);
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));
280 }
281 context.set = zelda3::ResolveRoomBackgroundSet(context.data, room,
282 header->second.blockset);
283 if (parser.GetString("palette").has_value()) {
284 ASSIGN_OR_RETURN(context.palette, ResolvePalette(rom, parser));
285 } else {
287 zelda3::RoomBackgroundSheetPalette(rom, context.data, room,
288 context.row));
289 }
290 return context;
291}
292
293absl::Status ExportRoomBundle(Rom& rom, RoomContext& context,
294 const std::string& directory,
295 resources::OutputFormatter& formatter) {
296 if (directory.empty())
297 return absl::InvalidArgumentError("Empty --out-dir");
298 // Resolve and encode every supported slot before creating any output. Each
299 // PNG keeps raw indices 0-7 while carrying that slot's display palette.
300 std::vector<std::pair<std::string, std::vector<uint8_t>>> files;
301 nlohmann::json manifest = {{"schema_version", 1},
302 {"room", context.set.room},
303 {"palette_row", context.row},
304 {"source_rom", rom.filename()},
305 {"slots", nlohmann::json::array()}};
306 for (int slot = 0; slot < 8; ++slot) {
308 const auto palette_context,
309 zelda3::ResolveRoomSheetPalette(rom, context.data, context.set.room,
310 slot, context.row));
311 auto entry = PaletteContextJson(palette_context);
312 const uint16_t sheet = context.set.sheets[slot];
313 if (sheet >= zelda3::kGfxSheetCount ||
316 entry["status"] = "unsupported_sheet_format";
317 } else {
318 ASSIGN_OR_RETURN(const auto raw, zelda3::ReadGfxSheetData(rom, sheet));
320 auto png, zelda3::ExportSheetBlocksPng(raw, palette_context.colors));
321 const std::string name =
322 absl::StrFormat("slot-%d-sheet-%02X.png", slot, sheet);
323 entry["status"] = "exported";
324 entry["png"] = name;
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));
328 }
329 manifest["slots"].push_back(std::move(entry));
330 }
331 std::error_code ec;
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() + ")" : ""));
336 }
337 auto write = [&]() -> absl::Status {
338 for (const auto& [name, png] : files) {
340 (std::filesystem::path(directory) / name).string(), png));
341 }
342 return WriteJson(
343 (std::filesystem::path(directory) / "manifest.json").string(),
344 manifest);
345 };
346 const absl::Status status = write();
347 if (!status.ok()) {
348 // This invocation created the directory; never clean up a preexisting one.
349 std::filesystem::remove_all(directory, ec);
350 return status;
351 }
352 formatter.AddField("out_dir", directory);
353 formatter.AddRawJsonField("manifest", manifest.dump());
354 return absl::OkStatus();
355}
356
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));
360 });
361}
362
363// These commands use the US graphics pointer tables; on a Japanese ROM they
364// would read and write the wrong sheets.
365absl::Status RequireUsGfxTables(const Rom& rom) {
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");
370 }
371 return absl::OkStatus();
372}
373
374} // namespace
375
377 const resources::ArgumentParser& parser) {
378 return parser.RequireArgs({"sheet", "png"});
379}
380
382 Rom* rom, const resources::ArgumentParser& parser,
383 resources::OutputFormatter& formatter) {
384 RETURN_IF_ERROR(RequireUsGfxTables(*rom));
385 ASSIGN_OR_RETURN(const uint16_t sheet, ParseSheet(parser));
386 ASSIGN_OR_RETURN(const int block, ParseIntArg(parser, "block", 0));
387 ASSIGN_OR_RETURN(const int count, ParseIntArg(parser, "count", 16 - block));
388 std::optional<zelda3::RoomSheetPaletteContext> context;
389 ASSIGN_OR_RETURN(const auto palette,
390 ResolvePalette(*rom, parser, sheet, &context));
391 ASSIGN_OR_RETURN(const auto data, zelda3::ReadGfxSheetData(*rom, sheet));
392 ASSIGN_OR_RETURN(const auto png,
393 zelda3::ExportSheetBlocksPng(data, palette, block, count));
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);
399 RETURN_IF_ERROR(ValidateExportPaths(*rom, outputs));
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())
407 RETURN_IF_ERROR(WriteJson(*json_path, metadata));
408
409 formatter.AddField("sheet", absl::StrFormat("0x%02X", sheet));
410 formatter.AddField("png", path);
411 formatter.AddField("blocks",
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());
418 }
419 return absl::OkStatus();
420}
421
423 const resources::ArgumentParser& parser) {
424 RETURN_IF_ERROR(parser.RequireArgs({"sheet", "png"}));
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");
429 }
430 if (write && !parser.GetString("out").has_value()) {
431 return absl::InvalidArgumentError(
432 "--write needs --out <rom file>; the input ROM is never overwritten");
433 }
434 return absl::OkStatus();
435}
436
438 Rom* rom, const resources::ArgumentParser& parser,
439 resources::OutputFormatter& formatter) {
440 RETURN_IF_ERROR(RequireUsGfxTables(*rom));
441 ASSIGN_OR_RETURN(const uint16_t sheet, ParseSheet(parser));
442 ASSIGN_OR_RETURN(const int block, ParseIntArg(parser, "block", 0));
443 std::optional<zelda3::RoomSheetPaletteContext> context;
444 ASSIGN_OR_RETURN(const auto palette,
445 ResolvePalette(*rom, parser, sheet, &context));
446 if (context.has_value()) {
447 formatter.AddRawJsonField("room_context",
448 PaletteContextJson(*context).dump());
449 }
450 ASSIGN_OR_RETURN(const auto current, zelda3::ReadGfxSheetData(*rom, sheet));
451 const std::string png_path = *parser.GetString("png");
452 ASSIGN_OR_RETURN(const auto png_bytes, util::ReadBinaryFile(png_path));
453 ASSIGN_OR_RETURN(const auto png, util::DecodePng(png_bytes));
454 ASSIGN_OR_RETURN(const auto imported,
455 zelda3::ImportSheetBlocksPng(current, png, block, palette));
456
457 // Run the real writer on a copy of the ROM buffer; the diff is exactly
458 // what --write would change.
460 if (project_ != nullptr) {
462 }
463 Rom simulated = *rom;
464 const auto written =
465 zelda3::WriteGfxSheet(simulated, sheet, imported.snes_3bpp, policy);
466
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, " "));
471 formatter.AddField("changed_tiles",
472 static_cast<int>(imported.changed_tiles.size()));
473 formatter.AddField("sheet_byte_ranges", RangeText(imported.changed_ranges));
474 if (!written.ok()) {
475 // The preview still stands; say why the write would be refused.
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();
480 }
481 return written.status();
482 }
483 formatter.AddField("write_status", "ok");
484 const auto rom_diff =
485 rom::ComputeDiffRanges(rom->vector(), simulated.vector());
486 formatter.AddField("placement",
487 written->placement == zelda3::GfxSheetPlacement::kRelocated
488 ? "relocated"
489 : "in place");
490 formatter.AddField("rom_pc", absl::StrFormat("0x%06X", written->new_pc));
491 formatter.AddField("stored_bytes",
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));
496 formatter.AddField(
497 "rom_ranges",
498 absl::StrJoin(rom_diff.ranges, " ",
499 [](std::string* out, const auto& range) {
500 out->append(absl::StrFormat("0x%06X-0x%06X", range.first,
501 range.second - 1));
502 }));
503
504 if (parser.HasFlag("dry-run")) {
505 formatter.AddField("mode", "dry-run");
506 return absl::OkStatus();
507 }
508 const std::string out = *parser.GetString("out");
509 std::error_code ec;
510 if (!rom->filename().empty() &&
511 std::filesystem::equivalent(out, rom->filename(), ec)) {
512 return absl::InvalidArgumentError(
513 "--out is the input ROM; write to a new file");
514 }
516 formatter.AddField("mode", "write");
517 formatter.AddField("out", out);
518 return absl::OkStatus();
519}
520
522 const resources::ArgumentParser& parser) {
523 RETURN_IF_ERROR(parser.RequireArgs({"room"}));
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");
527 }
528 if (bundle && (parser.GetString("palette").has_value() ||
529 parser.GetString("json").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");
533 }
534 return absl::OkStatus();
535}
536
538 Rom* rom, const resources::ArgumentParser& parser,
539 resources::OutputFormatter& formatter) {
540 RETURN_IF_ERROR(RequireUsGfxTables(*rom));
541 ASSIGN_OR_RETURN(auto context, LoadRoomContext(*rom, parser));
542 if (const auto directory = parser.GetString("out-dir");
543 directory.has_value()) {
544 return ExportRoomBundle(*rom, context, *directory, formatter);
545 }
546 const auto& set = context.set;
547 ASSIGN_OR_RETURN(const auto png,
548 zelda3::ExportRoomBackgroundPng(*rom, set, context.palette));
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);
553 }
554 RETURN_IF_ERROR(ValidateExportPaths(*rom, outputs));
556
557 // Who else sees these sheets: an edit changes every one of these rooms.
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);
564 }
565 if (other.header_blockset == set.header_blockset) {
566 same_room_blockset.push_back(other.room);
567 }
568 }
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);
576 }
577 }
578 slots.push_back({{"slot", slot},
579 {"sheet", absl::StrFormat("0x%02X", set.sheets[slot])},
580 {"source", set.from_room_blockset[slot] ? "room_blockset"
581 : "main_blockset"},
582 {"png_y", slot * 32},
583 {"rooms_using_sheet", rooms}});
584 }
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"},
596 {"png", png_path},
597 {"slots", slots},
598 {"rooms_with_same_main_blockset", same_main},
599 {"rooms_with_same_room_blockset", same_room_blockset},
600 {"reimport", import_command},
601 };
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())));
606 }
607 for (const auto& [key, value] : sidecar.items()) {
608 formatter.AddRawJsonField(key, value.dump());
609 }
610 return absl::OkStatus();
611}
612
614 const resources::ArgumentParser& parser) {
615 RETURN_IF_ERROR(parser.RequireArgs({"room", "png"}));
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");
620 }
621 if (write && !parser.GetString("out").has_value()) {
622 return absl::InvalidArgumentError(
623 "--write needs --out <rom file>; the input ROM is never overwritten");
624 }
625 return absl::OkStatus();
626}
627
629 Rom* rom, const resources::ArgumentParser& parser,
630 resources::OutputFormatter& formatter) {
631 RETURN_IF_ERROR(RequireUsGfxTables(*rom));
632 ASSIGN_OR_RETURN(auto context, LoadRoomContext(*rom, parser));
633 const std::string png_path = *parser.GetString("png");
634 ASSIGN_OR_RETURN(const auto png_bytes, util::ReadBinaryFile(png_path));
635 ASSIGN_OR_RETURN(const auto png, util::DecodePng(png_bytes));
637 const auto imports,
638 zelda3::ImportRoomBackgroundPng(*rom, context.set, png, context.palette));
639
641 if (project_ != nullptr) {
643 }
644 Rom simulated = *rom;
645 const auto all_sets =
647 nlohmann::json sheets = nlohmann::json::array();
648 std::string refusal;
649 std::vector<int> affected_rooms;
650 for (const auto& import : imports) {
651 auto written = zelda3::WriteGfxSheet(simulated, import.sheet,
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()},
658 };
659 if (written.ok()) {
660 entry["placement"] =
661 written->placement == zelda3::GfxSheetPlacement::kRelocated
662 ? "relocated"
663 : "in place";
664 entry["stored_bytes"] = absl::StrFormat(
665 "%zu -> %zu", written->old_stored_size, written->new_stored_size);
666 } else {
667 entry["write_status"] = std::string(written.status().message());
668 if (refusal.empty()) {
669 refusal = entry["write_status"];
670 }
671 }
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);
676 }
677 }
678 sheets.push_back(std::move(entry));
679 }
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());
684
685 formatter.AddField("room", absl::StrFormat("0x%03X", context.set.room));
686 formatter.AddRawJsonField("sheets", sheets.dump());
687 formatter.AddField("rooms_affected", HexIds(affected_rooms));
688 formatter.AddField("write_status", refusal.empty() ? "ok" : refusal);
689 const auto rom_diff =
690 rom::ComputeDiffRanges(rom->vector(), simulated.vector());
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();
696 }
697 if (!refusal.empty()) {
698 return absl::FailedPreconditionError(refusal);
699 }
700 const std::string out = *parser.GetString("out");
701 std::error_code ec;
702 if (!rom->filename().empty() &&
703 std::filesystem::equivalent(out, rom->filename(), ec)) {
704 return absl::InvalidArgumentError(
705 "--out is the input ROM; write to a new file");
706 }
708 formatter.AddField("mode", "write");
709 formatter.AddField("out", out);
710 return absl::OkStatus();
711}
712
713} // namespace cli
714} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
auto filename() const
Definition rom.h:207
const auto & vector() const
Definition rom.h:205
auto data() const
Definition rom.h:201
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.
Utility for consistent output formatting across commands.
void AddRawJsonField(const std::string &key, const std::string &json)
Add a field whose value is already-serialized JSON (numbers, nested arrays, null)....
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
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)
Definition macro.h:62
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::StatusOr< zelda3::GfxSheetWritePolicy > BuildGfxSheetWritePolicy(const HackManifest &manifest)
DiffSummary ComputeDiffRanges(const std::vector< uint8_t > &before, const std::vector< uint8_t > &after)
Definition rom_diff.cc:11
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)
Definition hex.cc:133
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.
Definition game_data.cc:123
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)
Definition snes.cc:22
Rules a sheet write must follow.
std::array< uint8_t, 8 > sheets
std::array< bool, 8 > from_room_blockset
std::array< uint8_t, 8 > cgram_indices
RoomMainBlocksetResolution main