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"
61using json = nlohmann::json;
65 std::unordered_set<int> tiles;
66 auto tiles_opt = parser.
GetString(
"tiles");
67 if (!tiles_opt.has_value()) {
71 for (absl::string_view token :
72 absl::StrSplit(tiles_opt.value(),
',', absl::SkipEmpty())) {
73 std::string t = std::string(absl::StripAsciiWhitespace(token));
75 if (!ParseHexString(t, &v)) {
76 return absl::InvalidArgumentError(
77 absl::StrFormat(
"Invalid tile value in --tiles: %s", t));
79 if (v < 0 || v > 0xFF) {
80 return absl::InvalidArgumentError(
81 absl::StrFormat(
"Tile value out of range (0x00-0xFF): %s", t));
90 std::string trimmed = std::string(absl::StripAsciiWhitespace(token));
92 if (!ParseHexString(trimmed, &room_id)) {
93 return absl::InvalidArgumentError(
94 absl::StrFormat(
"Invalid room ID: %s", trimmed));
97 return absl::OutOfRangeError(
98 absl::StrFormat(
"Room ID out of range: 0x%02X", room_id));
105 std::vector<int> room_ids;
106 bool any_explicit =
false;
108 if (
auto room_opt = parser.
GetString(
"room"); room_opt.has_value()) {
111 room_ids.push_back(room_id);
114 if (
auto rooms_opt = parser.
GetString(
"rooms"); rooms_opt.has_value()) {
116 for (absl::string_view token :
117 absl::StrSplit(rooms_opt.value(),
',', absl::SkipEmpty())) {
119 room_ids.push_back(room_id);
128 room_ids.push_back(room_id);
135 room_ids.push_back(room_id);
139 std::sort(room_ids.begin(), room_ids.end());
140 room_ids.erase(std::unique(room_ids.begin(), room_ids.end()), room_ids.end());
142 if (room_ids.empty()) {
143 return absl::InvalidArgumentError(
144 "No rooms selected (use --room, --rooms, or --all)");
150 std::ifstream in(path, std::ios::in | std::ios::binary);
152 return absl::NotFoundError(
153 absl::StrFormat(
"Cannot open file for reading: %s", path));
155 std::stringstream ss;
157 if (!in.good() && !in.eof()) {
158 return absl::InternalError(
159 absl::StrFormat(
"Failed while reading file: %s", path));
165 const std::filesystem::path& path) {
166 std::error_code absolute_ec;
167 std::filesystem::path absolute = std::filesystem::absolute(path, absolute_ec);
169 return absl::InvalidArgumentError(absl::StrFormat(
170 "Cannot normalize path %s: %s", path.string(), absolute_ec.message()));
172 absolute = absolute.lexically_normal();
176 std::error_code canonical_ec;
177 const std::filesystem::path canonical =
178 std::filesystem::weakly_canonical(absolute, canonical_ec);
180 return absl::FailedPreconditionError(
181 absl::StrFormat(
"Cannot safely resolve path %s: %s", absolute.string(),
182 canonical_ec.message()));
184 const std::filesystem::path resolved = canonical.lexically_normal();
185 const std::filesystem::path parent = resolved.parent_path();
186 std::error_code parent_ec;
187 const auto parent_status = std::filesystem::status(parent, parent_ec);
188 if (parent_ec || !std::filesystem::exists(parent_status) ||
189 !std::filesystem::is_directory(parent_status)) {
190 return absl::FailedPreconditionError(absl::StrFormat(
191 "Artifact parent directory must already exist: %s%s", parent.string(),
192 !parent_ec ?
"" : absl::StrFormat(
" (%s)", parent_ec.message())));
198 const std::filesystem::path& target_path) {
199 static std::atomic<uint64_t> sequence{0};
200 const uint64_t tick =
static_cast<uint64_t
>(
201 std::chrono::steady_clock::now().time_since_epoch().count());
202 const uint64_t
id = sequence.fetch_add(1, std::memory_order_relaxed);
204 std::filesystem::path temp_name = target_path.filename();
205 temp_name += absl::StrFormat(
".yaze-tmp-%016x-%016x", tick,
id);
206 const auto parent = target_path.parent_path().empty()
207 ? std::filesystem::path(
".")
208 : target_path.parent_path();
209 return parent / temp_name;
213 const std::filesystem::path& target_path, absl::string_view content) {
214 constexpr int kMaxCreateAttempts = 100;
216 for (
int attempt = 0; attempt < kMaxCreateAttempts; ++attempt) {
220 HANDLE file = CreateFileW(
221 temp_path.wstring().c_str(), GENERIC_WRITE, 0,
nullptr, CREATE_NEW,
222 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH,
nullptr);
223 if (file == INVALID_HANDLE_VALUE) {
224 const DWORD error = GetLastError();
225 if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
228 return absl::PermissionDeniedError(absl::StrFormat(
229 "Cannot create temporary artifact for %s: %s", target_path.string(),
230 std::error_code(
static_cast<int>(error), std::system_category())
234 size_t written_total = 0;
235 while (written_total < content.size()) {
236 const size_t remaining = content.size() - written_total;
237 const DWORD chunk =
static_cast<DWORD
>(
238 std::min<size_t>(remaining, std::numeric_limits<DWORD>::max()));
240 if (!WriteFile(file, content.data() + written_total, chunk, &written,
243 const DWORD error = GetLastError();
245 std::error_code cleanup_ec;
246 std::filesystem::remove(temp_path, cleanup_ec);
247 return absl::InternalError(absl::StrFormat(
248 "Failed while writing temporary artifact for %s: %s",
249 target_path.string(),
250 std::error_code(
static_cast<int>(error), std::system_category())
253 written_total += written;
256 if (!FlushFileBuffers(file)) {
257 const DWORD error = GetLastError();
259 std::error_code cleanup_ec;
260 std::filesystem::remove(temp_path, cleanup_ec);
261 return absl::InternalError(absl::StrFormat(
262 "Failed to flush temporary artifact for %s: %s", target_path.string(),
263 std::error_code(
static_cast<int>(error), std::system_category())
266 if (!CloseHandle(file)) {
267 const DWORD error = GetLastError();
268 std::error_code cleanup_ec;
269 std::filesystem::remove(temp_path, cleanup_ec);
270 return absl::InternalError(absl::StrFormat(
271 "Failed to close temporary artifact for %s: %s", target_path.string(),
272 std::error_code(
static_cast<int>(error), std::system_category())
277 open(temp_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
279 const int error = errno;
280 if (error == EEXIST) {
283 return absl::PermissionDeniedError(absl::StrFormat(
284 "Cannot create temporary artifact for %s: %s", target_path.string(),
285 std::error_code(error, std::generic_category()).message()));
288 size_t written_total = 0;
289 while (written_total < content.size()) {
290 const size_t remaining = content.size() - written_total;
291 const size_t chunk = std::min<size_t>(remaining, 1024 * 1024);
292 const ssize_t written = write(fd, content.data() + written_total, chunk);
293 if (written < 0 && errno == EINTR) {
297 const int error = written < 0 ? errno : EIO;
299 std::error_code cleanup_ec;
300 std::filesystem::remove(temp_path, cleanup_ec);
301 return absl::InternalError(absl::StrFormat(
302 "Failed while writing temporary artifact for %s: %s",
303 target_path.string(),
304 std::error_code(error, std::generic_category()).message()));
306 written_total +=
static_cast<size_t>(written);
309 if (fsync(fd) != 0) {
310 const int error = errno;
312 std::error_code cleanup_ec;
313 std::filesystem::remove(temp_path, cleanup_ec);
314 return absl::InternalError(absl::StrFormat(
315 "Failed to flush temporary artifact for %s: %s", target_path.string(),
316 std::error_code(error, std::generic_category()).message()));
318 if (close(fd) != 0) {
319 const int error = errno;
320 std::error_code cleanup_ec;
321 std::filesystem::remove(temp_path, cleanup_ec);
322 return absl::InternalError(absl::StrFormat(
323 "Failed to close temporary artifact for %s: %s", target_path.string(),
324 std::error_code(error, std::generic_category()).message()));
331 return absl::ResourceExhaustedError(
332 absl::StrFormat(
"Could not allocate a unique temporary artifact for %s",
333 target_path.string()));
337 const std::filesystem::path& target_path) {
338 std::error_code rename_ec;
339 std::filesystem::rename(temp_path, target_path, rename_ec);
342 if (MoveFileExW(temp_path.wstring().c_str(), target_path.wstring().c_str(),
343 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
346 rename_ec = std::error_code(
static_cast<int>(GetLastError()),
347 std::system_category());
352 return absl::InternalError(
353 absl::StrFormat(
"Failed to publish artifact %s: %s",
354 target_path.string(), rename_ec.message()));
356 return absl::OkStatus();
360 std::error_code status_ec;
361 const auto status = std::filesystem::symlink_status(target_path, status_ec);
362 if (status_ec && status_ec != std::errc::no_such_file_or_directory) {
363 return absl::FailedPreconditionError(
364 absl::StrFormat(
"Cannot inspect artifact target %s: %s",
365 target_path.string(), status_ec.message()));
367 if (!status_ec && std::filesystem::is_directory(status)) {
368 return absl::FailedPreconditionError(
369 absl::StrFormat(
"Artifact target is a directory, not a file: %s",
370 target_path.string()));
372 if (!status_ec && std::filesystem::exists(status) &&
373 !std::filesystem::is_regular_file(status) &&
374 !std::filesystem::is_symlink(status)) {
375 return absl::FailedPreconditionError(absl::StrFormat(
376 "Artifact target is not a replaceable file: %s", target_path.string()));
378 return absl::OkStatus();
382 const std::filesystem::path& target_path) {
383 std::error_code status_ec;
384 const auto status = std::filesystem::symlink_status(target_path, status_ec);
385 if (status_ec == std::errc::no_such_file_or_directory ||
386 (!status_ec && !std::filesystem::exists(status))) {
390 return absl::FailedPreconditionError(
391 absl::StrFormat(
"Cannot inspect existing artifact %s: %s",
392 target_path.string(), status_ec.message()));
395 constexpr int kMaxCreateAttempts = 100;
396 for (
int attempt = 0; attempt < kMaxCreateAttempts; ++attempt) {
397 const std::filesystem::path rollback_path =
399 std::error_code copy_ec;
400 if (std::filesystem::is_symlink(status)) {
401 std::filesystem::copy_symlink(target_path, rollback_path, copy_ec);
403 std::filesystem::copy_file(target_path, rollback_path,
404 std::filesystem::copy_options::none, copy_ec);
407 return rollback_path;
409 if (copy_ec == std::errc::file_exists) {
412 return absl::FailedPreconditionError(absl::StrFormat(
413 "Cannot preserve existing artifact %s before publication: %s",
414 target_path.string(), copy_ec.message()));
417 return absl::ResourceExhaustedError(
418 absl::StrFormat(
"Could not allocate a rollback copy for artifact %s",
419 target_path.string()));
427 bool published =
false;
428 bool preserve_rollback_copy =
false;
432 const std::vector<PendingArtifactPublication>& artifacts) {
433 for (
const auto& artifact : artifacts) {
434 std::error_code cleanup_ec;
435 if (!artifact.temp_path.empty()) {
436 std::filesystem::remove(artifact.temp_path, cleanup_ec);
438 if (artifact.rollback_path.has_value() &&
439 !artifact.preserve_rollback_copy) {
441 std::filesystem::remove(*artifact.rollback_path, cleanup_ec);
447 std::vector<PendingArtifactPublication>* artifacts,
448 const absl::Status& publication_failure) {
449 absl::Status rollback_failure = absl::OkStatus();
450 for (
auto it = artifacts->rbegin(); it != artifacts->rend(); ++it) {
451 if (!it->published) {
455 if (it->rollback_path.has_value()) {
456 const absl::Status restore_status =
458 if (!restore_status.ok() && rollback_failure.ok()) {
459 it->preserve_rollback_copy =
true;
460 rollback_failure = absl::InternalError(absl::StrFormat(
461 "%s; preserved the original artifact at %s",
462 restore_status.message(), it->rollback_path->string()));
463 }
else if (!restore_status.ok()) {
464 it->preserve_rollback_copy =
true;
465 }
else if (restore_status.ok()) {
466 it->rollback_path.reset();
469 std::error_code remove_ec;
470 if (!std::filesystem::remove(it->target_path, remove_ec) || remove_ec) {
471 if (rollback_failure.ok()) {
472 rollback_failure = absl::InternalError(absl::StrFormat(
473 "Could not remove newly published artifact %s during rollback: "
475 it->target_path.string(),
476 remove_ec ? remove_ec.message() :
"artifact was missing"));
480 it->published =
false;
483 if (!rollback_failure.ok()) {
484 return absl::DataLossError(absl::StrFormat(
485 "Artifact publication failed (%s) and rollback failed (%s)",
486 publication_failure.message(), rollback_failure.message()));
488 return publication_failure;
492 const std::vector<PendingArtifactPublication>& artifacts) {
493 for (
size_t lhs_index = 0; lhs_index < artifacts.size(); ++lhs_index) {
494 for (
size_t rhs_index = lhs_index + 1; rhs_index < artifacts.size();
496 std::error_code equivalent_ec;
497 if (std::filesystem::equivalent(artifacts[lhs_index].target_path,
498 artifacts[rhs_index].target_path,
500 return absl::InvalidArgumentError(absl::StrFormat(
501 "Export artifact paths alias each other on this filesystem: %s "
503 artifacts[lhs_index].target_path.string(),
504 artifacts[rhs_index].target_path.string()));
507 if (!equivalent_ec) {
514 std::error_code lhs_exists_ec;
515 std::error_code rhs_exists_ec;
516 const bool lhs_exists = std::filesystem::exists(
517 artifacts[lhs_index].target_path, lhs_exists_ec);
518 const bool rhs_exists = std::filesystem::exists(
519 artifacts[rhs_index].target_path, rhs_exists_ec);
520 if (lhs_exists_ec || rhs_exists_ec || (lhs_exists && rhs_exists)) {
521 return absl::FailedPreconditionError(absl::StrFormat(
522 "Could not safely distinguish export artifact paths %s and %s: "
524 artifacts[lhs_index].target_path.string(),
525 artifacts[rhs_index].target_path.string(),
526 equivalent_ec.message()));
530 return absl::OkStatus();
534 std::vector<PendingArtifactPublication> artifacts) {
535 for (
const auto& artifact : artifacts) {
536 const absl::Status target_status =
538 if (!target_status.ok()) {
540 return target_status;
544 for (
auto& artifact : artifacts) {
549 return temp_or.status();
551 artifact.temp_path = *temp_or;
554 for (
auto& artifact : artifacts) {
556 if (!rollback_or.ok()) {
558 return rollback_or.status();
560 artifact.rollback_path = std::move(*rollback_or);
563 for (
auto& artifact : artifacts) {
564 const absl::Status publish_status =
566 if (!publish_status.ok()) {
567 const absl::Status final_status =
572 artifact.temp_path.clear();
573 artifact.published =
true;
575 const absl::Status alias_status =
577 if (!alias_status.ok()) {
578 const absl::Status final_status =
586 return absl::OkStatus();
590 const std::filesystem::path& target_path, absl::string_view content) {
591 std::vector<PendingArtifactPublication> artifacts;
594 .content = std::string(content),
600 const std::filesystem::path& out_path, absl::string_view out_content,
601 const std::optional<std::filesystem::path>& report_path,
602 absl::string_view report_content) {
605 std::vector<PendingArtifactPublication> artifacts;
608 .content = std::string(out_content),
610 if (report_path.has_value()) {
613 .content = std::string(report_content),
620 absl::string_view command_name) {
622 const auto report_path = parser.
GetString(
"report");
623 const bool has_report = report_path.has_value();
624 if (has_report && report_path->empty()) {
625 return absl::InvalidArgumentError(
626 absl::StrFormat(
"%s: --report cannot be empty", command_name));
628 const bool sandbox_requested =
629 parser.
HasFlag(
"sandbox") || absl::GetFlag(FLAGS_sandbox);
630 if (has_report && !parser.
HasFlag(
"dry-run")) {
631 return absl::InvalidArgumentError(absl::StrFormat(
632 "%s: --report is supported only with --dry-run; write-mode imports "
633 "must rely on command output and ROM backup verification",
636 if (has_report && sandbox_requested) {
637 return absl::InvalidArgumentError(absl::StrFormat(
638 "%s: --report cannot be combined with --sandbox; run the reported "
639 "dry-run against the source ROM",
642 if (parser.
HasFlag(
"mock-rom") && sandbox_requested) {
643 return absl::InvalidArgumentError(absl::StrFormat(
644 "%s: --mock-rom and --sandbox are mutually exclusive", command_name));
646 return absl::OkStatus();
650 absl::string_view command_name) {
653 return absl::InvalidArgumentError(
654 absl::StrFormat(
"%s: --out cannot be empty", command_name));
656 if (
const auto report = parser.
GetString(
"report");
657 report.has_value() && report->empty()) {
658 return absl::InvalidArgumentError(
659 absl::StrFormat(
"%s: --report cannot be empty", command_name));
661 return absl::OkStatus();
665 const std::filesystem::path& path) {
669absl::StatusOr<bool>
PathsAlias(
const std::filesystem::path& lhs,
670 const std::filesystem::path& rhs) {
673 if (normalized_lhs == normalized_rhs) {
677 std::error_code equivalent_ec;
678 const bool equivalent = std::filesystem::equivalent(
679 normalized_lhs, normalized_rhs, equivalent_ec);
680 if (!equivalent_ec) {
687 std::error_code lhs_exists_ec;
688 std::error_code rhs_exists_ec;
689 const bool lhs_exists =
690 std::filesystem::exists(normalized_lhs, lhs_exists_ec);
691 const bool rhs_exists =
692 std::filesystem::exists(normalized_rhs, rhs_exists_ec);
693 if (lhs_exists_ec || rhs_exists_ec || (lhs_exists && rhs_exists)) {
694 return absl::FailedPreconditionError(absl::StrFormat(
695 "Could not safely compare paths %s and %s: %s", normalized_lhs.string(),
696 normalized_rhs.string(), equivalent_ec.message()));
702 absl::string_view option_name,
const std::filesystem::path& artifact_path,
706 const bool aliases_active_rom,
708 if (aliases_active_rom) {
709 return absl::InvalidArgumentError(absl::StrFormat(
710 "%s path aliases the active ROM; choose a separate artifact file: "
712 option_name, artifact_path.string()));
718 const bool aliases_source_rom,
720 if (aliases_source_rom) {
721 return absl::InvalidArgumentError(absl::StrFormat(
722 "%s path aliases the %s; choose a separate artifact file: %s",
726 artifact_path.string()));
730 return absl::OkStatus();
743 const auto out_path_value = parser.
GetString(
"out");
744 if (!out_path_value.has_value() || out_path_value->empty()) {
745 return absl::InvalidArgumentError(
"--out must name an artifact file");
755 const auto report_path_value = parser.
GetString(
"report");
756 if (!report_path_value.has_value()) {
757 return resolved_paths;
759 if (report_path_value->empty()) {
760 return absl::InvalidArgumentError(
"--report must name an artifact file");
770 if (artifacts_alias) {
771 return absl::InvalidArgumentError(absl::StrFormat(
772 "--out and --report paths alias each other; choose separate artifact "
777 resolved_paths.report_path = report_path;
778 return resolved_paths;
783 const auto report_path_value = parser.
GetString(
"report");
784 if (!report_path_value.has_value() || report_path_value->empty()) {
790 if (rom !=
nullptr && !rom->
filename().empty()) {
792 const bool aliases_active_rom,
794 if (aliases_active_rom) {
795 return absl::InvalidArgumentError(absl::StrFormat(
796 "--report path aliases the active ROM; choose a separate report "
798 report_path.string()));
807 case absl::StatusCode::kOk:
809 case absl::StatusCode::kCancelled:
811 case absl::StatusCode::kUnknown:
813 case absl::StatusCode::kInvalidArgument:
814 return "INVALID_ARGUMENT";
815 case absl::StatusCode::kDeadlineExceeded:
816 return "DEADLINE_EXCEEDED";
817 case absl::StatusCode::kNotFound:
819 case absl::StatusCode::kAlreadyExists:
820 return "ALREADY_EXISTS";
821 case absl::StatusCode::kPermissionDenied:
822 return "PERMISSION_DENIED";
823 case absl::StatusCode::kResourceExhausted:
824 return "RESOURCE_EXHAUSTED";
825 case absl::StatusCode::kFailedPrecondition:
826 return "FAILED_PRECONDITION";
827 case absl::StatusCode::kAborted:
829 case absl::StatusCode::kOutOfRange:
830 return "OUT_OF_RANGE";
831 case absl::StatusCode::kUnimplemented:
832 return "UNIMPLEMENTED";
833 case absl::StatusCode::kInternal:
835 case absl::StatusCode::kUnavailable:
836 return "UNAVAILABLE";
837 case absl::StatusCode::kDataLoss:
839 case absl::StatusCode::kUnauthenticated:
840 return "UNAUTHENTICATED";
847 {
"command", std::string(command_name)},
848 {
"status",
"success"},
849 {
"dry_run", dry_run},
850 {
"mode", dry_run ?
"dry-run" :
"write"},
855 const absl::Status& status) {
856 formatter.
AddField(
"status",
"error");
859 formatter.
AddField(
"message", std::string(status.message()));
864 const std::optional<std::filesystem::path>& resolved_report_path,
865 json report,
const absl::Status& status) {
867 report[
"status"] =
"error";
868 report[
"error"] =
json{
870 {
"message", std::string(status.message())},
874 absl::Status report_status = absl::OkStatus();
875 if (resolved_report_path.has_value()) {
877 report.dump(2) +
"\n");
879 if (!report_status.ok()) {
881 return report_status;
883 return absl::InternalError(
884 absl::StrFormat(
"Command failed (%s) and report write failed (%s)",
885 status.message(), report_status.message()));
893 const absl::Status& status) {
901 out[
"ok"] = preflight.
ok();
902 json errors = json::array();
903 for (
const auto& err : preflight.
errors) {
905 e[
"code"] = err.code;
906 e[
"message"] = err.message;
908 if (err.room_id >= 0) {
909 e[
"room_id"] = absl::StrFormat(
"0x%02X", err.room_id);
911 errors.push_back(std::move(e));
913 out[
"errors"] = std::move(errors);
917template <
typename Serializer>
920 json* report, Serializer&& serializer) {
923 const absl::Status write_status = std::forward<Serializer>(serializer)();
924 if (!write_status.ok()) {
925 (*report)[
"write_error"] = std::string(write_status.message());
928 (*report)[
"write_status"] =
"success";
933 if (parser.
HasFlag(
"mock-rom")) {
934 (*report)[
"save_status"] =
"mock-rom-skipped";
936 return absl::OkStatus();
941 const absl::Status save_status = rom->
SaveToFile(save_settings);
942 if (!save_status.ok()) {
943 (*report)[
"save_error"] = std::string(save_status.message());
947 (*report)[
"save_status"] =
"saved";
949 return absl::OkStatus();
953 const std::vector<zelda3::WaterFillZoneEntry>& zones) {
954 constexpr std::array<int, 2> kD4RoomIdsRequiringCollision = {0x25, 0x27};
956 std::unordered_set<int> imported_rooms;
957 imported_rooms.reserve(zones.size());
958 for (
const auto& zone : zones) {
959 imported_rooms.insert(zone.room_id);
962 std::vector<int> required_rooms;
963 for (
int room_id : kD4RoomIdsRequiringCollision) {
964 if (imported_rooms.contains(room_id)) {
965 required_rooms.push_back(room_id);
968 return required_rooms;
976 auto room_id_str = parser.
GetString(
"room").value();
979 if (!ParseHexString(room_id_str, &room_id)) {
980 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
985 const bool list_all = parser.
HasFlag(
"all");
986 const bool list_nonzero =
987 parser.
HasFlag(
"nonzero") || (!list_all && filter_tiles.empty());
990 formatter.
AddField(
"room_id", room_id);
994 !filter_tiles.empty()
996 : (list_all ?
"all" : (list_nonzero ?
"nonzero" :
"all")));
1000 formatter.
AddField(
"status",
"error");
1001 formatter.
AddField(
"error", map_or.status().ToString());
1003 return map_or.status();
1006 const auto& map = map_or.value();
1007 formatter.
AddField(
"has_data", map.has_data);
1009 int nonzero_count = 0;
1010 for (uint8_t tile : map.tiles) {
1015 formatter.
AddField(
"nonzero_tiles", nonzero_count);
1018 int match_count = 0;
1020 for (
int y = 0; y < 64; ++y) {
1021 for (
int x = 0; x < 64; ++x) {
1022 uint8_t tile = map.tiles[
static_cast<size_t>(y * 64 + x)];
1024 if (!filter_tiles.empty()) {
1025 if (filter_tiles.find(
static_cast<int>(tile)) == filter_tiles.end()) {
1028 }
else if (list_nonzero) {
1032 }
else if (!list_all) {
1050 formatter.
AddField(
"match_count", match_count);
1051 formatter.
AddField(
"status",
"success");
1053 return absl::OkStatus();
1060 if (rom !=
nullptr && !rom->
filename().empty()) {
1071 auto artifact_paths_or =
1072 ResolveExportArtifactPaths(parser, invocation_context);
1073 if (!artifact_paths_or.ok()) {
1074 AddStructuredError(formatter, artifact_paths_or.status());
1075 return artifact_paths_or.status();
1077 const ResolvedExportArtifactPaths artifact_paths = *artifact_paths_or;
1080 std::string out_path;
1081 std::string exported_json;
1082 int requested_rooms = 0;
1083 int exported_room_count = 0;
1084 const absl::Status status = [&]() -> absl::Status {
1086 out_path = parser.
GetString(
"out").value();
1087 requested_rooms =
static_cast<int>(room_ids.size());
1088 report[
"out_path"] = out_path;
1089 report[
"requested_rooms"] = requested_rooms;
1091 std::vector<zelda3::CustomCollisionRoomEntry> export_rooms;
1092 export_rooms.reserve(room_ids.size());
1094 for (
int room_id : room_ids) {
1096 if (!map.has_data) {
1102 for (
int offset = 0; offset < kCollisionGridSize * kCollisionGridSize;
1104 const uint8_t tile = map.tiles[
static_cast<size_t>(offset)];
1109 static_cast<uint16_t
>(offset), tile});
1111 if (!entry.
tiles.empty()) {
1112 export_rooms.push_back(std::move(entry));
1119 exported_room_count =
static_cast<int>(export_rooms.size());
1120 report[
"exported_rooms"] = exported_room_count;
1122 return absl::OkStatus();
1127 auto final_artifact_paths_or =
1128 ResolveExportArtifactPaths(parser, invocation_context);
1129 absl::Status final_path_status = final_artifact_paths_or.status();
1130 if (final_artifact_paths_or.ok() &&
1131 *final_artifact_paths_or != artifact_paths) {
1132 final_path_status = absl::FailedPreconditionError(
1133 "Export artifact path identity changed during the command; no "
1134 "artifact was published");
1137 absl::Status final_status;
1142 final_status = final_path_status.ok()
1143 ? FinalizeExportWithReport(artifact_paths,
1144 std::move(report), status)
1146 }
else if (!final_path_status.ok()) {
1147 final_status = final_path_status;
1149 final_status = PublishExportArtifactsAtomically(
1150 artifact_paths.out_path, exported_json, artifact_paths.report_path,
1151 report.dump(2) +
"\n");
1153 if (!final_status.ok()) {
1154 AddStructuredError(formatter, final_status);
1155 return final_status;
1159 formatter.
AddField(
"out_path", out_path);
1160 formatter.
AddField(
"requested_rooms", requested_rooms);
1161 formatter.
AddField(
"exported_rooms", exported_room_count);
1162 formatter.
AddField(
"status",
"success");
1164 return absl::OkStatus();
1170 auto report_path_or = ResolveReportArtifactPath(parser, rom);
1171 if (!report_path_or.ok()) {
1172 AddStructuredError(formatter, report_path_or.status());
1173 return report_path_or.status();
1175 const std::optional<std::filesystem::path> report_path = *report_path_or;
1177 const bool dry_run = parser.
HasFlag(
"dry-run");
1178 json report = BuildBaseReport(
GetName(), dry_run);
1179 const absl::Status status = [&]() -> absl::Status {
1180 const std::string in_path = parser.
GetString(
"in").value();
1181 const bool replace_all = parser.
HasFlag(
"replace-all");
1182 const bool force = parser.
HasFlag(
"force");
1183 report[
"in_path"] = in_path;
1184 report[
"replace_all"] = replace_all;
1185 report[
"force"] = force;
1188 return absl::FailedPreconditionError(
1189 "Custom collision write support not present in this ROM");
1197 const auto preflight =
1199 report[
"preflight"] = BuildPreflightJson(preflight);
1200 if (!preflight.ok()) {
1201 return preflight.ToStatus();
1204 if (replace_all && !dry_run && !force) {
1205 return absl::FailedPreconditionError(
1206 "--replace-all requires --force (run with --dry-run first)");
1211 auto imported_rooms,
1213 report[
"imported_room_entries"] =
static_cast<int>(imported_rooms.size());
1217 std::vector<zelda3::Room> rooms;
1220 rooms.emplace_back(room_id, rom,
nullptr);
1223 int populated_rooms = 0;
1224 int cleared_rooms = 0;
1225 std::unordered_set<int> touched_rooms;
1226 for (
const auto& imported : imported_rooms) {
1227 touched_rooms.insert(imported.room_id);
1228 auto& room = rooms[imported.room_id];
1229 room.custom_collision().tiles.fill(0);
1230 room.custom_collision().has_data =
false;
1231 room.MarkCustomCollisionDirty();
1233 bool has_nonzero =
false;
1234 for (
const auto& tile : imported.tiles) {
1235 const int offset =
static_cast<int>(tile.offset);
1236 if (offset < 0 || offset >= kCollisionGridSize * kCollisionGridSize) {
1239 if (tile.value == 0) {
1242 room.SetCollisionTile(offset % kCollisionGridSize,
1243 offset / kCollisionGridSize, tile.value);
1254 int replace_all_clears = 0;
1257 if (touched_rooms.contains(room_id)) {
1260 auto& room = rooms[room_id];
1261 room.custom_collision().tiles.fill(0);
1262 room.custom_collision().has_data =
false;
1263 room.MarkCustomCollisionDirty();
1265 ++replace_all_clears;
1268 report[
"replace_all_clears"] = replace_all_clears;
1271 RETURN_IF_ERROR(SerializeAndPersistImport(rom, parser, &report, [&]() {
1276 report[
"populated_rooms"] = populated_rooms;
1277 report[
"cleared_rooms"] = cleared_rooms;
1280 formatter.
AddField(
"in_path", in_path);
1281 formatter.
AddField(
"replace_all", replace_all);
1282 formatter.
AddField(
"force", force);
1283 formatter.
AddField(
"mode", dry_run ?
"dry-run" :
"write");
1284 formatter.
AddField(
"imported_room_entries",
1285 static_cast<int>(imported_rooms.size()));
1286 formatter.
AddField(
"populated_rooms", populated_rooms);
1287 formatter.
AddField(
"cleared_rooms", cleared_rooms);
1290 report.value(
"write_status", std::string(
"unknown")));
1292 report.value(
"save_status", std::string(
"unknown")));
1294 formatter.
AddField(
"status",
"success");
1296 return absl::OkStatus();
1299 return FinalizeWithReport(report_path, std::move(report), status);
1306 if (rom !=
nullptr && !rom->
filename().empty()) {
1317 auto artifact_paths_or =
1318 ResolveExportArtifactPaths(parser, invocation_context);
1319 if (!artifact_paths_or.ok()) {
1320 AddStructuredError(formatter, artifact_paths_or.status());
1321 return artifact_paths_or.status();
1323 const ResolvedExportArtifactPaths artifact_paths = *artifact_paths_or;
1326 std::string out_path;
1327 std::string exported_json;
1328 int requested_rooms = 0;
1329 int exported_zone_count = 0;
1330 const absl::Status status = [&]() -> absl::Status {
1331 out_path = parser.
GetString(
"out").value();
1333 requested_rooms =
static_cast<int>(room_ids.size());
1334 report[
"out_path"] = out_path;
1335 report[
"requested_rooms"] = requested_rooms;
1338 return absl::FailedPreconditionError(
1339 "WaterFill reserved region missing in this ROM");
1343 std::unordered_set<int> room_filter(room_ids.begin(), room_ids.end());
1344 std::vector<zelda3::WaterFillZoneEntry> filtered;
1345 filtered.reserve(zones.size());
1346 for (
const auto& zone : zones) {
1347 if (!room_filter.contains(zone.room_id)) {
1350 filtered.push_back(zone);
1355 exported_zone_count =
static_cast<int>(filtered.size());
1356 report[
"exported_zones"] = exported_zone_count;
1358 return absl::OkStatus();
1363 auto final_artifact_paths_or =
1364 ResolveExportArtifactPaths(parser, invocation_context);
1365 absl::Status final_path_status = final_artifact_paths_or.status();
1366 if (final_artifact_paths_or.ok() &&
1367 *final_artifact_paths_or != artifact_paths) {
1368 final_path_status = absl::FailedPreconditionError(
1369 "Export artifact path identity changed during the command; no "
1370 "artifact was published");
1373 absl::Status final_status;
1378 final_status = final_path_status.ok()
1379 ? FinalizeExportWithReport(artifact_paths,
1380 std::move(report), status)
1382 }
else if (!final_path_status.ok()) {
1383 final_status = final_path_status;
1385 final_status = PublishExportArtifactsAtomically(
1386 artifact_paths.out_path, exported_json, artifact_paths.report_path,
1387 report.dump(2) +
"\n");
1389 if (!final_status.ok()) {
1390 AddStructuredError(formatter, final_status);
1391 return final_status;
1395 formatter.
AddField(
"out_path", out_path);
1396 formatter.
AddField(
"requested_rooms", requested_rooms);
1397 formatter.
AddField(
"exported_zones", exported_zone_count);
1398 formatter.
AddField(
"status",
"success");
1400 return absl::OkStatus();
1406 auto report_path_or = ResolveReportArtifactPath(parser, rom);
1407 if (!report_path_or.ok()) {
1408 AddStructuredError(formatter, report_path_or.status());
1409 return report_path_or.status();
1411 const std::optional<std::filesystem::path> report_path = *report_path_or;
1413 const bool dry_run = parser.
HasFlag(
"dry-run");
1414 const bool strict_masks = parser.
HasFlag(
"strict-masks");
1415 json report = BuildBaseReport(
GetName(), dry_run);
1416 const absl::Status status = [&]() -> absl::Status {
1417 const std::string in_path = parser.
GetString(
"in").value();
1418 report[
"in_path"] = in_path;
1419 report[
"strict_masks"] = strict_masks;
1422 return absl::FailedPreconditionError(
1423 "WaterFill reserved region missing in this ROM");
1429 const auto required_collision_rooms =
1430 RequiredCollisionRoomsForImportedWaterFillZones(zones);
1431 if (!required_collision_rooms.empty()) {
1432 json required_rooms_json = json::array();
1433 for (
int room_id : required_collision_rooms) {
1434 required_rooms_json.push_back(absl::StrFormat(
"0x%02X", room_id));
1436 report[
"required_collision_rooms"] = std::move(required_rooms_json);
1445 required_collision_rooms;
1446 const auto preflight =
1448 report[
"preflight"] = BuildPreflightJson(preflight);
1449 if (!preflight.ok()) {
1450 return preflight.ToStatus();
1453 auto original_zones = zones;
1456 int normalized_masks = 0;
1457 std::unordered_map<int, uint8_t> before_masks;
1458 before_masks.reserve(original_zones.size());
1459 for (
const auto& z : original_zones) {
1460 before_masks[z.room_id] = z.sram_bit_mask;
1462 for (
const auto& z : zones) {
1463 auto it = before_masks.find(z.room_id);
1464 if (it == before_masks.end() || it->second != z.sram_bit_mask) {
1469 report[
"zone_count"] =
static_cast<int>(zones.size());
1470 report[
"normalized_masks"] = normalized_masks;
1472 if (strict_masks && normalized_masks > 0) {
1473 return absl::FailedPreconditionError(absl::StrFormat(
1474 "WaterFill masks require normalization (%d changed); rerun without "
1475 "--strict-masks to apply normalized masks",
1480 RETURN_IF_ERROR(SerializeAndPersistImport(rom, parser, &report, [&]() {
1486 formatter.
AddField(
"in_path", in_path);
1487 formatter.
AddField(
"mode", dry_run ?
"dry-run" :
"write");
1488 formatter.
AddField(
"strict_masks", strict_masks);
1489 formatter.
AddField(
"zone_count",
static_cast<int>(zones.size()));
1490 formatter.
AddField(
"normalized_masks", normalized_masks);
1493 report.value(
"write_status", std::string(
"unknown")));
1495 report.value(
"save_status", std::string(
"unknown")));
1497 formatter.
AddField(
"status",
"success");
1499 return absl::OkStatus();
1502 return FinalizeWithReport(report_path, std::move(report), status);
1507 return ValidateImportArguments(parser,
GetName());
1512 return ValidateExportArguments(parser,
GetName());
1517 return ValidateExportArguments(parser,
GetName());
1522 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::StatusOr< bool > PathsAlias(const std::filesystem::path &lhs, const std::filesystem::path &rhs)
absl::StatusOr< std::string > ReadTextFile(const std::string &path)
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 > ResolveStableArtifactPath(const std::filesystem::path &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)
absl::Status RejectArtifactRomAliases(absl::string_view option_name, const std::filesystem::path &artifact_path, const resources::CommandInvocationContext &invocation_context)
absl::StatusOr< std::filesystem::path > NormalizedAbsolutePath(const std::filesystem::path &path)
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)
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::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::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