15#include <system_error>
16#include <unordered_map>
17#include <unordered_set>
21#include "absl/flags/declare.h"
22#include "absl/flags/flag.h"
23#include "absl/status/status.h"
24#include "absl/strings/ascii.h"
25#include "absl/strings/str_format.h"
26#include "absl/strings/str_split.h"
27#include "absl/strings/string_view.h"
28#include "absl/types/span.h"
30#include "nlohmann/json.hpp"
65using json = nlohmann::json;
69 std::unordered_set<int> tiles;
70 auto tiles_opt = parser.
GetString(
"tiles");
71 if (!tiles_opt.has_value()) {
75 for (absl::string_view token :
76 absl::StrSplit(tiles_opt.value(),
',', absl::SkipEmpty())) {
77 std::string t = std::string(absl::StripAsciiWhitespace(token));
79 if (!ParseHexString(t, &v)) {
80 return absl::InvalidArgumentError(
81 absl::StrFormat(
"Invalid tile value in --tiles: %s", t));
83 if (v < 0 || v > 0xFF) {
84 return absl::InvalidArgumentError(
85 absl::StrFormat(
"Tile value out of range (0x00-0xFF): %s", t));
94 std::string trimmed = std::string(absl::StripAsciiWhitespace(token));
96 if (!ParseHexString(trimmed, &room_id)) {
97 return absl::InvalidArgumentError(
98 absl::StrFormat(
"Invalid room ID: %s", trimmed));
101 return absl::OutOfRangeError(
102 absl::StrFormat(
"Room ID out of range: 0x%02X", room_id));
109 std::vector<int> room_ids;
110 bool any_explicit =
false;
112 if (
auto room_opt = parser.
GetString(
"room"); room_opt.has_value()) {
115 room_ids.push_back(room_id);
118 if (
auto rooms_opt = parser.
GetString(
"rooms"); rooms_opt.has_value()) {
120 for (absl::string_view token :
121 absl::StrSplit(rooms_opt.value(),
',', absl::SkipEmpty())) {
123 room_ids.push_back(room_id);
132 room_ids.push_back(room_id);
139 room_ids.push_back(room_id);
143 std::sort(room_ids.begin(), room_ids.end());
144 room_ids.erase(std::unique(room_ids.begin(), room_ids.end()), room_ids.end());
146 if (room_ids.empty()) {
147 return absl::InvalidArgumentError(
148 "No rooms selected (use --room, --rooms, or --all)");
153absl::StatusOr<std::string> ReadTextFile(
const std::string& path) {
154 std::ifstream in(path, std::ios::in | std::ios::binary);
156 return absl::NotFoundError(
157 absl::StrFormat(
"Cannot open file for reading: %s", path));
159 std::stringstream ss;
161 if (!in.good() && !in.eof()) {
162 return absl::InternalError(
163 absl::StrFormat(
"Failed while reading file: %s", path));
169 const std::filesystem::path& target_path) {
170 static std::atomic<uint64_t> sequence{0};
171 const uint64_t tick =
static_cast<uint64_t
>(
172 std::chrono::steady_clock::now().time_since_epoch().count());
173 const uint64_t
id = sequence.fetch_add(1, std::memory_order_relaxed);
175 std::filesystem::path temp_name = target_path.filename();
176 temp_name += absl::StrFormat(
".yaze-tmp-%016x-%016x", tick,
id);
177 const auto parent = target_path.parent_path().empty()
178 ? std::filesystem::path(
".")
179 : target_path.parent_path();
180 return parent / temp_name;
184 const std::filesystem::path& target_path, absl::string_view content) {
185 constexpr int kMaxCreateAttempts = 100;
187 for (
int attempt = 0; attempt < kMaxCreateAttempts; ++attempt) {
191 HANDLE file = CreateFileW(
192 temp_path.wstring().c_str(), GENERIC_WRITE, 0,
nullptr, CREATE_NEW,
193 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH,
nullptr);
194 if (file == INVALID_HANDLE_VALUE) {
195 const DWORD error = GetLastError();
196 if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
199 return absl::PermissionDeniedError(absl::StrFormat(
200 "Cannot create temporary artifact for %s: %s", target_path.string(),
201 std::error_code(
static_cast<int>(error), std::system_category())
205 size_t written_total = 0;
206 while (written_total < content.size()) {
207 const size_t remaining = content.size() - written_total;
208 const DWORD chunk =
static_cast<DWORD
>(
209 std::min<size_t>(remaining, std::numeric_limits<DWORD>::max()));
211 if (!WriteFile(file, content.data() + written_total, chunk, &written,
214 const DWORD error = GetLastError();
216 std::error_code cleanup_ec;
217 std::filesystem::remove(temp_path, cleanup_ec);
218 return absl::InternalError(absl::StrFormat(
219 "Failed while writing temporary artifact for %s: %s",
220 target_path.string(),
221 std::error_code(
static_cast<int>(error), std::system_category())
224 written_total += written;
227 if (!FlushFileBuffers(file)) {
228 const DWORD error = GetLastError();
230 std::error_code cleanup_ec;
231 std::filesystem::remove(temp_path, cleanup_ec);
232 return absl::InternalError(absl::StrFormat(
233 "Failed to flush temporary artifact for %s: %s", target_path.string(),
234 std::error_code(
static_cast<int>(error), std::system_category())
237 if (!CloseHandle(file)) {
238 const DWORD error = GetLastError();
239 std::error_code cleanup_ec;
240 std::filesystem::remove(temp_path, cleanup_ec);
241 return absl::InternalError(absl::StrFormat(
242 "Failed to close temporary artifact for %s: %s", target_path.string(),
243 std::error_code(
static_cast<int>(error), std::system_category())
248 open(temp_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
250 const int error = errno;
251 if (error == EEXIST) {
254 return absl::PermissionDeniedError(absl::StrFormat(
255 "Cannot create temporary artifact for %s: %s", target_path.string(),
256 std::error_code(error, std::generic_category()).message()));
259 size_t written_total = 0;
260 while (written_total < content.size()) {
261 const size_t remaining = content.size() - written_total;
262 const size_t chunk = std::min<size_t>(remaining, 1024 * 1024);
263 const ssize_t written = write(fd, content.data() + written_total, chunk);
264 if (written < 0 && errno == EINTR) {
268 const int error = written < 0 ? errno : EIO;
270 std::error_code cleanup_ec;
271 std::filesystem::remove(temp_path, cleanup_ec);
272 return absl::InternalError(absl::StrFormat(
273 "Failed while writing temporary artifact for %s: %s",
274 target_path.string(),
275 std::error_code(error, std::generic_category()).message()));
277 written_total +=
static_cast<size_t>(written);
280 if (fsync(fd) != 0) {
281 const int error = errno;
283 std::error_code cleanup_ec;
284 std::filesystem::remove(temp_path, cleanup_ec);
285 return absl::InternalError(absl::StrFormat(
286 "Failed to flush temporary artifact for %s: %s", target_path.string(),
287 std::error_code(error, std::generic_category()).message()));
289 if (close(fd) != 0) {
290 const int error = errno;
291 std::error_code cleanup_ec;
292 std::filesystem::remove(temp_path, cleanup_ec);
293 return absl::InternalError(absl::StrFormat(
294 "Failed to close temporary artifact for %s: %s", target_path.string(),
295 std::error_code(error, std::generic_category()).message()));
302 return absl::ResourceExhaustedError(
303 absl::StrFormat(
"Could not allocate a unique temporary artifact for %s",
304 target_path.string()));
308 const std::filesystem::path& target_path) {
309 std::error_code rename_ec;
310 std::filesystem::rename(temp_path, target_path, rename_ec);
313 if (MoveFileExW(temp_path.wstring().c_str(), target_path.wstring().c_str(),
314 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
317 rename_ec = std::error_code(
static_cast<int>(GetLastError()),
318 std::system_category());
323 return absl::InternalError(
324 absl::StrFormat(
"Failed to publish artifact %s: %s",
325 target_path.string(), rename_ec.message()));
327 return absl::OkStatus();
331 std::error_code status_ec;
332 const auto status = std::filesystem::symlink_status(target_path, status_ec);
333 if (status_ec && status_ec != std::errc::no_such_file_or_directory) {
334 return absl::FailedPreconditionError(
335 absl::StrFormat(
"Cannot inspect artifact target %s: %s",
336 target_path.string(), status_ec.message()));
338 if (!status_ec && std::filesystem::is_directory(status)) {
339 return absl::FailedPreconditionError(
340 absl::StrFormat(
"Artifact target is a directory, not a file: %s",
341 target_path.string()));
343 if (!status_ec && std::filesystem::exists(status) &&
344 !std::filesystem::is_regular_file(status) &&
345 !std::filesystem::is_symlink(status)) {
346 return absl::FailedPreconditionError(absl::StrFormat(
347 "Artifact target is not a replaceable file: %s", target_path.string()));
349 return absl::OkStatus();
353 const std::filesystem::path& target_path) {
354 std::error_code status_ec;
355 const auto status = std::filesystem::symlink_status(target_path, status_ec);
356 if (status_ec == std::errc::no_such_file_or_directory ||
357 (!status_ec && !std::filesystem::exists(status))) {
361 return absl::FailedPreconditionError(
362 absl::StrFormat(
"Cannot inspect existing artifact %s: %s",
363 target_path.string(), status_ec.message()));
366 constexpr int kMaxCreateAttempts = 100;
367 for (
int attempt = 0; attempt < kMaxCreateAttempts; ++attempt) {
368 const std::filesystem::path rollback_path =
370 std::error_code copy_ec;
371 if (std::filesystem::is_symlink(status)) {
372 std::filesystem::copy_symlink(target_path, rollback_path, copy_ec);
374 std::filesystem::copy_file(target_path, rollback_path,
375 std::filesystem::copy_options::none, copy_ec);
378 return rollback_path;
380 if (copy_ec == std::errc::file_exists) {
383 return absl::FailedPreconditionError(absl::StrFormat(
384 "Cannot preserve existing artifact %s before publication: %s",
385 target_path.string(), copy_ec.message()));
388 return absl::ResourceExhaustedError(
389 absl::StrFormat(
"Could not allocate a rollback copy for artifact %s",
390 target_path.string()));
398 bool published =
false;
399 bool preserve_rollback_copy =
false;
403 const std::vector<PendingArtifactPublication>& artifacts) {
404 for (
const auto& artifact : artifacts) {
405 std::error_code cleanup_ec;
406 if (!artifact.temp_path.empty()) {
407 std::filesystem::remove(artifact.temp_path, cleanup_ec);
409 if (artifact.rollback_path.has_value() &&
410 !artifact.preserve_rollback_copy) {
412 std::filesystem::remove(*artifact.rollback_path, cleanup_ec);
418 std::vector<PendingArtifactPublication>* artifacts,
419 const absl::Status& publication_failure) {
420 absl::Status rollback_failure = absl::OkStatus();
421 for (
auto it = artifacts->rbegin(); it != artifacts->rend(); ++it) {
422 if (!it->published) {
426 if (it->rollback_path.has_value()) {
427 const absl::Status restore_status =
429 if (!restore_status.ok() && rollback_failure.ok()) {
430 it->preserve_rollback_copy =
true;
431 rollback_failure = absl::InternalError(absl::StrFormat(
432 "%s; preserved the original artifact at %s",
433 restore_status.message(), it->rollback_path->string()));
434 }
else if (!restore_status.ok()) {
435 it->preserve_rollback_copy =
true;
436 }
else if (restore_status.ok()) {
437 it->rollback_path.reset();
440 std::error_code remove_ec;
441 if (!std::filesystem::remove(it->target_path, remove_ec) || remove_ec) {
442 if (rollback_failure.ok()) {
443 rollback_failure = absl::InternalError(absl::StrFormat(
444 "Could not remove newly published artifact %s during rollback: "
446 it->target_path.string(),
447 remove_ec ? remove_ec.message() :
"artifact was missing"));
451 it->published =
false;
454 if (!rollback_failure.ok()) {
455 return absl::DataLossError(absl::StrFormat(
456 "Artifact publication failed (%s) and rollback failed (%s)",
457 publication_failure.message(), rollback_failure.message()));
459 return publication_failure;
463 const std::vector<PendingArtifactPublication>& artifacts) {
464 for (
size_t lhs_index = 0; lhs_index < artifacts.size(); ++lhs_index) {
465 for (
size_t rhs_index = lhs_index + 1; rhs_index < artifacts.size();
467 std::error_code equivalent_ec;
468 if (std::filesystem::equivalent(artifacts[lhs_index].target_path,
469 artifacts[rhs_index].target_path,
471 return absl::InvalidArgumentError(absl::StrFormat(
472 "Export artifact paths alias each other on this filesystem: %s "
474 artifacts[lhs_index].target_path.string(),
475 artifacts[rhs_index].target_path.string()));
478 if (!equivalent_ec) {
485 std::error_code lhs_exists_ec;
486 std::error_code rhs_exists_ec;
487 const bool lhs_exists = std::filesystem::exists(
488 artifacts[lhs_index].target_path, lhs_exists_ec);
489 const bool rhs_exists = std::filesystem::exists(
490 artifacts[rhs_index].target_path, rhs_exists_ec);
491 if (lhs_exists_ec || rhs_exists_ec || (lhs_exists && rhs_exists)) {
492 return absl::FailedPreconditionError(absl::StrFormat(
493 "Could not safely distinguish export artifact paths %s and %s: "
495 artifacts[lhs_index].target_path.string(),
496 artifacts[rhs_index].target_path.string(),
497 equivalent_ec.message()));
501 return absl::OkStatus();
505 std::vector<PendingArtifactPublication> artifacts) {
506 for (
const auto& artifact : artifacts) {
507 const absl::Status target_status =
509 if (!target_status.ok()) {
511 return target_status;
515 for (
auto& artifact : artifacts) {
520 return temp_or.status();
522 artifact.temp_path = *temp_or;
525 for (
auto& artifact : artifacts) {
527 if (!rollback_or.ok()) {
529 return rollback_or.status();
531 artifact.rollback_path = std::move(*rollback_or);
534 for (
auto& artifact : artifacts) {
535 const absl::Status publish_status =
537 if (!publish_status.ok()) {
538 const absl::Status final_status =
543 artifact.temp_path.clear();
544 artifact.published =
true;
546 const absl::Status alias_status =
548 if (!alias_status.ok()) {
549 const absl::Status final_status =
557 return absl::OkStatus();
561 const std::filesystem::path& target_path, absl::string_view content) {
562 std::vector<PendingArtifactPublication> artifacts;
565 .content = std::string(content),
571 const std::filesystem::path& out_path, absl::string_view out_content,
572 const std::optional<std::filesystem::path>& report_path,
573 absl::string_view report_content) {
576 std::vector<PendingArtifactPublication> artifacts;
579 .content = std::string(out_content),
581 if (report_path.has_value()) {
584 .content = std::string(report_content),
591 absl::string_view command_name) {
593 const auto report_path = parser.
GetString(
"report");
594 const bool has_report = report_path.has_value();
595 if (has_report && report_path->empty()) {
596 return absl::InvalidArgumentError(
597 absl::StrFormat(
"%s: --report cannot be empty", command_name));
599 const bool sandbox_requested =
600 parser.
HasFlag(
"sandbox") || absl::GetFlag(FLAGS_sandbox);
601 if (has_report && !parser.
HasFlag(
"dry-run")) {
602 return absl::InvalidArgumentError(absl::StrFormat(
603 "%s: --report is supported only with --dry-run; write-mode imports "
604 "must rely on command output and ROM backup verification",
607 if (has_report && sandbox_requested) {
608 return absl::InvalidArgumentError(absl::StrFormat(
609 "%s: --report cannot be combined with --sandbox; run the reported "
610 "dry-run against the source ROM",
613 if (parser.
HasFlag(
"mock-rom") && sandbox_requested) {
614 return absl::InvalidArgumentError(absl::StrFormat(
615 "%s: --mock-rom and --sandbox are mutually exclusive", command_name));
617 return absl::OkStatus();
621 absl::string_view command_name) {
624 return absl::InvalidArgumentError(
625 absl::StrFormat(
"%s: --out cannot be empty", command_name));
627 if (
const auto report = parser.
GetString(
"report");
628 report.has_value() && report->empty()) {
629 return absl::InvalidArgumentError(
630 absl::StrFormat(
"%s: --report cannot be empty", command_name));
632 return absl::OkStatus();
645 const auto out_path_value = parser.
GetString(
"out");
646 if (!out_path_value.has_value() || out_path_value->empty()) {
647 return absl::InvalidArgumentError(
"--out must name an artifact file");
651 ResolveStableArtifactPath(*out_path_value));
653 RejectArtifactRomAliases(
"--out", out_path, invocation_context));
657 const auto report_path_value = parser.
GetString(
"report");
658 if (!report_path_value.has_value()) {
659 return resolved_paths;
661 if (report_path_value->empty()) {
662 return absl::InvalidArgumentError(
"--report must name an artifact file");
666 ResolveStableArtifactPath(*report_path_value));
668 RejectArtifactRomAliases(
"--report", report_path, invocation_context));
671 PathsAlias(out_path, report_path));
672 if (artifacts_alias) {
673 return absl::InvalidArgumentError(absl::StrFormat(
674 "--out and --report paths alias each other; choose separate artifact "
679 resolved_paths.report_path = report_path;
680 return resolved_paths;
685 const auto report_path_value = parser.
GetString(
"report");
686 if (!report_path_value.has_value() || report_path_value->empty()) {
691 ResolveStableArtifactPath(*report_path_value));
692 if (rom !=
nullptr && !rom->
filename().empty()) {
694 const bool aliases_active_rom,
695 PathsAlias(report_path, std::filesystem::path(rom->
filename())));
696 if (aliases_active_rom) {
697 return absl::InvalidArgumentError(absl::StrFormat(
698 "--report path aliases the active ROM; choose a separate report "
700 report_path.string()));
709 case absl::StatusCode::kOk:
711 case absl::StatusCode::kCancelled:
713 case absl::StatusCode::kUnknown:
715 case absl::StatusCode::kInvalidArgument:
716 return "INVALID_ARGUMENT";
717 case absl::StatusCode::kDeadlineExceeded:
718 return "DEADLINE_EXCEEDED";
719 case absl::StatusCode::kNotFound:
721 case absl::StatusCode::kAlreadyExists:
722 return "ALREADY_EXISTS";
723 case absl::StatusCode::kPermissionDenied:
724 return "PERMISSION_DENIED";
725 case absl::StatusCode::kResourceExhausted:
726 return "RESOURCE_EXHAUSTED";
727 case absl::StatusCode::kFailedPrecondition:
728 return "FAILED_PRECONDITION";
729 case absl::StatusCode::kAborted:
731 case absl::StatusCode::kOutOfRange:
732 return "OUT_OF_RANGE";
733 case absl::StatusCode::kUnimplemented:
734 return "UNIMPLEMENTED";
735 case absl::StatusCode::kInternal:
737 case absl::StatusCode::kUnavailable:
738 return "UNAVAILABLE";
739 case absl::StatusCode::kDataLoss:
741 case absl::StatusCode::kUnauthenticated:
742 return "UNAUTHENTICATED";
749 {
"command", std::string(command_name)},
750 {
"status",
"success"},
751 {
"dry_run", dry_run},
752 {
"mode", dry_run ?
"dry-run" :
"write"},
757 const absl::Status& status) {
758 formatter.
AddField(
"status",
"error");
761 formatter.
AddField(
"message", std::string(status.message()));
766 const std::optional<std::filesystem::path>& resolved_report_path,
767 json report,
const absl::Status& status) {
769 report[
"status"] =
"error";
770 report[
"error"] =
json{
772 {
"message", std::string(status.message())},
776 absl::Status report_status = absl::OkStatus();
777 if (resolved_report_path.has_value()) {
779 report.dump(2) +
"\n");
781 if (!report_status.ok()) {
783 return report_status;
785 return absl::InternalError(
786 absl::StrFormat(
"Command failed (%s) and report write failed (%s)",
787 status.message(), report_status.message()));
795 const absl::Status& status) {
803 out[
"ok"] = preflight.
ok();
804 json errors = json::array();
805 for (
const auto& err : preflight.
errors) {
807 e[
"code"] = err.code;
808 e[
"message"] = err.message;
810 if (err.room_id >= 0) {
811 e[
"room_id"] = absl::StrFormat(
"0x%02X", err.room_id);
813 errors.push_back(std::move(e));
815 out[
"errors"] = std::move(errors);
819template <
typename Serializer>
822 json* report, Serializer&& serializer) {
825 const absl::Status write_status = std::forward<Serializer>(serializer)();
826 if (!write_status.ok()) {
827 (*report)[
"write_error"] = std::string(write_status.message());
830 (*report)[
"write_status"] =
"success";
835 if (parser.
HasFlag(
"mock-rom")) {
836 (*report)[
"save_status"] =
"mock-rom-skipped";
838 return absl::OkStatus();
843 const absl::Status save_status = rom->
SaveToFile(save_settings);
844 if (!save_status.ok()) {
845 (*report)[
"save_error"] = std::string(save_status.message());
849 (*report)[
"save_status"] =
"saved";
851 return absl::OkStatus();
855 const std::vector<zelda3::WaterFillZoneEntry>& zones) {
856 constexpr std::array<int, 2> kD4RoomIdsRequiringCollision = {0x25, 0x27};
858 std::unordered_set<int> imported_rooms;
859 imported_rooms.reserve(zones.size());
860 for (
const auto& zone : zones) {
861 imported_rooms.insert(zone.room_id);
864 std::vector<int> required_rooms;
865 for (
int room_id : kD4RoomIdsRequiringCollision) {
866 if (imported_rooms.contains(room_id)) {
867 required_rooms.push_back(room_id);
870 return required_rooms;
878 auto room_id_str = parser.
GetString(
"room").value();
881 if (!ParseHexString(room_id_str, &room_id)) {
882 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
887 const bool list_all = parser.
HasFlag(
"all");
888 const bool list_nonzero =
889 parser.
HasFlag(
"nonzero") || (!list_all && filter_tiles.empty());
892 formatter.
AddField(
"room_id", room_id);
896 !filter_tiles.empty()
898 : (list_all ?
"all" : (list_nonzero ?
"nonzero" :
"all")));
902 formatter.
AddField(
"status",
"error");
903 formatter.
AddField(
"error", map_or.status().ToString());
905 return map_or.status();
908 const auto& map = map_or.value();
909 formatter.
AddField(
"has_data", map.has_data);
911 int nonzero_count = 0;
912 for (uint8_t tile : map.tiles) {
917 formatter.
AddField(
"nonzero_tiles", nonzero_count);
922 for (
int y = 0; y < 64; ++y) {
923 for (
int x = 0; x < 64; ++x) {
924 uint8_t tile = map.tiles[
static_cast<size_t>(y * 64 + x)];
926 if (!filter_tiles.empty()) {
927 if (filter_tiles.find(
static_cast<int>(tile)) == filter_tiles.end()) {
930 }
else if (list_nonzero) {
934 }
else if (!list_all) {
952 formatter.
AddField(
"match_count", match_count);
953 formatter.
AddField(
"status",
"success");
955 return absl::OkStatus();
962 if (rom !=
nullptr && !rom->
filename().empty()) {
973 auto artifact_paths_or =
974 ResolveExportArtifactPaths(parser, invocation_context);
975 if (!artifact_paths_or.ok()) {
976 AddStructuredError(formatter, artifact_paths_or.status());
977 return artifact_paths_or.status();
979 const ResolvedExportArtifactPaths artifact_paths = *artifact_paths_or;
982 std::string out_path;
983 std::string exported_json;
984 int requested_rooms = 0;
985 int exported_room_count = 0;
986 const absl::Status status = [&]() -> absl::Status {
988 out_path = parser.
GetString(
"out").value();
989 requested_rooms =
static_cast<int>(room_ids.size());
990 report[
"out_path"] = out_path;
991 report[
"requested_rooms"] = requested_rooms;
993 std::vector<zelda3::CustomCollisionRoomEntry> export_rooms;
994 export_rooms.reserve(room_ids.size());
996 for (
int room_id : room_ids) {
1004 for (
int offset = 0; offset < kCollisionGridSize * kCollisionGridSize;
1006 const uint8_t tile = map.tiles[
static_cast<size_t>(offset)];
1011 static_cast<uint16_t
>(offset), tile});
1013 if (!entry.
tiles.empty()) {
1014 export_rooms.push_back(std::move(entry));
1021 exported_room_count =
static_cast<int>(export_rooms.size());
1022 report[
"exported_rooms"] = exported_room_count;
1024 return absl::OkStatus();
1029 auto final_artifact_paths_or =
1030 ResolveExportArtifactPaths(parser, invocation_context);
1031 absl::Status final_path_status = final_artifact_paths_or.status();
1032 if (final_artifact_paths_or.ok() &&
1033 *final_artifact_paths_or != artifact_paths) {
1034 final_path_status = absl::FailedPreconditionError(
1035 "Export artifact path identity changed during the command; no "
1036 "artifact was published");
1039 absl::Status final_status;
1044 final_status = final_path_status.ok()
1045 ? FinalizeExportWithReport(artifact_paths,
1046 std::move(report), status)
1048 }
else if (!final_path_status.ok()) {
1049 final_status = final_path_status;
1051 final_status = PublishExportArtifactsAtomically(
1052 artifact_paths.out_path, exported_json, artifact_paths.report_path,
1053 report.dump(2) +
"\n");
1055 if (!final_status.ok()) {
1056 AddStructuredError(formatter, final_status);
1057 return final_status;
1061 formatter.
AddField(
"out_path", out_path);
1062 formatter.
AddField(
"requested_rooms", requested_rooms);
1063 formatter.
AddField(
"exported_rooms", exported_room_count);
1064 formatter.
AddField(
"status",
"success");
1066 return absl::OkStatus();
1072 auto report_path_or = ResolveReportArtifactPath(parser, rom);
1073 if (!report_path_or.ok()) {
1074 AddStructuredError(formatter, report_path_or.status());
1075 return report_path_or.status();
1077 const std::optional<std::filesystem::path> report_path = *report_path_or;
1079 const bool dry_run = parser.
HasFlag(
"dry-run");
1080 json report = BuildBaseReport(
GetName(), dry_run);
1081 const absl::Status status = [&]() -> absl::Status {
1082 const std::string in_path = parser.
GetString(
"in").value();
1083 const bool replace_all = parser.
HasFlag(
"replace-all");
1084 const bool force = parser.
HasFlag(
"force");
1085 report[
"in_path"] = in_path;
1086 report[
"replace_all"] = replace_all;
1087 report[
"force"] = force;
1090 return absl::FailedPreconditionError(
1091 "Custom collision write support not present in this ROM");
1099 const auto preflight =
1101 report[
"preflight"] = BuildPreflightJson(preflight);
1102 if (!preflight.ok()) {
1103 return preflight.ToStatus();
1106 if (replace_all && !dry_run && !force) {
1107 return absl::FailedPreconditionError(
1108 "--replace-all requires --force (run with --dry-run first)");
1113 auto imported_rooms,
1115 report[
"imported_room_entries"] =
static_cast<int>(imported_rooms.size());
1119 std::vector<zelda3::Room> rooms;
1122 rooms.emplace_back(room_id, rom,
nullptr);
1125 int populated_rooms = 0;
1126 int cleared_rooms = 0;
1127 int changed_rooms = 0;
1128 int unchanged_rooms = 0;
1129 std::unordered_set<int> touched_rooms;
1130 for (
const auto& imported : imported_rooms) {
1131 touched_rooms.insert(imported.room_id);
1134 for (
const auto& tile : imported.tiles) {
1135 const int offset =
static_cast<int>(tile.offset);
1136 if (offset < 0 || offset >= kCollisionGridSize * kCollisionGridSize) {
1139 if (tile.value == 0) {
1142 desired.
tiles[
static_cast<size_t>(offset)] = tile.value;
1154 if (current.has_data == desired.
has_data &&
1155 current.tiles == desired.
tiles) {
1160 auto& room = rooms[imported.room_id];
1161 room.custom_collision() = desired;
1162 room.MarkCustomCollisionDirty();
1166 int replace_all_clears = 0;
1169 if (touched_rooms.contains(room_id)) {
1174 if (!current.has_data) {
1177 auto& room = rooms[room_id];
1178 room.custom_collision().tiles.fill(0);
1179 room.custom_collision().has_data =
false;
1180 room.MarkCustomCollisionDirty();
1183 ++replace_all_clears;
1186 report[
"changed_rooms"] = changed_rooms;
1187 report[
"unchanged_rooms"] = unchanged_rooms;
1188 report[
"replace_all_clears"] = replace_all_clears;
1189 report[
"populated_rooms"] = populated_rooms;
1190 report[
"cleared_rooms"] = cleared_rooms;
1193 if (changed_rooms == 0) {
1194 report[
"write_status"] =
"not-needed";
1195 report[
"save_status"] =
"not-needed";
1197 RETURN_IF_ERROR(SerializeAndPersistImport(rom, parser, &report, [&]() {
1204 formatter.
AddField(
"in_path", in_path);
1205 formatter.
AddField(
"replace_all", replace_all);
1206 formatter.
AddField(
"force", force);
1207 formatter.
AddField(
"mode", dry_run ?
"dry-run" :
"write");
1208 formatter.
AddField(
"imported_room_entries",
1209 static_cast<int>(imported_rooms.size()));
1210 formatter.
AddField(
"populated_rooms", populated_rooms);
1211 formatter.
AddField(
"cleared_rooms", cleared_rooms);
1212 formatter.
AddField(
"changed_rooms", changed_rooms);
1213 formatter.
AddField(
"unchanged_rooms", unchanged_rooms);
1214 formatter.
AddField(
"replace_all_clears", replace_all_clears);
1217 report.value(
"write_status", std::string(
"unknown")));
1219 report.value(
"save_status", std::string(
"unknown")));
1221 formatter.
AddField(
"status",
"success");
1223 return absl::OkStatus();
1226 return FinalizeWithReport(report_path, std::move(report), status);
1233 if (rom !=
nullptr && !rom->
filename().empty()) {
1244 auto artifact_paths_or =
1245 ResolveExportArtifactPaths(parser, invocation_context);
1246 if (!artifact_paths_or.ok()) {
1247 AddStructuredError(formatter, artifact_paths_or.status());
1248 return artifact_paths_or.status();
1250 const ResolvedExportArtifactPaths artifact_paths = *artifact_paths_or;
1253 std::string out_path;
1254 std::string exported_json;
1255 int requested_rooms = 0;
1256 int exported_zone_count = 0;
1257 const absl::Status status = [&]() -> absl::Status {
1258 out_path = parser.
GetString(
"out").value();
1260 requested_rooms =
static_cast<int>(room_ids.size());
1261 report[
"out_path"] = out_path;
1262 report[
"requested_rooms"] = requested_rooms;
1265 return absl::FailedPreconditionError(
1266 "WaterFill reserved region missing in this ROM");
1270 std::unordered_set<int> room_filter(room_ids.begin(), room_ids.end());
1271 std::vector<zelda3::WaterFillZoneEntry> filtered;
1272 filtered.reserve(zones.size());
1273 for (
const auto& zone : zones) {
1274 if (!room_filter.contains(zone.room_id)) {
1277 filtered.push_back(zone);
1282 exported_zone_count =
static_cast<int>(filtered.size());
1283 report[
"exported_zones"] = exported_zone_count;
1285 return absl::OkStatus();
1290 auto final_artifact_paths_or =
1291 ResolveExportArtifactPaths(parser, invocation_context);
1292 absl::Status final_path_status = final_artifact_paths_or.status();
1293 if (final_artifact_paths_or.ok() &&
1294 *final_artifact_paths_or != artifact_paths) {
1295 final_path_status = absl::FailedPreconditionError(
1296 "Export artifact path identity changed during the command; no "
1297 "artifact was published");
1300 absl::Status final_status;
1305 final_status = final_path_status.ok()
1306 ? FinalizeExportWithReport(artifact_paths,
1307 std::move(report), status)
1309 }
else if (!final_path_status.ok()) {
1310 final_status = final_path_status;
1312 final_status = PublishExportArtifactsAtomically(
1313 artifact_paths.out_path, exported_json, artifact_paths.report_path,
1314 report.dump(2) +
"\n");
1316 if (!final_status.ok()) {
1317 AddStructuredError(formatter, final_status);
1318 return final_status;
1322 formatter.
AddField(
"out_path", out_path);
1323 formatter.
AddField(
"requested_rooms", requested_rooms);
1324 formatter.
AddField(
"exported_zones", exported_zone_count);
1325 formatter.
AddField(
"status",
"success");
1327 return absl::OkStatus();
1333 auto report_path_or = ResolveReportArtifactPath(parser, rom);
1334 if (!report_path_or.ok()) {
1335 AddStructuredError(formatter, report_path_or.status());
1336 return report_path_or.status();
1338 const std::optional<std::filesystem::path> report_path = *report_path_or;
1340 const bool dry_run = parser.
HasFlag(
"dry-run");
1341 const bool strict_masks = parser.
HasFlag(
"strict-masks");
1342 json report = BuildBaseReport(
GetName(), dry_run);
1343 const absl::Status status = [&]() -> absl::Status {
1344 const std::string in_path = parser.
GetString(
"in").value();
1345 report[
"in_path"] = in_path;
1346 report[
"strict_masks"] = strict_masks;
1349 return absl::FailedPreconditionError(
1350 "WaterFill reserved region missing in this ROM");
1356 const auto required_collision_rooms =
1357 RequiredCollisionRoomsForImportedWaterFillZones(zones);
1358 if (!required_collision_rooms.empty()) {
1359 json required_rooms_json = json::array();
1360 for (
int room_id : required_collision_rooms) {
1361 required_rooms_json.push_back(absl::StrFormat(
"0x%02X", room_id));
1363 report[
"required_collision_rooms"] = std::move(required_rooms_json);
1372 required_collision_rooms;
1373 const auto preflight =
1375 report[
"preflight"] = BuildPreflightJson(preflight);
1376 if (!preflight.ok()) {
1377 return preflight.ToStatus();
1380 auto original_zones = zones;
1383 int normalized_masks = 0;
1384 std::unordered_map<int, uint8_t> before_masks;
1385 before_masks.reserve(original_zones.size());
1386 for (
const auto& z : original_zones) {
1387 before_masks[z.room_id] = z.sram_bit_mask;
1389 for (
const auto& z : zones) {
1390 auto it = before_masks.find(z.room_id);
1391 if (it == before_masks.end() || it->second != z.sram_bit_mask) {
1396 report[
"zone_count"] =
static_cast<int>(zones.size());
1397 report[
"normalized_masks"] = normalized_masks;
1399 if (strict_masks && normalized_masks > 0) {
1400 return absl::FailedPreconditionError(absl::StrFormat(
1401 "WaterFill masks require normalization (%d changed); rerun without "
1402 "--strict-masks to apply normalized masks",
1407 RETURN_IF_ERROR(SerializeAndPersistImport(rom, parser, &report, [&]() {
1413 formatter.
AddField(
"in_path", in_path);
1414 formatter.
AddField(
"mode", dry_run ?
"dry-run" :
"write");
1415 formatter.
AddField(
"strict_masks", strict_masks);
1416 formatter.
AddField(
"zone_count",
static_cast<int>(zones.size()));
1417 formatter.
AddField(
"normalized_masks", normalized_masks);
1420 report.value(
"write_status", std::string(
"unknown")));
1422 report.value(
"save_status", std::string(
"unknown")));
1424 formatter.
AddField(
"status",
"success");
1426 return absl::OkStatus();
1429 return FinalizeWithReport(report_path, std::move(report), status);
1434 return ValidateImportArguments(parser,
GetName());
1439 return ValidateExportArguments(parser,
GetName());
1444 return ValidateExportArguments(parser,
GetName());
1449 return ValidateImportArguments(parser,
GetName());
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 SaveToFile(const SaveSettings &settings)
std::string GetName() const override
Get the command name.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ExecuteWithContext(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, const resources::CommandInvocationContext &invocation_context) override
Execute with immutable, invocation-scoped ROM path identity.
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 Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ExecuteWithContext(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, const resources::CommandInvocationContext &invocation_context) override
Execute with immutable, invocation-scoped ROM path identity.
std::string GetName() const override
Get the command name.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
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 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.
ABSL_DECLARE_FLAG(bool, sandbox)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::StatusOr< int > ParseRoomIdToken(absl::string_view token)
std::filesystem::path NextArtifactTempPath(const std::filesystem::path &target_path)
absl::StatusOr< ResolvedExportArtifactPaths > ResolveExportArtifactPaths(const resources::ArgumentParser &parser, const resources::CommandInvocationContext &invocation_context)
absl::Status FinalizeExportWithReport(const ResolvedExportArtifactPaths &artifact_paths, json report, const absl::Status &status)
absl::StatusOr< std::unordered_set< int > > ParseTileFilter(const resources::ArgumentParser &parser)
absl::Status RollBackPublishedArtifacts(std::vector< PendingArtifactPublication > *artifacts, const absl::Status &publication_failure)
void AddStructuredError(resources::OutputFormatter &formatter, const absl::Status &status)
constexpr int kCollisionGridSize
absl::Status ValidateImportArguments(const resources::ArgumentParser &parser, absl::string_view command_name)
absl::Status PublishResolvedTextFileAtomically(const std::filesystem::path &target_path, absl::string_view content)
absl::Status RejectArtifactSetAliasesAfterPublication(const std::vector< PendingArtifactPublication > &artifacts)
absl::StatusOr< std::optional< std::filesystem::path > > ResolveReportArtifactPath(const resources::ArgumentParser &parser, const Rom *rom)
void CleanupPendingArtifactFiles(const std::vector< PendingArtifactPublication > &artifacts)
absl::StatusOr< std::optional< std::filesystem::path > > CreateArtifactRollbackCopy(const std::filesystem::path &target_path)
absl::StatusOr< std::filesystem::path > WriteExclusiveArtifactTemp(const std::filesystem::path &target_path, absl::string_view content)
absl::Status FinalizeWithReport(const std::optional< std::filesystem::path > &resolved_report_path, json report, const absl::Status &status)
json BuildBaseReport(absl::string_view command_name, bool dry_run)
absl::Status ReplaceArtifactFromTemp(const std::filesystem::path &temp_path, const std::filesystem::path &target_path)
absl::Status PublishExportArtifactsAtomically(const std::filesystem::path &out_path, absl::string_view out_content, const std::optional< std::filesystem::path > &report_path, absl::string_view report_content)
std::vector< int > RequiredCollisionRoomsForImportedWaterFillZones(const std::vector< zelda3::WaterFillZoneEntry > &zones)
absl::Status PublishArtifactSetAtomically(std::vector< PendingArtifactPublication > artifacts)
std::string StatusCodeName(absl::StatusCode code)
absl::Status SerializeAndPersistImport(Rom *rom, const resources::ArgumentParser &parser, json *report, Serializer &&serializer)
json BuildPreflightJson(const zelda3::OracleRomSafetyPreflightResult &preflight)
absl::Status ValidatePublishTarget(const std::filesystem::path &target_path)
absl::Status ValidateExportArguments(const resources::ArgumentParser &parser, absl::string_view command_name)
absl::StatusOr< std::vector< int > > ParseRoomSelection(const resources::ArgumentParser &parser)
absl::StatusOr< std::filesystem::path > ResolveStableArtifactPath(const std::filesystem::path &path)
absl::Status RejectArtifactRomAliases(absl::string_view option_name, const std::filesystem::path &artifact_path, const CommandInvocationContext &invocation_context)
absl::StatusOr< bool > PathsAlias(const std::filesystem::path &lhs, const std::filesystem::path &rhs)
bool ParseHexString(absl::string_view str, uint64_t *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::filesystem::path target_path
std::optional< std::filesystem::path > rollback_path
std::filesystem::path temp_path
bool operator==(const ResolvedExportArtifactPaths &) const =default
std::optional< std::filesystem::path > report_path
std::filesystem::path out_path
std::optional< std::filesystem::path > active_rom_path
std::optional< std::filesystem::path > source_rom_path
std::array< uint8_t, 64 *64 > tiles
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