9#include <unordered_map>
10#include <unordered_set>
13#include "absl/status/status.h"
14#include "absl/strings/ascii.h"
15#include "absl/strings/str_format.h"
16#include "absl/strings/str_split.h"
17#include "absl/types/span.h"
19#include "nlohmann/json.hpp"
37using json = nlohmann::json;
41 std::unordered_set<int> tiles;
42 auto tiles_opt = parser.
GetString(
"tiles");
43 if (!tiles_opt.has_value()) {
47 for (absl::string_view token :
48 absl::StrSplit(tiles_opt.value(),
',', absl::SkipEmpty())) {
49 std::string t = std::string(absl::StripAsciiWhitespace(token));
51 if (!ParseHexString(t, &v)) {
52 return absl::InvalidArgumentError(
53 absl::StrFormat(
"Invalid tile value in --tiles: %s", t));
55 if (v < 0 || v > 0xFF) {
56 return absl::InvalidArgumentError(
57 absl::StrFormat(
"Tile value out of range (0x00-0xFF): %s", t));
66 std::string trimmed = std::string(absl::StripAsciiWhitespace(token));
68 if (!ParseHexString(trimmed, &room_id)) {
69 return absl::InvalidArgumentError(
70 absl::StrFormat(
"Invalid room ID: %s", trimmed));
73 return absl::OutOfRangeError(
74 absl::StrFormat(
"Room ID out of range: 0x%02X", room_id));
81 std::vector<int> room_ids;
82 bool any_explicit =
false;
84 if (
auto room_opt = parser.
GetString(
"room"); room_opt.has_value()) {
87 room_ids.push_back(room_id);
90 if (
auto rooms_opt = parser.
GetString(
"rooms"); rooms_opt.has_value()) {
92 for (absl::string_view token :
93 absl::StrSplit(rooms_opt.value(),
',', absl::SkipEmpty())) {
95 room_ids.push_back(room_id);
104 room_ids.push_back(room_id);
111 room_ids.push_back(room_id);
115 std::sort(room_ids.begin(), room_ids.end());
116 room_ids.erase(std::unique(room_ids.begin(), room_ids.end()), room_ids.end());
118 if (room_ids.empty()) {
119 return absl::InvalidArgumentError(
120 "No rooms selected (use --room, --rooms, or --all)");
126 std::ifstream in(path, std::ios::in | std::ios::binary);
128 return absl::NotFoundError(
129 absl::StrFormat(
"Cannot open file for reading: %s", path));
131 std::stringstream ss;
133 if (!in.good() && !in.eof()) {
134 return absl::InternalError(
135 absl::StrFormat(
"Failed while reading file: %s", path));
141 const std::string& content) {
142 std::ofstream out(path, std::ios::out | std::ios::binary | std::ios::trunc);
143 if (!out.is_open()) {
144 return absl::PermissionDeniedError(
145 absl::StrFormat(
"Cannot open file for writing: %s", path));
149 return absl::InternalError(
150 absl::StrFormat(
"Failed while writing file: %s", path));
152 return absl::OkStatus();
157 case absl::StatusCode::kOk:
159 case absl::StatusCode::kCancelled:
161 case absl::StatusCode::kUnknown:
163 case absl::StatusCode::kInvalidArgument:
164 return "INVALID_ARGUMENT";
165 case absl::StatusCode::kDeadlineExceeded:
166 return "DEADLINE_EXCEEDED";
167 case absl::StatusCode::kNotFound:
169 case absl::StatusCode::kAlreadyExists:
170 return "ALREADY_EXISTS";
171 case absl::StatusCode::kPermissionDenied:
172 return "PERMISSION_DENIED";
173 case absl::StatusCode::kResourceExhausted:
174 return "RESOURCE_EXHAUSTED";
175 case absl::StatusCode::kFailedPrecondition:
176 return "FAILED_PRECONDITION";
177 case absl::StatusCode::kAborted:
179 case absl::StatusCode::kOutOfRange:
180 return "OUT_OF_RANGE";
181 case absl::StatusCode::kUnimplemented:
182 return "UNIMPLEMENTED";
183 case absl::StatusCode::kInternal:
185 case absl::StatusCode::kUnavailable:
186 return "UNAVAILABLE";
187 case absl::StatusCode::kDataLoss:
189 case absl::StatusCode::kUnauthenticated:
190 return "UNAUTHENTICATED";
197 {
"command", std::string(command_name)},
198 {
"status",
"success"},
199 {
"dry_run", dry_run},
200 {
"mode", dry_run ?
"dry-run" :
"write"},
205 const json& report) {
206 auto report_path = parser.
GetString(
"report");
207 if (!report_path.has_value()) {
208 return absl::OkStatus();
214 json report,
const absl::Status& status) {
216 report[
"status"] =
"error";
217 report[
"error"] =
json{
219 {
"message", std::string(status.message())},
224 if (!report_status.ok()) {
226 return report_status;
228 return absl::InternalError(
229 absl::StrFormat(
"Command failed (%s) and report write failed (%s)",
230 status.message(), report_status.message()));
239 out[
"ok"] = preflight.
ok();
240 json errors = json::array();
241 for (
const auto& err : preflight.
errors) {
243 e[
"code"] = err.code;
244 e[
"message"] = err.message;
246 if (err.room_id >= 0) {
247 e[
"room_id"] = absl::StrFormat(
"0x%02X", err.room_id);
249 errors.push_back(std::move(e));
251 out[
"errors"] = std::move(errors);
256 const std::vector<zelda3::WaterFillZoneEntry>& zones) {
257 constexpr std::array<int, 2> kD4RoomIdsRequiringCollision = {0x25, 0x27};
259 std::unordered_set<int> imported_rooms;
260 imported_rooms.reserve(zones.size());
261 for (
const auto& zone : zones) {
262 imported_rooms.insert(zone.room_id);
265 std::vector<int> required_rooms;
266 for (
int room_id : kD4RoomIdsRequiringCollision) {
267 if (imported_rooms.contains(room_id)) {
268 required_rooms.push_back(room_id);
271 return required_rooms;
279 auto room_id_str = parser.
GetString(
"room").value();
282 if (!ParseHexString(room_id_str, &room_id)) {
283 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
288 const bool list_all = parser.
HasFlag(
"all");
289 const bool list_nonzero =
290 parser.
HasFlag(
"nonzero") || (!list_all && filter_tiles.empty());
293 formatter.
AddField(
"room_id", room_id);
297 !filter_tiles.empty()
299 : (list_all ?
"all" : (list_nonzero ?
"nonzero" :
"all")));
303 formatter.
AddField(
"status",
"error");
304 formatter.
AddField(
"error", map_or.status().ToString());
306 return map_or.status();
309 const auto& map = map_or.value();
310 formatter.
AddField(
"has_data", map.has_data);
312 int nonzero_count = 0;
313 for (uint8_t tile : map.tiles) {
318 formatter.
AddField(
"nonzero_tiles", nonzero_count);
323 for (
int y = 0; y < 64; ++y) {
324 for (
int x = 0; x < 64; ++x) {
325 uint8_t tile = map.tiles[
static_cast<size_t>(y * 64 + x)];
327 if (!filter_tiles.empty()) {
328 if (filter_tiles.find(
static_cast<int>(tile)) == filter_tiles.end()) {
331 }
else if (list_nonzero) {
335 }
else if (!list_all) {
353 formatter.
AddField(
"match_count", match_count);
354 formatter.
AddField(
"status",
"success");
356 return absl::OkStatus();
363 const absl::Status status = [&]() -> absl::Status {
365 const std::string out_path = parser.
GetString(
"out").value();
366 report[
"out_path"] = out_path;
367 report[
"requested_rooms"] =
static_cast<int>(room_ids.size());
369 std::vector<zelda3::CustomCollisionRoomEntry> export_rooms;
370 export_rooms.reserve(room_ids.size());
372 for (
int room_id : room_ids) {
380 for (
int offset = 0; offset < kCollisionGridSize * kCollisionGridSize;
382 const uint8_t tile = map.tiles[
static_cast<size_t>(offset)];
387 static_cast<uint16_t
>(offset), tile});
389 if (!entry.
tiles.empty()) {
390 export_rooms.push_back(std::move(entry));
395 const std::string exported_json,
398 report[
"exported_rooms"] =
static_cast<int>(export_rooms.size());
401 formatter.
AddField(
"out_path", out_path);
402 formatter.
AddField(
"requested_rooms",
static_cast<int>(room_ids.size()));
403 formatter.
AddField(
"exported_rooms",
static_cast<int>(export_rooms.size()));
404 formatter.
AddField(
"status",
"success");
406 return absl::OkStatus();
409 return FinalizeWithReport(parser, std::move(report), status);
415 const bool dry_run = parser.
HasFlag(
"dry-run");
417 const absl::Status status = [&]() -> absl::Status {
418 const std::string in_path = parser.
GetString(
"in").value();
419 const bool replace_all = parser.
HasFlag(
"replace-all");
420 const bool force = parser.
HasFlag(
"force");
421 report[
"in_path"] = in_path;
422 report[
"replace_all"] = replace_all;
423 report[
"force"] = force;
426 return absl::FailedPreconditionError(
427 "Custom collision write support not present in this ROM");
435 const auto preflight =
437 report[
"preflight"] = BuildPreflightJson(preflight);
438 if (!preflight.ok()) {
439 return preflight.ToStatus();
442 if (replace_all && !dry_run && !force) {
443 return absl::FailedPreconditionError(
444 "--replace-all requires --force (run with --dry-run first)");
451 report[
"imported_room_entries"] =
static_cast<int>(imported_rooms.size());
455 std::vector<zelda3::Room> rooms;
458 rooms.emplace_back(room_id, rom,
nullptr);
461 int populated_rooms = 0;
462 int cleared_rooms = 0;
463 std::unordered_set<int> touched_rooms;
464 for (
const auto& imported : imported_rooms) {
465 touched_rooms.insert(imported.room_id);
466 auto& room = rooms[imported.room_id];
467 room.custom_collision().tiles.fill(0);
468 room.custom_collision().has_data =
false;
469 room.MarkCustomCollisionDirty();
471 bool has_nonzero =
false;
472 for (
const auto& tile : imported.tiles) {
473 const int offset =
static_cast<int>(tile.offset);
474 if (offset < 0 || offset >= kCollisionGridSize * kCollisionGridSize) {
477 if (tile.value == 0) {
480 room.SetCollisionTile(offset % kCollisionGridSize,
481 offset / kCollisionGridSize, tile.value);
492 int replace_all_clears = 0;
495 if (touched_rooms.contains(room_id)) {
498 auto& room = rooms[room_id];
499 room.custom_collision().tiles.fill(0);
500 room.custom_collision().has_data =
false;
501 room.MarkCustomCollisionDirty();
503 ++replace_all_clears;
506 report[
"replace_all_clears"] = replace_all_clears;
512 report[
"populated_rooms"] = populated_rooms;
513 report[
"cleared_rooms"] = cleared_rooms;
516 formatter.
AddField(
"in_path", in_path);
517 formatter.
AddField(
"replace_all", replace_all);
519 formatter.
AddField(
"mode", dry_run ?
"dry-run" :
"write");
520 formatter.
AddField(
"imported_room_entries",
521 static_cast<int>(imported_rooms.size()));
522 formatter.
AddField(
"populated_rooms", populated_rooms);
523 formatter.
AddField(
"cleared_rooms", cleared_rooms);
524 formatter.
AddField(
"status",
"success");
526 return absl::OkStatus();
529 return FinalizeWithReport(parser, std::move(report), status);
536 const absl::Status status = [&]() -> absl::Status {
537 const std::string out_path = parser.
GetString(
"out").value();
539 report[
"out_path"] = out_path;
540 report[
"requested_rooms"] =
static_cast<int>(room_ids.size());
543 return absl::FailedPreconditionError(
544 "WaterFill reserved region missing in this ROM");
548 std::unordered_set<int> room_filter(room_ids.begin(), room_ids.end());
549 std::vector<zelda3::WaterFillZoneEntry> filtered;
550 filtered.reserve(zones.size());
551 for (
const auto& zone : zones) {
552 if (!room_filter.contains(zone.room_id)) {
555 filtered.push_back(zone);
561 report[
"exported_zones"] =
static_cast<int>(filtered.size());
564 formatter.
AddField(
"out_path", out_path);
565 formatter.
AddField(
"requested_rooms",
static_cast<int>(room_ids.size()));
566 formatter.
AddField(
"exported_zones",
static_cast<int>(filtered.size()));
567 formatter.
AddField(
"status",
"success");
569 return absl::OkStatus();
572 return FinalizeWithReport(parser, std::move(report), status);
578 const bool dry_run = parser.
HasFlag(
"dry-run");
579 const bool strict_masks = parser.
HasFlag(
"strict-masks");
581 const absl::Status status = [&]() -> absl::Status {
582 const std::string in_path = parser.
GetString(
"in").value();
583 report[
"in_path"] = in_path;
584 report[
"strict_masks"] = strict_masks;
587 return absl::FailedPreconditionError(
588 "WaterFill reserved region missing in this ROM");
594 const auto required_collision_rooms =
595 RequiredCollisionRoomsForImportedWaterFillZones(zones);
596 if (!required_collision_rooms.empty()) {
597 json required_rooms_json = json::array();
598 for (
int room_id : required_collision_rooms) {
599 required_rooms_json.push_back(absl::StrFormat(
"0x%02X", room_id));
601 report[
"required_collision_rooms"] = std::move(required_rooms_json);
610 required_collision_rooms;
611 const auto preflight =
613 report[
"preflight"] = BuildPreflightJson(preflight);
614 if (!preflight.ok()) {
615 return preflight.ToStatus();
618 auto original_zones = zones;
621 int normalized_masks = 0;
622 std::unordered_map<int, uint8_t> before_masks;
623 before_masks.reserve(original_zones.size());
624 for (
const auto& z : original_zones) {
625 before_masks[z.room_id] = z.sram_bit_mask;
627 for (
const auto& z : zones) {
628 auto it = before_masks.find(z.room_id);
629 if (it == before_masks.end() || it->second != z.sram_bit_mask) {
634 report[
"zone_count"] =
static_cast<int>(zones.size());
635 report[
"normalized_masks"] = normalized_masks;
637 if (strict_masks && normalized_masks > 0) {
638 return absl::FailedPreconditionError(absl::StrFormat(
639 "WaterFill masks require normalization (%d changed); rerun without "
640 "--strict-masks to apply normalized masks",
649 formatter.
AddField(
"in_path", in_path);
650 formatter.
AddField(
"mode", dry_run ?
"dry-run" :
"write");
651 formatter.
AddField(
"strict_masks", strict_masks);
652 formatter.
AddField(
"zone_count",
static_cast<int>(zones.size()));
653 formatter.
AddField(
"normalized_masks", normalized_masks);
654 formatter.
AddField(
"status",
"success");
656 return absl::OkStatus();
659 return FinalizeWithReport(parser, std::move(report), status);
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
std::string GetName() const override
Get the command name.
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.
std::string GetName() const override
Get the command name.
std::string GetName() const override
Get the command name.
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.
std::string GetName() const override
Get the command name.
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.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::StatusOr< int > ParseRoomIdToken(absl::string_view token)
absl::Status WriteTextFile(const std::string &path, const std::string &content)
absl::StatusOr< std::unordered_set< int > > ParseTileFilter(const resources::ArgumentParser &parser)
absl::StatusOr< std::string > ReadTextFile(const std::string &path)
constexpr int kCollisionGridSize
absl::Status WriteReportIfRequested(const resources::ArgumentParser &parser, const json &report)
json BuildBaseReport(absl::string_view command_name, bool dry_run)
std::vector< int > RequiredCollisionRoomsForImportedWaterFillZones(const std::vector< zelda3::WaterFillZoneEntry > &zones)
absl::Status FinalizeWithReport(const resources::ArgumentParser &parser, json report, const absl::Status &status)
std::string StatusCodeName(absl::StatusCode code)
json BuildPreflightJson(const zelda3::OracleRomSafetyPreflightResult &preflight)
absl::StatusOr< std::vector< int > > ParseRoomSelection(const resources::ArgumentParser &parser)
bool ParseHexString(absl::string_view str, int *out)
absl::StatusOr< std::string > DumpWaterFillZonesToJsonString(const std::vector< WaterFillZoneEntry > &zones)
absl::Status NormalizeWaterFillZoneMasks(std::vector< WaterFillZoneEntry > *zones)
absl::StatusOr< std::vector< CustomCollisionRoomEntry > > LoadCustomCollisionRoomsFromJsonString(const std::string &json_content)
absl::StatusOr< std::string > DumpCustomCollisionRoomsToJsonString(const std::vector< CustomCollisionRoomEntry > &rooms)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillZonesFromJsonString(const std::string &json_content)
OracleRomSafetyPreflightResult RunOracleRomSafetyPreflight(Rom *rom, const OracleRomSafetyPreflightOptions &options)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr bool HasWaterFillReservedRegion(std::size_t rom_size)
constexpr int kNumberOfRooms
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
absl::Status WriteWaterFillTable(Rom *rom, const std::vector< WaterFillZoneEntry > &zones)
#define RETURN_IF_ERROR(expr)
std::vector< CustomCollisionTileEntry > tiles
bool validate_custom_collision_maps
std::vector< int > room_ids_requiring_custom_collision
bool require_custom_collision_write_support
bool require_water_fill_reserved_region
bool validate_water_fill_table
std::vector< OracleRomSafetyIssue > errors