yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
collision_source_pairing.cc
Go to the documentation of this file.
2
3#include <fstream>
4#include <iterator>
5#include <optional>
6#include <string>
7#include <system_error>
8#include <unordered_set>
9#include <vector>
10
11#include "absl/strings/str_format.h"
13#include "util/macro.h"
15
16#if defined(YAZE_WITH_JSON)
17#include "nlohmann/json.hpp"
18#endif
19
20namespace yaze::editor {
21namespace {
22
23namespace fs = std::filesystem;
24
25absl::StatusOr<std::optional<std::string>> ReadOptionalFile(
26 const fs::path& path) {
27 std::error_code ec;
28 if (!fs::exists(path, ec)) {
29 if (ec) {
30 return absl::FailedPreconditionError(absl::StrFormat(
31 "Cannot inspect %s: %s", path.string(), ec.message()));
32 }
33 return std::optional<std::string>();
34 }
35 std::ifstream in(path, std::ios::binary);
36 if (!in) {
37 return absl::FailedPreconditionError(
38 absl::StrFormat("Cannot read %s", path.string()));
39 }
40 return std::optional<std::string>(
41 std::string(std::istreambuf_iterator<char>(in), {}));
42}
43
44// The collision source that the ROM currently on disk would produce.
45absl::StatusOr<std::string> CollisionSourceOfRomBytes(
46 const std::string& rom_bytes) {
47 Rom disk_rom;
48 Rom::LoadOptions options; // Load as yaze does, without resource labels.
49 options.load_resource_labels = false;
51 std::vector<uint8_t>(rom_bytes.begin(), rom_bytes.end()), options));
53}
54
55// Ignore presentation differences only. The import parser accepts aliases,
56// skips some invalid entries, and coalesces duplicate offsets, so it is not a
57// suitable source-binding guard. Comparing parsed documents retains every
58// array entry (including explicit floor zero), field, and numeric type.
59absl::StatusOr<bool> CollisionSourcesMatch(const std::string& source,
60 const std::string& generated,
61 const fs::path& source_path) {
62#if !defined(YAZE_WITH_JSON)
63 return absl::UnimplementedError("JSON support not enabled");
64#else
65 using json = nlohmann::json;
66 if (source.find('\0') != std::string::npos) {
67 return absl::FailedPreconditionError(
68 absl::StrFormat("Invalid collision source %s: embedded NUL byte",
69 source_path.string()));
70 }
71 bool duplicate_key = false;
72 std::vector<std::unordered_set<std::string>> object_keys;
73 try {
74 auto parsed =
75 json::parse(source, [&](int, json::parse_event_t event, json& value) {
76 if (event == json::parse_event_t::object_start) {
77 object_keys.emplace_back();
78 } else if (event == json::parse_event_t::object_end) {
79 object_keys.pop_back();
80 } else if (event == json::parse_event_t::key) {
81 if (!object_keys.back().insert(value.get<std::string>()).second) {
82 duplicate_key = true;
83 }
84 }
85 return true;
86 });
87 if (duplicate_key) {
88 return absl::FailedPreconditionError(absl::StrFormat(
89 "Invalid collision source %s: duplicate JSON object key",
90 source_path.string()));
91 }
92 // JSON equality coerces 1.0 to integer 1. Canonical serialization preserves
93 // that distinction, while normalizing whitespace and object-key ordering.
94 return parsed.dump() == json::parse(generated).dump();
95 } catch (const json::exception& error) {
96 return absl::FailedPreconditionError(absl::StrFormat(
97 "Invalid collision source %s: %s", source_path.string(), error.what()));
98 }
99#endif
100}
101
102} // namespace
103
104absl::Status ValidateRomCollisionSourceBinding(Rom* replacement_rom,
105 const fs::path& json_path) {
106 if (replacement_rom == nullptr || !replacement_rom->is_loaded()) {
107 return absl::FailedPreconditionError("No replacement ROM loaded");
108 }
109 ASSIGN_OR_RETURN(const auto source, ReadOptionalFile(json_path));
110 if (!source.has_value()) {
111 return absl::FailedPreconditionError(absl::StrFormat(
112 "Configured collision source %s is missing; restore it before "
113 "swapping the project ROM. The project binding was not changed.",
114 json_path.string()));
115 }
116 ASSIGN_OR_RETURN(const std::string replacement_source,
119 const bool source_matches,
120 CollisionSourcesMatch(*source, replacement_source, json_path));
121 if (!source_matches) {
122 return absl::FailedPreconditionError(absl::StrFormat(
123 "Replacement ROM %s does not match configured collision source %s. "
124 "Swap ROM does not publish collision changes. Select a ROM matching "
125 "this source. For collision edits, use Save ROM on the bound project "
126 "and then Build Project. The saved copy and project binding were kept.",
127 replacement_rom->filename(), json_path.string()));
128 }
129 return absl::OkStatus();
130}
131
132absl::Status SaveRomWithCollisionSource(Rom* rom, const fs::path& json_path) {
133 if (rom == nullptr || !rom->is_loaded() || rom->filename().empty()) {
134 return absl::FailedPreconditionError("No ROM file to save");
135 }
137 const fs::path rom_path(rom->filename());
138
139 ASSIGN_OR_RETURN(const std::string new_json,
141 ASSIGN_OR_RETURN(const auto disk_json, ReadOptionalFile(json_path));
142
143 // Preserve the user's formatting when collision data did not change.
144 if (disk_json.has_value()) {
145 ASSIGN_OR_RETURN(const bool source_matches,
146 CollisionSourcesMatch(*disk_json, new_json, json_path));
147 if (source_matches) {
148 Rom::SaveSettings settings;
149 return rom->SaveToFile(settings);
150 }
151 }
152
154 .subject = "ROM and custom collision source",
155 .published_file = "custom collision source"};
157 {rom_path, json_path}, labels));
158
159 // Read both files under the lock so no other writer can slip in between.
160 ASSIGN_OR_RETURN(const auto disk_rom, ReadOptionalFile(rom_path));
161 if (!disk_rom.has_value()) {
162 return absl::FailedPreconditionError(absl::StrFormat(
163 "Cannot pair a custom collision source with %s: the ROM file does "
164 "not exist yet",
165 rom_path.string()));
166 }
167 ASSIGN_OR_RETURN(const auto locked_json, ReadOptionalFile(json_path));
168 if (locked_json != disk_json) {
169 return absl::AbortedError(absl::StrFormat(
170 "%s changed while saving; nothing was written. Save again.",
171 json_path.string()));
172 }
173 if (locked_json.has_value()) {
174 ASSIGN_OR_RETURN(const std::string disk_source,
175 CollisionSourceOfRomBytes(*disk_rom));
177 const bool source_matches,
178 CollisionSourcesMatch(*locked_json, disk_source, json_path));
179 if (!source_matches) {
180 return absl::FailedPreconditionError(absl::StrFormat(
181 "%s does not match the custom collision in %s. It was changed "
182 "outside yaze, or the two were already out of step. Nothing was "
183 "written; reconcile them first (Oracle: "
184 "Scripts/Generate/validate_custom_collision_source.py).",
185 json_path.string(), rom_path.string()));
186 }
187 }
188
189 const std::vector<uint8_t>& bytes = rom->vector();
190 std::vector<core::SourceArtifactUpdate> updates;
191 updates.push_back(core::SourceArtifactUpdate{
192 .target = rom_path,
193 .before = *disk_rom,
194 .after = std::string(bytes.begin(), bytes.end())});
195 updates.push_back(core::SourceArtifactUpdate{
196 .target = json_path, .before = locked_json, .after = new_json});
198 *lock, std::move(updates), core::ComputeSourceArtifactSha256(*disk_rom)));
199 rom->set_dirty(false);
200 return absl::OkStatus();
201}
202
203} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
absl::Status CheckLayoutWritable() const
Definition rom.cc:432
auto filename() const
Definition rom.h:219
void set_dirty(bool dirty)
Definition rom.h:201
const auto & vector() const
Definition rom.h:217
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:893
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:781
bool is_loaded() const
Definition rom.h:199
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
absl::StatusOr< std::string > CollisionSourceOfRomBytes(const std::string &rom_bytes)
absl::StatusOr< bool > CollisionSourcesMatch(const std::string &source, const std::string &generated, const fs::path &source_path)
absl::StatusOr< std::optional< std::string > > ReadOptionalFile(const fs::path &path)
Editors are the view controllers for the application.
absl::Status SaveRomWithCollisionSource(Rom *rom, const fs::path &json_path)
absl::Status ValidateRomCollisionSourceBinding(Rom *replacement_rom, const fs::path &json_path)
absl::StatusOr< std::string > DumpCustomCollisionSourceFromRom(Rom *rom)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:69