yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_lifecycle_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <string>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/ascii.h"
11#include "absl/strings/match.h"
12#include "absl/strings/str_format.h"
18#include "core/hack_manifest.h"
19#include "core/project.h"
20#include "rom/rom.h"
21#include "util/rom_hash.h"
23
24namespace yaze::editor {
25
26namespace {
27
28std::string NormalizePath(std::string value) {
29 if (value.empty()) {
30 return value;
31 }
32 std::filesystem::path path(value);
33 path = path.lexically_normal();
34 path.make_preferred();
35 return path.string();
36}
37
38std::string Lowercase(std::string value) {
39 absl::AsciiStrToLower(&value);
40 return value;
41}
42
43bool PathsMatch(const std::string& lhs, const std::string& rhs) {
44 if (lhs.empty() || rhs.empty()) {
45 return false;
46 }
47 const std::string normalized_lhs = NormalizePath(lhs);
48 const std::string normalized_rhs = NormalizePath(rhs);
49 if (normalized_lhs == normalized_rhs) {
50 return true;
51 }
52 return Lowercase(std::filesystem::path(normalized_lhs).filename().string()) ==
53 Lowercase(std::filesystem::path(normalized_rhs).filename().string());
54}
55
57 std::string loaded_path;
60 std::string build_output_path;
61 bool matches_configured_project = false;
62 bool matches_editable_target = false;
63 bool matches_build_output = false;
64};
65
67 if (!project) {
68 return "";
69 }
70 if (project->hack_manifest.loaded() &&
71 !project->hack_manifest.build_pipeline().dev_rom.empty()) {
72 const std::string development_rom = NormalizePath(project->GetAbsolutePath(
74 std::error_code ec;
75 const auto canonical_rom = std::filesystem::canonical(development_rom, ec);
76 return ec ? development_rom : NormalizePath(canonical_rom.string());
77 }
78 return NormalizePath(project->GetAbsolutePath(project->rom_filename));
79}
80
82 if (!project) {
83 return "";
84 }
85 if (project->hack_manifest.loaded() &&
86 !project->hack_manifest.build_pipeline().patched_rom.empty()) {
87 return NormalizePath(project->GetAbsolutePath(
89 }
90 return NormalizePath(project->GetAbsolutePath(project->build_target));
91}
92
94 const std::string& loaded_path) {
95 RomTargetInfo info;
96 info.loaded_path = NormalizePath(loaded_path);
97 if (!project) {
98 return info;
99 }
100
104
111 return info;
112}
113
114std::string GetEditableTargetPath(const RomTargetInfo& info) {
115 if (!info.editable_target_path.empty()) {
116 return info.editable_target_path;
117 }
118 if (!info.configured_project_path.empty()) {
119 return info.configured_project_path;
120 }
121 return "(unknown)";
122}
123
124} // namespace
125
127 : rom_file_manager_(deps.rom_file_manager),
128 session_coordinator_(deps.session_coordinator),
129 toast_manager_(deps.toast_manager),
130 popup_manager_(deps.popup_manager),
131 project_(deps.project) {}
132
140
141// =============================================================================
142// ROM Hash & Write Policy
143// =============================================================================
144
146 if (!session || !session->rom.is_loaded() || !session->rom_baseline) {
148 current_rom_path_.clear();
150 return;
151 }
152 current_rom_identity_ = session->rom_baseline->hashes;
153 current_rom_path_ = session->rom_baseline->normalized_path;
154 current_rom_editing_source_path_ = session->rom_baseline->editing_source_path;
155}
156
158 const std::string& source_path) const {
159 // Save As must retain the editing-target exemption only for this session's
160 // own descendants. A hash-matched foreign ROM does not acquire it by saving.
161 if (current_rom_path_.empty() ||
162 NormalizePath(source_path) != current_rom_path_) {
163 return false;
164 }
165 const auto source =
167 return source.matches_editable_target && !source.matches_build_output;
168}
169
171 if (!project_ || !project_->project_opened()) {
172 return false;
173 }
174 const auto target_info = GetRomTargetInfo(project_, current_rom_path_);
175 if ((target_info.matches_editable_target ||
177 !target_info.matches_build_output) {
178 return false;
179 }
180 const std::string expected(
181 absl::StripAsciiWhitespace(project_->rom_metadata.expected_hash));
182 if (expected.empty() || current_rom_identity_.crc32.empty()) {
183 return false;
184 }
186}
187
189 if (!project_ || !project_->project_opened() || !rom || !rom->is_loaded()) {
190 return absl::OkStatus();
191 }
192
193 const auto target_info = GetRomTargetInfo(project_, rom->filename());
194 if (!target_info.matches_build_output) {
195 return absl::OkStatus();
196 }
197
200 return absl::OkStatus();
201 }
202
204 return absl::OkStatus();
205 }
206
207 const std::string editable_target = GetEditableTargetPath(target_info);
208 if (toast_manager_) {
210 absl::StrFormat("Project opened a build-output ROM; edit '%s' instead",
211 editable_target),
213 }
214 return absl::FailedPreconditionError(absl::StrFormat(
215 "Loaded ROM '%s' matches the project's build output. Edit '%s' instead.",
216 target_info.loaded_path.empty() ? rom->filename()
217 : target_info.loaded_path,
218 editable_target));
219}
220
222 const auto open_status = CheckRomOpenPolicy(rom);
223 if (!open_status.ok()) {
224 return open_status;
225 }
226 if (!project_ || !project_->project_opened() || !rom || !rom->is_loaded() ||
230 return absl::OkStatus();
231 }
232
233 const std::string development_rom = project_->GetAbsolutePath(
236 development_rom)) {
237 return absl::FailedPreconditionError(absl::StrFormat(
238 "Project source binding requires the manifest development ROM %s; "
239 "cannot swap to %s. Keep editing the development ROM with Save ROM, "
240 "then use Build Project. Save As copies do not change this source "
241 "binding. The copy and project binding were kept.",
242 development_rom, rom->filename()));
243 }
244 return absl::OkStatus();
245}
246
248 Rom* rom, const std::optional<std::string>& target_filename) {
249 if (!project_ || !project_->project_opened()) {
250 return absl::OkStatus();
251 }
252
253 const auto policy = project_->rom_metadata.write_policy;
254 const std::string target_path =
255 target_filename.has_value()
256 ? *target_filename
257 : (rom && !rom->filename().empty() ? rom->filename()
259 const std::string source_path =
260 rom && !rom->filename().empty() ? rom->filename() : current_rom_path_;
261 const auto target_info = GetRomTargetInfo(project_, target_path);
262 const auto source_info = GetRomTargetInfo(project_, source_path);
263 if (target_info.matches_build_output) {
264 if (policy == project::RomWritePolicy::kAllow) {
265 return absl::OkStatus();
266 }
267 if (toast_manager_) {
269 absl::StrFormat(
270 "ROM write blocked: '%s' is the project's build output",
271 target_info.loaded_path.empty() ? target_path
272 : target_info.loaded_path),
274 }
275 return absl::PermissionDeniedError(absl::StrFormat(
276 "ROM write blocked: '%s' is the project's build output. Edit '%s' "
277 "instead.",
278 target_info.loaded_path.empty() ? target_path : target_info.loaded_path,
279 GetEditableTargetPath(target_info)));
280 }
281
282 if ((source_info.matches_editable_target || HasEditableSource(source_path)) &&
283 !source_info.matches_build_output) {
284 return absl::OkStatus();
285 }
286
287 const std::string expected(
288 absl::StripAsciiWhitespace(project_->rom_metadata.expected_hash));
289
290 if (expected.empty() || current_rom_identity_.crc32.empty() ||
292 return absl::OkStatus();
293 }
294
295 if (policy == project::RomWritePolicy::kAllow) {
296 return absl::OkStatus();
297 }
298
299 if (policy == project::RomWritePolicy::kBlock) {
300 if (toast_manager_) {
302 "ROM write blocked: project expects a different ROM hash",
304 }
305 return absl::PermissionDeniedError(
306 "ROM write blocked by project write policy");
307 }
308
311 if (popup_manager_) {
313 }
314 if (toast_manager_) {
315 toast_manager_->Show("ROM hash mismatch: confirmation required to save",
317 }
318 return absl::CancelledError("ROM write confirmation required");
319 }
320
321 return absl::OkStatus();
322}
323
325 if (!rom || !rom->is_loaded()) {
326 return absl::InvalidArgumentError("ROM not loaded");
327 }
328
329 if (!project_ || !project_->project_opened()) {
330 return absl::OkStatus();
331 }
332
333 // An explicitly configured manifest is part of the project's save-safety
334 // contract. Never silently disable those guardrails when the file is missing
335 // or malformed.
336 if (!project_->hack_manifest_file.empty() &&
338 const std::string manifest_path =
340 if (toast_manager_) {
342 absl::StrFormat(
343 "Save blocked: configured hack manifest is unavailable: %s",
344 manifest_path),
346 }
347 return absl::FailedPreconditionError(absl::StrFormat(
348 "Configured hack manifest is missing or malformed: %s", manifest_path));
349 }
350
351 if (!project_->hack_manifest.loaded() ||
353 return absl::OkStatus();
354 }
355
359 options.validate_water_fill_table = true;
360 options.validate_custom_collision_maps = true;
361 options.max_collision_errors = 6;
362
363 const auto preflight = zelda3::RunOracleRomSafetyPreflight(rom, options);
364 if (preflight.ok()) {
365 return absl::OkStatus();
366 }
367
368 const auto& first = preflight.errors.front();
369 if (toast_manager_) {
370 toast_manager_->Show(absl::StrFormat("Oracle ROM safety [%s]: %s",
371 first.code, first.message.c_str()),
373 }
374 return preflight.ToStatus();
375}
376
377// =============================================================================
378// ROM Write Confirmation State Machine
379// =============================================================================
380
385
390
391// =============================================================================
392// Pot Item Save Confirmation
393// =============================================================================
394
396 int total_rooms) {
398 pending_pot_item_unloaded_rooms_ = unloaded_rooms;
399 pending_pot_item_total_rooms_ = total_rooms;
400}
401
404 PotItemSaveDecision decision) {
408
409 if (decision == PotItemSaveDecision::kCancel) {
410 return decision;
411 }
412
415 }
417 return decision;
418}
419
420// =============================================================================
421// Backup Management
422// =============================================================================
423
424std::vector<RomFileManager::BackupEntry> RomLifecycleManager::GetRomBackups(
425 Rom* rom) const {
426 if (!rom || !rom_file_manager_) {
427 return {};
428 }
429 return rom_file_manager_->ListBackups(rom->filename());
430}
431
433 if (!rom) {
434 return absl::FailedPreconditionError("No ROM loaded");
435 }
436 if (!rom_file_manager_) {
437 return absl::FailedPreconditionError("No ROM file manager");
438 }
440}
441
443 const std::string& folder,
444 int retention_count,
445 bool keep_daily,
446 int keep_daily_days) {
448 return;
454}
455
456} // 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:49
auto filename() const
Definition rom.h:207
bool is_loaded() const
Definition rom.h:187
bool HasProjectRegistry() const
bool loaded() const
Check if the manifest has been loaded.
const BuildPipeline & build_pipeline() const
void Show(const char *name)
void SetBackupRetentionCount(int count)
absl::Status PruneBackups(const std::string &rom_filename) const
std::vector< BackupEntry > ListBackups(const std::string &rom_filename) const
void SetBackupBeforeSave(bool enabled)
void SetBackupFolder(const std::string &folder)
void SetBackupKeepDaily(bool enabled)
std::vector< RomFileManager::BackupEntry > GetRomBackups(Rom *rom) const
void SetPotItemConfirmPending(int unloaded_rooms, int total_rooms)
Set pot-item confirmation pending (called by SaveRom when needed).
absl::Status CheckRomWritePolicy(Rom *rom, const std::optional< std::string > &target_filename=std::nullopt)
Enforce project write policy; may set pending_rom_write_confirm.
const project::YazeProject * project_
bool IsRomHashMismatch() const
Check whether the ROM hash mismatches the project's expected hash.
void ActivateRomIdentity(const RomSession *session)
bool HasEditableSource(const std::string &source_path) const
absl::Status CheckRomOpenPolicy(Rom *rom)
Validate that the loaded ROM is a safe project target to open/edit.
PotItemSaveDecision ResolvePotItemSaveConfirmation(PotItemSaveDecision decision)
util::RomIdentityHashes current_rom_identity_
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
Run Oracle-specific ROM safety preflight before saving.
void ApplyDefaultBackupPolicy(bool enabled, const std::string &folder, int retention_count, bool keep_daily, int keep_daily_days)
Apply default backup policy from user settings.
static bool PathsReferToSameBackingFile(const std::string &lhs, const std::string &rhs)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
constexpr const char * kRomWriteConfirm
std::string GetBuildOutputProjectRomPath(const project::YazeProject *project)
std::string GetEditableProjectRomPath(const project::YazeProject *project)
RomTargetInfo GetRomTargetInfo(const project::YazeProject *project, const std::string &loaded_path)
bool PathsMatch(const std::string &lhs, const std::string &rhs)
Editors are the view controllers for the application.
OracleRomSafetyPreflightResult RunOracleRomSafetyPreflight(Rom *rom, const OracleRomSafetyPreflightOptions &options)
Represents a single session, containing a ROM and its associated editors.
std::optional< RomBaselineIdentity > rom_baseline
std::string expected_hash
Definition project.h:224
RomWritePolicy write_policy
Definition project.h:225
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
bool project_opened() const
Definition project.h:505
core::HackManifest hack_manifest
Definition project.h:360
std::string hack_manifest_file
Definition project.h:329
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2028
RomHashMatch Match(std::string_view expected) const
Definition rom_hash.cc:90