yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
editor_manager_project_io.cc
Go to the documentation of this file.
1// ROM/project I/O and its callback wiring. Method bodies are kept with the
2// qualified editor implementation; editor_manager.cc owns the remaining shell.
4
5#include <algorithm>
6#include <cstdint>
7#include <exception>
8#include <filesystem>
9#include <fstream>
10#include <initializer_list>
11#include <iterator>
12#include <map>
13#include <optional>
14#include <random>
15#include <string>
16#include <utility>
17#include <vector>
18
19#ifdef __APPLE__
20#include <TargetConditionals.h>
21#endif
22
23#include "absl/status/status.h"
24#include "absl/strings/match.h"
25#include "absl/strings/str_format.h"
26
28#include "app/editor/editor.h"
43#include "app/platform/timing.h"
44#include "core/features.h"
46#include "core/project.h"
47#include "core/rom_settings.h"
50#include "rom/rom.h"
51#include "rom/rom_diff.h"
52#include "rom/transaction.h"
53#include "util/file_util.h"
54#include "util/json.h"
55#include "util/log.h"
56#include "util/macro.h"
57#include "util/platform_paths.h"
60#include "zelda3/game_data.h"
64
65#ifdef YAZE_ENABLE_TESTING
67#endif
68
69namespace yaze::editor {
70namespace {
71// Declared source files only. Build outputs and the next build request are
72// consumers of the saved state, not inputs that a build may refresh in place.
73std::map<std::string, std::string> ProjectSaveSources(
74 const project::YazeProject& project) {
75 std::map<std::string, std::string> sources;
76 const auto add = [&](const std::string& role, const std::string& path) {
77 if (!path.empty())
78 sources.emplace(role, project.GetAbsolutePath(path));
79 };
80 for (size_t i = 0; i < project.additional_roms.size(); ++i)
81 add("dependency.additional_roms." + std::to_string(i),
82 project.additional_roms[i]);
83 add("sidecar", project.lyra_terrain_file);
84 add("dependency.labels_filename", project.labels_filename);
85 add("dependency.symbols_filename", project.symbols_filename);
86 add("dependency.cutscene_shots", project.cutscene_shots);
87 add("dependency.overworld_graphics_preview",
89 add("dependency.dungeon_tile_patterns", project.dungeon_tile_patterns);
90 add("dependency.custom_collision_json", project.custom_collision_json);
91 add("dependency.hack_manifest_file", project.hack_manifest_file);
92 add("dependency.sprite_catalog_file", project.sprite_catalog_file);
93 add("dependency.terrain_catalog_file", project.terrain_catalog_file);
94 add("dependency.palette_catalog_file", project.palette_catalog_file);
95 // Planning is a live project reference, not a build input: its own CAS
96 // save owns it, and agent status edits must not invalidate a native Save.
97 add("dependency.atlas_symbols", project.atlas.symbols);
98 for (const auto& [id, files] : project.custom_object_files) {
99 for (size_t i = 0; i < files.size(); ++i) {
100 const auto path = std::filesystem::path(files[i]);
101 const auto resolved = path.is_absolute()
102 ? path
103 : std::filesystem::path(project.GetAbsolutePath(
104 project.custom_objects_folder)) /
105 path;
106 add("dependency.object_" + std::to_string(id) + "_" + std::to_string(i),
107 resolved.string());
108 }
109 }
110 for (size_t i = 0; i < project.atlas.annotations.size(); ++i)
111 add("dependency.atlas_" + std::to_string(i), project.atlas.annotations[i]);
112 for (size_t i = 0; i < project.sprite_assets.size(); ++i) {
113 const auto prefix = "dependency.sprite_" + std::to_string(i);
114 const auto& asset = project.sprite_assets[i];
115 add(prefix + "_zsm", asset.zsm_path);
116 if (!asset.source_path.empty())
117 add(prefix + "_source", (std::filesystem::path(project.GetAbsolutePath(
118 project.sprite_source_root)) /
119 asset.source_path)
120 .string());
121 for (size_t slot = 0; slot < asset.graphics_sources.size(); ++slot) {
122 const auto& source = asset.graphics_sources[slot];
123 if (source.kind == "bin" || source.kind == "raw_rom")
124 add(prefix + "_slot_" + std::to_string(slot), source.path);
125 }
126 }
127 return sources;
128}
129
131 public:
135 delete;
136
138 if (!committed_) {
139 for (auto it = editors_.rbegin(); it != editors_.rend(); ++it) {
140 (*it)->RollbackSaveTransaction();
141 }
142 }
143 }
144
145 absl::Status Begin(Editor* editor) {
146 if (editor == nullptr) {
147 return absl::OkStatus();
148 }
150 editors_.push_back(editor);
151 return absl::OkStatus();
152 }
153
154 void Commit() {
155 for (Editor* editor : editors_) {
156 editor->CommitSaveTransaction();
157 }
158 committed_ = true;
159 }
160
161 private:
162 std::vector<Editor*> editors_;
163 bool committed_ = false;
164};
165
167 std::initializer_list<const char*> keys) {
168 for (const char* key : keys) {
169 if (overrides.addresses.find(key) != overrides.addresses.end()) {
170 return true;
171 }
172 }
173 return false;
174}
175
177 return !project.custom_objects_folder.empty() ||
178 !project.custom_object_files.empty();
179}
180
181constexpr int kTrackCustomObjectId = 0x31;
182
184 std::string* warning) {
185 if (project == nullptr) {
186 return false;
187 }
188 if (project->custom_objects_folder.empty()) {
189 return false;
190 }
191 if (project->custom_object_files.find(kTrackCustomObjectId) !=
192 project->custom_object_files.end()) {
193 return false;
194 }
195
196 const auto& defaults =
199 if (defaults.empty()) {
200 return false;
201 }
202
203 project->custom_object_files[kTrackCustomObjectId] = defaults;
204 if (warning != nullptr) {
205 *warning = absl::StrFormat(
206 "Project defines custom_objects_folder but is missing "
207 "custom_object_files[0x%02X]. Seeded default mapping (%d entries).",
208 kTrackCustomObjectId, static_cast<int>(defaults.size()));
209 }
210 return true;
211}
212
213std::vector<std::string> ValidateRomAddressOverrides(
214 const core::RomAddressOverrides& overrides, const Rom& rom) {
215 std::vector<std::string> warnings;
216 if (overrides.addresses.empty()) {
217 return warnings;
218 }
219
220 const auto rom_size = rom.size();
221 auto warn = [&](const std::string& message) {
222 warnings.push_back(message);
223 };
224
225 auto check_range = [&](const std::string& label, uint32_t addr, size_t span) {
226 const size_t addr_size = static_cast<size_t>(addr);
227 if (addr_size >= rom_size || addr_size + span > rom_size) {
228 warn(absl::StrFormat("ROM override '%s' out of range: 0x%X (size 0x%X)",
229 label, addr, rom_size));
230 }
231 };
232
233 for (const auto& [key, value] : overrides.addresses) {
234 check_range(key, value, 1);
235 }
236
239 // Defaults match the Oracle expanded message bank ($2F8000-$2FFFFF).
240 // Kept local to avoid pulling message editor constants into core validation.
241 constexpr uint32_t kExpandedMessageStartDefault = 0x178000;
242 constexpr uint32_t kExpandedMessageEndDefault = 0x17FFFF;
243 const uint32_t start =
245 .value_or(kExpandedMessageStartDefault);
246 const uint32_t end =
248 .value_or(kExpandedMessageEndDefault);
249 if (start >= rom_size || end >= rom_size) {
250 warn(absl::StrFormat(
251 "Expanded message range out of ROM bounds: 0x%X-0x%X", start, end));
252 } else if (end < start) {
253 warn(absl::StrFormat("Expanded message range invalid: 0x%X-0x%X", start,
254 end));
255 }
256 }
257
258 if (auto hook =
260 check_range(core::RomAddressKey::kExpandedMusicHook, *hook, 4);
261 if (*hook < rom_size) {
262 auto opcode = rom.ReadByte(*hook);
263 if (opcode.ok() && opcode.value() != 0x22) {
264 warn(absl::StrFormat(
265 "Expanded music hook at 0x%X is not a JSL opcode (0x%02X)", *hook,
266 opcode.value()));
267 }
268 }
269 }
270
271 if (auto main =
274 }
275
276 if (auto aux = overrides.GetAddress(core::RomAddressKey::kExpandedMusicAux)) {
277 check_range(core::RomAddressKey::kExpandedMusicAux, *aux, 4);
278 }
279
280 if (HasAnyOverride(overrides,
285 const uint32_t marker =
288 const uint32_t magic =
291 check_range("overworld_ptr_marker", marker, 1);
292 if (marker < rom_size) {
293 if (rom.data()[marker] != static_cast<uint8_t>(magic)) {
294 warn(absl::StrFormat(
295 "Overworld expanded pointer marker mismatch at 0x%X: expected "
296 "0x%02X, found 0x%02X",
297 marker, static_cast<uint8_t>(magic), rom.data()[marker]));
298 }
299 }
300 }
301
302 if (HasAnyOverride(overrides,
307 const uint32_t flag_addr =
308 overrides
311 check_range("overworld_entrance_flag_expanded", flag_addr, 1);
312 if (flag_addr < rom_size && rom.data()[flag_addr] == 0xB8) {
313 warn(
314 absl::StrFormat("Overworld entrance flag at 0x%X is still 0xB8 "
315 "(vanilla); expanded entrance tables may be ignored",
316 flag_addr));
317 }
318 }
319
320 return warnings;
321}
322
323// The ROM file's remembered load options, when its encoding decodes and the
324// ROM still has the ZSCustomOverworld byte the choice was made with.
325std::optional<RomLoadOptionsDialog::RememberedChoice>
326FindRememberedRomLoadChoice(const UserSettings& settings, const Rom* rom) {
327 if (rom == nullptr || !rom->is_loaded()) {
328 return std::nullopt;
329 }
330 const auto& choices = settings.prefs().rom_load_choices;
331 const auto it =
332 choices.find(RomLoadOptionsDialog::RomChoiceKey(rom->filename()));
333 if (it == choices.end()) {
334 return std::nullopt;
335 }
336 auto choice = RomLoadOptionsDialog::DecodeRememberedChoice(it->second);
337 if (!choice.has_value() ||
338 choice->zso_version_byte !=
340 return std::nullopt;
341 }
342 return choice;
343}
344
345} // namespace
346
348 // Initialize ROM load options dialog callbacks
350 [this](const RomLoadOptionsDialog::LoadOptions& options) {
351 // The apply guard checked the owner. Release it before anything
352 // below can switch sessions, so that switch does not report this
353 // confirm as a dialog closed without changes.
355
356 // Apply feature flags from dialog
357 auto& flags = core::FeatureFlags::get();
360 if (auto* session = session_coordinator_->GetActiveRomSession()) {
361 session->feature_flags = flags;
362 }
363 }
364 // Store them in the session's project context too, or the next
365 // ApplyCurrentProjectRuntimeContext() puts the old flags back.
367
368 // Remember or forget the choice for this ROM file.
369 const std::string rom_key = RomLoadOptionsDialog::RomChoiceKey(
371 if (!rom_key.empty()) {
372 auto& choices = user_settings_.prefs().rom_load_choices;
373 const int zso_byte = rom_load_options_dialog_.zso_version_byte();
374 if (options.remember_for_rom && zso_byte >= 0) {
375 choices[rom_key] =
377 } else {
378 choices.erase(rom_key);
379 }
380 settings_dirty_ = true;
382 }
383
384 // Create project if requested
385 if (options.create_project && !options.project_name.empty()) {
386 auto status =
388 if (!status.ok()) {
389 toast_manager_.Show(absl::StrFormat("Failed to create project: %s",
390 status.message()),
392 } else {
393 toast_manager_.Show("Project created: " + options.project_name,
395 }
396 }
397
398 // Close dialog, then pick the post-load surface again: the options
399 // may have created a project, but an open editor still wins. A
400 // review of an open ROM keeps the user where they are.
404 options.remember_for_rom
405 ? "ROM load options applied and remembered for this file"
406 : "ROM load options applied",
408 } else {
410 }
411
412 LOG_INFO("EditorManager", "ROM load options applied: preset=%s",
413 options.selected_preset.c_str());
414 });
415 // Cancel keeps the flags the session already has. After an open, it still
416 // has to leave the user on an editor or the picker.
422 }
423 });
426 return true;
427 }
428 CloseRomLoadOptions("its session or ROM file is no longer active");
429 return false;
430 });
431}
432
444
449
461
462void EditorManager::CloseRomLoadOptions(const char* reason) {
463 const bool had_owner = rom_load_options_owner_.has_value();
467 if (had_owner) {
469 absl::StrFormat("ROM load options closed without changes because %s",
470 reason),
472 }
473}
474
477 return false;
478 }
479 const auto choice =
480 FindRememberedRomLoadChoice(user_settings_, GetCurrentRom());
481 if (!choice.has_value()) {
482 return false;
483 }
487 return true;
488}
489
491 const auto* rom = GetCurrentRom();
492 if (!rom || !rom->is_loaded()) {
493 return;
494 }
495 const RomLoadOptionsOwner owner{
499 toast_manager_.Show("Opened with this ROM's remembered load options",
500 ToastType::kInfo, 6.0f, "Change…", [this, owner]() {
503 }
504 });
505}
506
507void EditorManager::FinishRawRomOpen(bool remembered_load_options,
508 bool ask_for_load_options,
509 bool is_first_time_rom_path) {
510 if (ask_for_load_options && ui_coordinator_) {
511 // The dialog's confirm and cancel callbacks show the post-load surface.
512 // A path new to recent files leans toward creating a project.
513 ui_coordinator_->SetWelcomeScreenVisible(false);
514 rom_load_options_dialog_.Open(GetCurrentRom(), is_first_time_rom_path);
517 return;
518 }
520 if (remembered_load_options) {
522 }
523}
524
526 Rom* rom = GetCurrentRom();
527 if (rom == nullptr || !rom->is_loaded()) {
528 toast_manager_.Show("Open a ROM to review its load options",
530 return;
531 }
534 "This project's save options come from its project file",
536 return;
537 }
538 const auto remembered = FindRememberedRomLoadChoice(user_settings_, rom);
541 remembered.has_value() ? remembered->options.selected_preset
542 : std::string(),
543 remembered.has_value());
546}
547
549 // Utility callbacks removed - now handled via EventBus
550
552 [this](bool visible, bool expanded) {
553 user_settings_.prefs().sidebar_visible = visible;
554 user_settings_.prefs().sidebar_panel_expanded = expanded;
555 settings_dirty_ = true;
557 });
560 settings_dirty_ = true;
562 });
564 [this](float width) {
566 settings_dirty_ = true;
568 });
569
571 const std::string& category) {
572 if (category.empty() ||
574 return;
575 }
578
579 const auto& prefs = user_settings_.prefs();
580 auto it = prefs.panel_visibility_state.find(category);
581 if (it != prefs.panel_visibility_state.end()) {
583 window_manager_.GetActiveSessionId(), it->second);
584 } else {
585 // No saved visibility state for this category yet.
586 //
587 // Apply LayoutPresets defaults only when this category has *no*
588 // visible panels (editors may have already shown their own defaults,
589 // e.g. Dungeon Workbench).
590 const size_t session_id = window_manager_.GetActiveSessionId();
591 bool any_visible = false;
592 for (const auto& desc :
593 window_manager_.GetWindowsInCategory(session_id, category)) {
594 if (desc.visibility_flag && *desc.visibility_flag) {
595 any_visible = true;
596 break;
597 }
598 }
599
600 if (!any_visible) {
601 const EditorType type =
603 for (const auto& window_id : LayoutPresets::GetDefaultWindows(type)) {
604 window_manager_.OpenWindow(session_id, window_id);
605 }
606 }
607 }
608
609 settings_dirty_ = true;
611 });
612
614 [this](const std::string& category) -> Editor* {
615 Editor* editor = ResolveEditorForCategory(category);
617 return editor;
618 });
619
621 [this](const std::string& category) {
623 if (type != EditorType::kSettings && type != EditorType::kUnknown) {
624 SwitchToEditor(type, true);
625 }
626 });
627
629 [this](const std::string& category) {
630 if (ui_coordinator_) {
631 ui_coordinator_->SetStartupSurface(StartupSurface::kEditor);
632 }
633
634 if (category == "Agent") {
635#ifdef YAZE_BUILD_AGENT_UI
636 ShowAIAgent();
637#endif
638 return;
639 }
640
642 if (type != EditorType::kSettings) {
643 SwitchToEditor(type, true);
644 }
645 });
646
648
650 [this](const std::string& category, const std::string& path) {
651 if (category == "Assembly") {
652 if (auto* editor_set = GetCurrentEditorSet()) {
653 editor_set->ChangeActiveAssemblyFile(path);
655 }
656 }
657 });
658}
659
661 auto load_from_path = [this](const std::string& file_name) -> absl::Status {
662 if (file_name.empty()) {
663 return absl::OkStatus();
664 }
665 if (ActivateExistingSessionForOpen(file_name)) {
666 return absl::OkStatus();
667 }
668
669 // Check if this is a project file - route to project loading
670 if (absl::EndsWith(file_name, ".yaze") ||
671 absl::EndsWith(file_name, ".zsproj") ||
672 absl::EndsWith(file_name, ".yazeproj")) {
673 return OpenRomOrProjectInternal(file_name);
674 }
675
676 auto path_status =
677 session_coordinator_->CheckBackingFileAvailable(file_name);
678 if (!path_status.ok()) {
679 toast_manager_.Show(std::string(path_status.message()),
681 return path_status;
682 }
683
684 // Delegate ROM loading to RomFileManager
685 Rom temp_rom;
686 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, file_name));
687
688 const std::optional<size_t> previous_session_id =
690 const size_t previous_session_count =
691 session_coordinator_->GetTotalSessionCount();
692 auto session_or = session_coordinator_->CreateSessionFromRom(
693 std::move(temp_rom), file_name);
694 if (!session_or.ok()) {
695 return session_or.status();
696 }
697
701
702 // HandleSessionCreated binds the project context that governed this open.
703 // Reapply it here rather than partially clearing global runtime state.
706 // Remembered load options have to be live before assets load. Without a
707 // UI there is no dialog to skip, so headless opens keep neutral flags.
708 const bool remembered_load_options =
710
711 // Keep ResourceLabelProvider in sync with the newly-active ROM session
712 // before any editors/assets query room/sprite names.
714
715#ifdef YAZE_ENABLE_TESTING
717#endif
718
720 const auto& recent_files = manager.GetRecentFiles();
721 const bool is_first_time_rom_path =
722 std::find(recent_files.begin(), recent_files.end(), file_name) ==
723 recent_files.end();
724 auto asset_status = LoadAssetsForMode();
725 if (!asset_status.ok()) {
726 auto rollback_status =
727 DiscardProvisionalSessionCreatedSince(previous_session_count);
728 RestoreProjectContextAfterFailedOpen(previous_session_id);
729 if (!rollback_status.ok()) {
730 return absl::InternalError(
731 absl::StrFormat("%s; session rollback failed: %s",
732 asset_status.message(), rollback_status.message()));
733 }
734 return asset_status;
735 }
736
737 manager.AddFile(file_name);
738 manager.Save();
739
740 if (ui_coordinator_) {
742 remembered_load_options,
743 !remembered_load_options && !current_project_.project_opened(),
744 is_first_time_rom_path);
745 }
746
747 return absl::OkStatus();
748 };
749
750#if defined(__APPLE__) && TARGET_OS_IOS == 1
751 // On iOS, route through the SwiftUI overlay document picker to get proper
752 // security-scoped access to iCloud Drive and Files app locations. This
753 // mirrors how OpenProject() works on iOS and supports cloud ROMs.
754 // The SwiftUI picker calls YazeIOSBridge.loadRomAtPath: after importing the
755 // ROM to a persistent sandbox directory.
757 return absl::OkStatus();
758#else
761 return load_from_path(file_name);
762#endif
763}
764
766 const std::optional<std::string>& save_as_filename) {
767 auto* current_rom = GetCurrentRom();
768 auto* current_editor_set = GetCurrentEditorSet();
769 if (!current_rom || !current_editor_set) {
770 return absl::FailedPreconditionError("No ROM or editor set loaded");
771 }
772 if (auto* overworld = static_cast<OverworldEditor*>(
773 current_editor_set->GetExistingEditor(EditorType::kOverworld));
774 overworld && overworld->HasUnpublishedProjectGraphics()) {
775 return absl::FailedPreconditionError(
776 "Publish project graphics before Save ROM (Edit / Import Project "
777 "Graphics)");
778 }
781 RETURN_IF_ERROR(CheckRomWritePolicy(save_as_filename));
782 }
783 const auto lyra_layout = current_rom->InspectLyraLayout();
784 const bool lyra_terrain_save =
785 lyra_layout.presence == rom::LyraLayoutPresence::kValid &&
786 lyra_layout.a0_terrain_write_supported;
787 if (!lyra_terrain_save) {
788 RETURN_IF_ERROR(current_rom->CheckLayoutWritable());
789 } else if (!save_as_filename.has_value()) {
790 return absl::FailedPreconditionError("Lyra A0 terrain requires Save As");
791 }
792 if (save_as_filename.has_value() && save_as_filename->empty()) {
793 return absl::InvalidArgumentError("No filename provided for save as");
794 }
795 if (save_as_filename.has_value()) {
796 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(
797 *save_as_filename, GetCurrentSessionId()));
798 }
799
800 // Refuse excluded model edits before serializers, project rebinding, or disk
801 // writes. Reuse the same preflight as direct Save / Apply Room.
802 if (auto* dungeon = static_cast<DungeonEditorV2*>(
803 current_editor_set->GetExistingEditor(EditorType::kDungeon))) {
804 RETURN_IF_ERROR(dungeon->ValidateSaveScope());
805 }
806
807 ASSIGN_OR_RETURN(const project::YazeProject* save_project,
809 // Hash/write/safety checks must read the same immutable session snapshot as
810 // the rest of this save, never a process-global project left by another ROM.
811 rom_lifecycle_.SetProjectContext(save_project);
812 struct LifecycleProjectContextGuard {
813 RomLifecycleManager* lifecycle = nullptr;
814 project::YazeProject* restore = nullptr;
815 ~LifecycleProjectContextGuard() {
816 if (lifecycle) {
817 lifecycle->SetProjectContext(restore);
818 }
819 }
820 } lifecycle_project_guard{&rom_lifecycle_, &current_project_};
821
822 // GraphicsEditor tracks pixel edits in its model, not in the ROM buffer.
823 // With Save Graphics Sheets enabled it joins the editor saves below; with
824 // it disabled the edits cannot be written, so block before any serializer
825 // can mutate the ROM rather than report a save that omitted the sheets.
826 // Gfx group tables (blocksets, spritesets, palettesets) are edited in
827 // GameData. Without Save Gfx Groups they cannot be written, so block before
828 // any serializer runs instead of saving a ROM that silently drops them.
829 auto* save_session = session_coordinator_->GetActiveRomSession();
830 auto& palette_manager = gfx::PaletteManager::Get();
831 const bool pending_palettes =
832 save_session != nullptr &&
833 palette_manager.HasUnsavedChanges(&save_session->game_data);
834 // Both existing save settings authorize PaletteManager's modified colors.
835 // Do not make palette-only persistence depend on opening Dungeon first.
836 const bool palette_save_enabled =
839 if (pending_palettes && !palette_save_enabled) {
840 return absl::FailedPreconditionError(
841 "Save blocked: palette edits are pending, but palette saving is "
842 "disabled. Enable Save All Palettes or Save Dungeon Palettes, or undo "
843 "the palette edits before saving the ROM.");
844 }
845 const bool pending_gfx_groups =
846 save_session != nullptr && save_session->HasPendingGfxGroupChanges();
847 if (pending_gfx_groups && !core::FeatureFlags::get().kSaveGfxGroups) {
848 return absl::FailedPreconditionError(
849 "Save blocked: blockset/spriteset (gfx group) edits are pending, but "
850 "Save Gfx Groups is disabled. Enable it, or undo the gfx group edits "
851 "before saving the ROM.");
852 }
853
854 if (current_editor_set->HasPendingGraphicsChanges() &&
856 return absl::FailedPreconditionError(
857 "Save blocked: graphics sheet edits are pending, but Save Graphics "
858 "Sheets is disabled. Enable it, or discard the graphics sheet edits "
859 "before saving the ROM.");
860 }
861
862 // Preflight map-only Screen edits before any serializer can mutate the ROM.
863 // Retain the exact planned extents for manifest protection if disk diffing
864 // is unavailable. Never materialize an unopened Screen editor here.
865 auto* screen_editor = static_cast<ScreenEditor*>(
866 current_editor_set->GetExistingEditor(EditorType::kScreen));
867 std::vector<std::pair<uint32_t, uint32_t>> predicted_screen_write_ranges;
868 if (screen_editor != nullptr && (screen_editor->IsRomBackedStateValid() ||
869 screen_editor->HasPendingScreenChanges())) {
870 ASSIGN_OR_RETURN(auto plan, screen_editor->PrepareDungeonMapSave());
871 for (const auto& write : plan.writes) {
872 predicted_screen_write_ranges.emplace_back(
873 write.pc_address, write.pc_address + write.data.size());
874 }
875 }
876
877 // --- State machine checks (delegated to RomLifecycleManager) ---
879 return absl::CancelledError("Save pending confirmation");
880 }
881
882 RETURN_IF_ERROR(CheckRomWritePolicy(save_as_filename));
883
884 if (lyra_terrain_save) {
885 // This capability never enlists generic editor serializers. Refuse known
886 // pending model domains rather than silently omit their edits.
887 auto* dungeon = static_cast<DungeonEditorV2*>(
888 current_editor_set->GetExistingEditor(EditorType::kDungeon));
889 if (pending_palettes || pending_gfx_groups ||
890 current_editor_set->HasPendingGraphicsChanges() ||
891 current_editor_set->HasPendingScreenChanges() ||
892 (dungeon && dungeon->HasPendingDungeonChanges())) {
893 return absl::FailedPreconditionError(
894 "Lyra Save As supports only A0 terrain; unrelated edits are pending");
895 }
896 auto* overworld = static_cast<OverworldEditor*>(
897 current_editor_set->GetExistingEditor(EditorType::kOverworld));
899 return absl::FailedPreconditionError(
900 "Open Lyra A0 terrain before Save As");
901 }
903 RETURN_IF_ERROR(overworld->SaveLyraA0TerrainAs(*save_as_filename));
904 CaptureCurrentRomIdentity(/*preserve_editing_source=*/true);
905 if (auto* session = session_coordinator_->GetActiveRomSession()) {
906 session->filepath = *save_as_filename;
907 session->backup_restore_pending = false;
908 }
911 recent.AddFile(*save_as_filename);
912 recent.Save();
913 if (popup_manager_)
915 return RecordCompletedNativeSave(*save_project, *save_as_filename,
916 /*save_as=*/true);
917 }
918
919 // Pot streams are saved sparsely: unchanged/unloaded rooms are inventoried
920 // from ROM by the serializer. No load-all confirmation is necessary.
921
922 // --- Backup policy setup ---
923 const auto experiment_policy =
924 current_editor_ != nullptr
927 const bool force_experimental_backup =
929 experiment_policy.experimental &&
930 experiment_policy.disabled_save_posture ==
932 if (force_experimental_backup) {
934 "Experimental editor save: backup forced before writing the ROM",
936 }
937 if (save_project->project_opened()) {
939 save_project->workspace_settings.backup_on_save ||
940 force_experimental_backup,
941 save_project->GetAbsolutePath(save_project->rom_backup_folder),
945 } else {
947 user_settings_.prefs().backup_before_save || force_experimental_backup,
948 "", 20, true, 14);
949 }
950
951 // Reject an already-invalid Oracle layout before any editor serializer can
952 // mutate the ROM buffer or clear its own dirty tracking. The post-save check
953 // below remains mandatory because serializers may introduce a violation.
955
956 // Serializers clear their dirty flags after a successful in-memory write.
957 // Capture their conservative write prediction first so conflict protection
958 // still has a fail-closed fallback when the on-disk ROM cannot be read.
959 const auto predicted_dungeon_write_ranges =
960 current_editor_set->CollectDungeonWriteRanges();
961
962 ScopedRomTransaction rom_transaction(*current_rom);
963 ScopedEditorSaveTransactions editor_transactions;
964
965 auto save_editor = [&editor_transactions](Editor* editor) -> absl::Status {
966 if (editor == nullptr) {
967 return absl::OkStatus();
968 }
969 RETURN_IF_ERROR(editor_transactions.Begin(editor));
970 return editor->Save();
971 };
972
973 // --- Save editor-specific data ---
974 // A failed lazy Screen load leaves an invalid, clean editor behind. It has
975 // nothing to serialize and must not poison unrelated ROM saves.
976 if (screen_editor != nullptr && (screen_editor->IsRomBackedStateValid() ||
977 screen_editor->HasPendingScreenChanges())) {
978 RETURN_IF_ERROR(save_editor(screen_editor));
979 }
980 // A coordinated save must not materialize an unrelated lazy editor. Its
981 // serializer can project broad writes (and correctly trip manifest guards)
982 // even though the user never opened or edited that domain.
984 save_editor(current_editor_set->GetExistingEditor(EditorType::kDungeon)));
985 // Project context binding creates an Overworld editor before its model is
986 // loaded. Its empty caches are not ROM edits and must not be serialized.
987 if (auto* overworld = static_cast<OverworldEditor*>(
988 current_editor_set->GetExistingEditor(EditorType::kOverworld));
989 overworld && overworld->overworld().is_loaded()) {
990 RETURN_IF_ERROR(save_editor(overworld));
991 }
992
993 // Dungeon may already have serialized palettes under its own transaction.
994 // Otherwise enlist the Palette editor in the same file-save transaction.
995 if (pending_palettes &&
996 palette_manager.HasUnsavedChanges(&save_session->game_data)) {
998 RETURN_IF_ERROR(save_editor(
999 current_editor_set->GetExistingEditor(EditorType::kPalette)));
1000 }
1001
1002 if (core::FeatureFlags::get().kSaveMessages) {
1005 save_editor(current_editor_set->GetEditor(EditorType::kMessage)));
1006 }
1007
1008 // Graphics sheets are written through zelda3::WriteGfxSheet (in place, or
1009 // relocated into manifest-registered free space) before the write-conflict
1010 // check below diffs the ROM against disk.
1011 if (core::FeatureFlags::get().kSaveGraphicsSheet) {
1012 RETURN_IF_ERROR(save_editor(
1013 current_editor_set->GetExistingEditor(EditorType::kGraphics)));
1014 }
1015
1016 // Gfx group tables are written byte-for-byte where they differ, then read
1017 // back. The ROM transaction restores them if a later step fails.
1018 if (pending_gfx_groups && core::FeatureFlags::get().kSaveGfxGroups) {
1020 zelda3::SaveGfxGroups(*current_rom, save_session->game_data));
1021 }
1022
1023 // Oracle guardrails: refuse to write obviously corrupted ROM layouts.
1025
1026 // --- Write conflict check (ASM-owned address protection) ---
1027 if (save_project->project_opened() && save_project->hack_manifest.loaded()) {
1029 std::vector<std::pair<uint32_t, uint32_t>> write_ranges;
1030 bool diff_computed = false;
1031
1032 if (!current_rom->filename().empty()) {
1033 std::ifstream file(current_rom->filename(), std::ios::binary);
1034 if (file.is_open()) {
1035 file.seekg(0, std::ios::end);
1036 const std::streampos end = file.tellg();
1037 if (end >= 0) {
1038 std::vector<uint8_t> disk_data(static_cast<size_t>(end));
1039 file.seekg(0, std::ios::beg);
1040 file.read(reinterpret_cast<char*>(disk_data.data()),
1041 static_cast<std::streamsize>(disk_data.size()));
1042 if (file) {
1043 diff_computed = true;
1044 auto diff = yaze::rom::ComputeDiffRanges(disk_data,
1045 current_rom->vector());
1046 if (!diff.ranges.empty()) {
1047 LOG_DEBUG("EditorManager",
1048 "ROM save diff: %zu bytes changed in %zu range(s)",
1049 diff.total_bytes_changed, diff.ranges.size());
1050 write_ranges = std::move(diff.ranges);
1051 }
1052 }
1053 }
1054 }
1055 }
1056
1057 if (write_ranges.empty() && !diff_computed) {
1058 write_ranges = predicted_dungeon_write_ranges;
1059 write_ranges.insert(write_ranges.end(),
1060 predicted_screen_write_ranges.begin(),
1061 predicted_screen_write_ranges.end());
1062 }
1063
1064 if (!write_ranges.empty()) {
1065 auto conflicts =
1066 save_project->hack_manifest.AnalyzePcWriteRanges(write_ranges);
1067 if (!conflicts.empty()) {
1068 rom_lifecycle_.SetPendingWriteConflicts(std::move(conflicts));
1069 if (popup_manager_) {
1071 }
1073 absl::StrFormat(
1074 "Save paused: %zu write conflict(s) with ASM hooks",
1077 return absl::CancelledError("Write conflict confirmation required");
1078 }
1079 }
1080 } else {
1081 // Bypass is single-use, set by the warning popup.
1083 }
1084 }
1085
1086 // Delegate the final atomic disk write to RomFileManager. Save As is part of
1087 // this same transaction and writes only the requested target path.
1088 // A project's tracked custom_collision.json follows only the project's own
1089 // ROM file, never Save As targets or practice copies opened under it.
1090 std::filesystem::path collision_source;
1091 if (!save_as_filename.has_value() && save_project->project_opened() &&
1092 !save_project->custom_collision_json.empty() &&
1093 !save_project->rom_filename.empty()) {
1094 std::error_code same_ec;
1095 if (std::filesystem::equivalent(
1096 save_project->GetAbsolutePath(save_project->rom_filename),
1097 current_rom->filename(), same_ec)) {
1098 collision_source =
1099 save_project->GetAbsolutePath(save_project->custom_collision_json);
1100 }
1101 }
1102 // A same-byte save still supersedes the prior native operation. Retire its
1103 // external context before committing disk/baseline state, so publication or
1104 // write failure cannot leave the previous operation consumable as current.
1105 // Pending confirmation and serializer/conflict checks above do not retire it.
1107 auto save_status =
1108 save_as_filename.has_value()
1109 ? rom_file_manager_.SaveRomAs(current_rom, *save_as_filename)
1110 : rom_file_manager_.SaveRom(current_rom, collision_source);
1111 if (save_status.ok()) {
1112 editor_transactions.Commit();
1113 rom_transaction.Commit();
1114 CaptureCurrentRomIdentity(/*preserve_editing_source=*/true);
1116 if (auto* session = session_coordinator_->GetActiveRomSession()) {
1117 session->backup_restore_pending = false;
1118 }
1119 }
1120 // Write-confirm bypass is single-use. Clear it after a successful save.
1122
1123 if (save_as_filename.has_value()) {
1125 if (auto* session = session_coordinator_->GetActiveRomSession()) {
1126 session->filepath = *save_as_filename;
1127 }
1128 }
1129
1131 manager.AddFile(*save_as_filename);
1132 manager.Save();
1133 if (popup_manager_) {
1135 }
1136 }
1138 *save_project, save_as_filename.value_or(current_rom->filename()),
1139 save_as_filename.has_value());
1140 }
1141 return save_status;
1142}
1143
1145 const project::YazeProject& project, const std::string& saved_rom,
1146 bool save_as, bool rom_written) {
1147#if defined(__EMSCRIPTEN__) || !defined(YAZE_WITH_JSON)
1148 // Browser persistence has no native producer or durable publication lock.
1149 return absl::OkStatus();
1150#else
1152 const auto finish = [&]() -> absl::Status {
1153 std::error_code error;
1154 const auto owner = std::filesystem::weakly_canonical(
1155 project.project_opened() ? project.filepath : saved_rom, error);
1156 if (error)
1157 return absl::FailedPreconditionError("Cannot resolve save receipt owner");
1158 request.receipt_path = owner.string() + ".native-save.json";
1159 request.operation = save_as ? "save_as" : "save";
1160 std::random_device random;
1161 for (int i = 0; i < 4; ++i)
1162 request.operation_id += absl::StrFormat("%08x", random());
1163 const auto* rom = GetCurrentRom();
1164 if (!rom)
1165 return absl::FailedPreconditionError("Saved ROM session is unavailable");
1166 const auto rom_path = std::filesystem::canonical(saved_rom, error);
1167 if (error)
1168 return absl::FailedPreconditionError("Cannot resolve saved ROM");
1169 if (rom_written) {
1170 request.artifacts.push_back(
1171 {"rom", rom_path, ComputeRomFileBytesSha256(*rom)});
1172 } else {
1173 // A project-only Save binds the ROM file only as this session loaded or
1174 // last wrote it; bytes changed by another writer are never blessed.
1175 const auto* session = session_coordinator_
1176 ? session_coordinator_->GetActiveRomSession()
1177 : nullptr;
1178 if (session == nullptr || !session->rom_baseline ||
1179 session->rom_baseline->backing_sha256.empty()) {
1180 return absl::FailedPreconditionError(
1181 "The ROM has no verified on-disk identity in this session; save "
1182 "the ROM first");
1183 }
1184 ASSIGN_OR_RETURN(const auto on_disk,
1186 if (on_disk != session->rom_baseline->backing_sha256) {
1187 return absl::FailedPreconditionError(
1188 "The ROM file changed on disk since yaze loaded or last saved it; "
1189 "use Revert to Saved to load it, or save the ROM from yaze to "
1190 "replace it");
1191 }
1192 request.artifacts.push_back({"rom", rom_path, on_disk});
1193 }
1194 // Pin the process's expected producer once; the publisher still rehashes
1195 // it before publication and on readback for every completed save.
1196 static const auto producer =
1197 []() -> absl::StatusOr<std::pair<std::filesystem::path, std::string>> {
1198 ASSIGN_OR_RETURN(auto executable,
1200 ASSIGN_OR_RETURN(auto hash,
1202 return std::make_pair(std::move(executable), std::move(hash));
1203 }();
1204 RETURN_IF_ERROR(producer.status());
1205 request.artifacts.push_back(
1206 {"producer", producer->first, producer->second});
1207
1208 const auto bind = [&](const std::string& role, const std::string& path) {
1209 const auto digest = core::ComputeSourceArtifactFileSha256(path);
1210 if (!digest.ok()) {
1211 request.binding_errors.push_back(
1212 role + ": " + std::string(digest.status().message()));
1213 } else {
1214 request.artifacts.push_back({role, path, *digest});
1215 }
1216 };
1217 if (project.project_opened()) {
1218 bind("project", project.filepath);
1219 const auto live_sources = ProjectSaveSources(project);
1220 std::ifstream descriptor(project.filepath, std::ios::binary);
1221 if (descriptor) {
1222 const std::string bytes{std::istreambuf_iterator<char>(descriptor), {}};
1223 const auto binding_status = project.ValidateSavedSourceBindings(bytes);
1224 if (!binding_status.ok())
1225 request.binding_errors.push_back(
1226 std::string(binding_status.message()));
1227 }
1228 for (const auto& [role, path] : live_sources)
1229 bind(role, path);
1230 for (size_t i = 0; i < project.sprite_assets.size(); ++i) {
1231 const auto& asset = project.sprite_assets[i];
1232 if (asset.source_path.empty())
1233 continue;
1234 const auto role = "dependency.sprite_" + std::to_string(i) + "_source";
1235 const auto binding = std::find_if(
1236 request.artifacts.begin(), request.artifacts.end(),
1237 [&](const auto& artifact) { return artifact.role == role; });
1238 if (project.sprite_source_root.empty() ||
1239 (binding != request.artifacts.end() &&
1240 binding->expected_sha256 != asset.source_sha256))
1241 request.binding_errors.push_back(role + ": imported source is stale");
1242 }
1243
1244#ifdef YAZE_WITH_JSON
1245 // Preview transfers are explicit external dependencies of the manifest.
1246 if (!project.overworld_graphics_preview.empty()) {
1247 try {
1248 const auto path =
1250 std::ifstream input(path);
1251 const auto preview = Json::parse(input);
1252 size_t index = 0;
1253 for (const auto& area : preview.at("areas"))
1254 for (const auto& transfer : area.at("transfers"))
1255 bind("dependency.preview_transfer_" + std::to_string(index++),
1256 (std::filesystem::path(path).parent_path() /
1257 transfer.at("path").get<std::string>())
1258 .string());
1259 } catch (const std::exception& exception) {
1260 request.binding_errors.push_back(
1261 std::string("Cannot bind graphics preview transfers: ") +
1262 exception.what());
1263 }
1264 }
1265#else
1266 if (!project.overworld_graphics_preview.empty())
1267 request.binding_errors.push_back(
1268 "Graphics preview dependency binding requires JSON support");
1269#endif
1270 if (!project.lyra_terrain_file.empty()) {
1271 auto* overworld = static_cast<OverworldEditor*>(
1272 GetCurrentEditorSet()->GetExistingEditor(EditorType::kOverworld));
1273 if (!overworld || !overworld->lyra_canvas_.loaded()) {
1274 request.binding_errors.push_back("Terrain document is not loaded");
1275 } else {
1276 auto context = project;
1277 const auto valid = overworld->lyra_canvas_.ValidateSave(
1278 &context, overworld->overworld());
1279 if (!valid.ok())
1280 request.binding_errors.push_back(std::string(valid.message()));
1281 std::ifstream input(
1282 project.GetAbsolutePath(project.lyra_terrain_file));
1283 const std::string bytes{std::istreambuf_iterator<char>(input), {}};
1284 const auto* live = overworld->lyra_canvas_.document();
1285 const auto saved =
1286 core::LyraTerrainDocument::Parse(bytes, live->profile());
1287 if (!saved.ok() || saved->Serialize() != live->Serialize())
1288 request.binding_errors.push_back(
1289 "Saved terrain differs from the live document");
1290 }
1291 }
1292 }
1293 // Keep completion recordable when a large or malformed project has many
1294 // unavailable optional inputs; never turn truncated evidence into complete.
1295 if (request.artifacts.size() > 256) {
1296 request.binding_errors.push_back(
1297 "More than 256 source bindings; incomplete");
1298 request.artifacts.resize(256);
1299 }
1300 if (request.binding_errors.size() > 256) {
1301 const auto omitted = request.binding_errors.size() - 255;
1302 request.binding_errors.resize(255);
1303 request.binding_errors.push_back(std::to_string(omitted) +
1304 " additional binding errors");
1305 }
1306 for (auto& diagnostic : request.binding_errors) {
1307 if (diagnostic.size() > 1024) {
1308 size_t end = 1000;
1309 while (end > 0 &&
1310 (static_cast<unsigned char>(diagnostic[end]) & 0xC0) == 0x80)
1311 --end;
1312 diagnostic = diagnostic.substr(0, end) + " [truncated]";
1313 }
1314 }
1315 const auto descriptor = std::find_if(
1316 request.artifacts.begin(), request.artifacts.end(),
1317 [](const auto& artifact) { return artifact.role == "project"; });
1318 const std::string project_path =
1319 descriptor == request.artifacts.end()
1320 ? ""
1321 : std::filesystem::absolute(descriptor->path)
1322 .lexically_normal()
1323 .string();
1324 ASSIGN_OR_RETURN(request.context,
1326 project_path));
1327 ASSIGN_OR_RETURN(auto receipt, core::PublishNativeSaveReceipt(request));
1328 if (!request.binding_errors.empty()) {
1329 toast_manager_.Show(std::string(rom_written ? "ROM" : "Project") +
1330 " saved; build inputs are incomplete: " +
1331 request.binding_errors.front(),
1333 }
1334 return absl::OkStatus();
1335 };
1336 absl::Status status;
1337 try {
1338 status = finish();
1339 } catch (const std::exception& exception) {
1340 status = absl::InternalError(exception.what());
1341 }
1342 if (!status.ok()) {
1343 return absl::FailedPreconditionError(
1344 rom_written
1345 ? "ROM saved, but native-save receipt is unavailable: " +
1346 std::string(status.message())
1347 : "Project saved, but its native-save receipt is unavailable; "
1348 "Save again to retry: " +
1349 std::string(status.message()));
1350 }
1352 if (auto* session = session_coordinator_->GetActiveRomSession()) {
1353 session->native_save_completion_pending = false;
1354 session->native_save_completion_save_as = false;
1355 }
1356 }
1357 return status;
1358#endif
1359}
1360
1363 if (const auto* session = session_coordinator_->GetActiveRomSession();
1364 session && session->rom_baseline &&
1365 !session->rom_baseline->editing_source_path.empty()) {
1366 return session->rom_baseline->editing_source_path;
1367 }
1368 }
1369 const auto* rom = GetCurrentRom();
1370 return rom ? rom->filename() : "";
1371}
1372
1374 const std::string& state, const std::string& editing_source_path) {
1375#if defined(__EMSCRIPTEN__) || !defined(YAZE_WITH_JSON)
1376 return absl::OkStatus();
1377#else
1378 const auto origin = editing_source_path.empty()
1380 : editing_source_path;
1381 if (origin.empty())
1382 return absl::OkStatus();
1383 std::string project_path;
1384 std::error_code error;
1386 std::filesystem::is_regular_file(current_project_.filepath, error)) {
1387 project_path = std::filesystem::absolute(current_project_.filepath, error)
1388 .lexically_normal()
1389 .string();
1390 }
1391 if (error == std::errc::no_such_file_or_directory) {
1392 error.clear();
1393 }
1394 if (error) {
1395 return absl::FailedPreconditionError(
1396 "Cannot resolve native-save project context: " + error.message());
1397 }
1398 ASSIGN_OR_RETURN(auto context,
1399 core::GetNativeSaveContext(origin, project_path));
1400 return core::RetireNativeSaveContext(context, state);
1401#endif
1402}
1403
1404absl::Status EditorManager::LoadProjectWithRom(bool allow_dialogs) {
1405 const auto replaced_project_session_id =
1408 : std::nullopt;
1409 auto pending_selection = std::move(pending_project_rom_selection_);
1411 std::string project_rom_path = pending_selection.has_value()
1412 ? pending_selection->candidate_path
1414 const std::string previous_project_rom_path =
1415 pending_selection.has_value() ? pending_selection->previous_path
1417 bool persist_selected_rom = pending_selection.has_value();
1418 auto abandon_selected_rom = [&]() {
1419 if (!persist_selected_rom) {
1420 return;
1421 }
1422 current_project_.rom_filename = previous_project_rom_path;
1424 };
1425
1426 // Check if project has a ROM file specified
1427 if (project_rom_path.empty()) {
1428 if (!allow_dialogs) {
1429 return absl::FailedPreconditionError("Project has no ROM configured");
1430 }
1431 // No ROM specified - prompt user to select one
1433 "Project has no ROM file configured. Please select a ROM.",
1435#if defined(__APPLE__) && TARGET_OS_IOS == 1
1436 // Guard: if the project lives inside a .yazeproj bundle the ROM path
1437 // defaults to bundle/rom, which may not exist yet because iCloud hasn't
1438 // finished the download. Popping the file picker here would let a
1439 // temporary path overwrite the correct bundle path in project.yaze.
1440 // Show guidance and let the user reopen the project once the download
1441 // is complete.
1442 {
1443 auto bundle_parent =
1444 std::filesystem::path(current_project_.filepath).parent_path();
1445 if (bundle_parent.extension() == ".yazeproj") {
1447 "ROM is downloading from iCloud. Reopen the project in a few "
1448 "seconds.",
1449 ToastType::kInfo, 6.0f);
1450 return absl::UnavailableError(
1451 "Project ROM is not available from iCloud yet");
1452 }
1453 }
1454 const std::string target_project_filepath = current_project_.filepath;
1455 const std::optional<size_t> target_session_id =
1457 const uint64_t selection_generation = ++project_rom_selection_generation_;
1460 [this, target_project_filepath, target_session_id,
1461 selection_generation](const std::string& rom_path) {
1462 if (selection_generation != project_rom_selection_generation_ ||
1463 current_project_.filepath != target_project_filepath ||
1464 active_project_context_session_id_ != target_session_id) {
1465 toast_manager_.Show(
1466 "ROM selection ignored because the active project changed",
1467 ToastType::kWarning);
1468 return;
1469 }
1470 const bool restore_pending_transition =
1472 const std::optional<size_t> previous_session_id =
1474 auto restore_after_failure = [&]() {
1475 if (restore_pending_transition) {
1476 RestoreProjectContextAfterFailedOpen(previous_session_id);
1477 }
1478 };
1479 if (rom_path.empty()) {
1480 restore_after_failure();
1481 return;
1482 }
1483 auto path_status =
1484 session_coordinator_->CheckBackingFileAvailable(rom_path);
1485 if (!path_status.ok()) {
1486 toast_manager_.Show(std::string(path_status.message()),
1488 restore_after_failure();
1489 return;
1490 }
1491 auto selection_status = ValidateProjectRomSelection(rom_path);
1492 if (!selection_status.ok()) {
1494 absl::StrFormat("Failed to update project ROM: %s",
1495 selection_status.message()),
1497 restore_after_failure();
1498 return;
1499 }
1500 auto status = LoadProjectWithRom();
1501 if (!status.ok()) {
1503 absl::StrFormat("Failed to load project ROM: %s",
1504 status.message()),
1506 restore_after_failure();
1507 }
1508 });
1509 return absl::OkStatus();
1510#else
1513 if (rom_path.empty()) {
1514 return absl::CancelledError("Project ROM selection cancelled");
1515 }
1516 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
1517 project_rom_path = rom_path;
1518 persist_selected_rom = true;
1519#endif
1520 }
1521
1522 auto backing_status =
1523 session_coordinator_->CheckBackingFileAvailable(project_rom_path);
1524 if (!backing_status.ok()) {
1525 abandon_selected_rom();
1526 return backing_status;
1527 }
1528
1529 // Load ROM from project
1530 Rom temp_rom;
1531 auto load_status = rom_file_manager_.LoadRom(&temp_rom, project_rom_path);
1532 if (!load_status.ok()) {
1533 if (!allow_dialogs) {
1534 abandon_selected_rom();
1535 return load_status;
1536 }
1537 // ROM file not found or invalid - prompt user to select new ROM
1539 absl::StrFormat("Could not load ROM '%s': %s. Please select a new ROM.",
1540 project_rom_path, load_status.message()),
1542#if defined(__APPLE__) && TARGET_OS_IOS == 1
1543 abandon_selected_rom();
1544 // If the ROM is inside a .yazeproj bundle its path may not be readable
1545 // yet because iCloud hasn't finished downloading it. Saving any other
1546 // path here would corrupt the project file with a temporary location.
1547 // Show guidance and bail without touching current_project_.rom_filename.
1548 {
1549 auto rom_parent = std::filesystem::path(project_rom_path).parent_path();
1550 if (rom_parent.extension() == ".yazeproj") {
1552 "ROM is still downloading from iCloud. Try reopening the project "
1553 "in a moment.",
1554 ToastType::kInfo, 6.0f);
1555 return absl::UnavailableError(
1556 "Project ROM is still downloading from iCloud");
1557 }
1558 }
1559 const std::string target_project_filepath = current_project_.filepath;
1560 const std::optional<size_t> target_session_id =
1562 const uint64_t selection_generation = ++project_rom_selection_generation_;
1565 [this, target_project_filepath, target_session_id,
1566 selection_generation](const std::string& rom_path) {
1567 if (selection_generation != project_rom_selection_generation_ ||
1568 current_project_.filepath != target_project_filepath ||
1569 active_project_context_session_id_ != target_session_id) {
1570 toast_manager_.Show(
1571 "ROM selection ignored because the active project changed",
1572 ToastType::kWarning);
1573 return;
1574 }
1575 const bool restore_pending_transition =
1577 const std::optional<size_t> previous_session_id =
1579 auto restore_after_failure = [&]() {
1580 if (restore_pending_transition) {
1581 RestoreProjectContextAfterFailedOpen(previous_session_id);
1582 }
1583 };
1584 if (rom_path.empty()) {
1585 restore_after_failure();
1586 return;
1587 }
1588 auto path_status =
1589 session_coordinator_->CheckBackingFileAvailable(rom_path);
1590 if (!path_status.ok()) {
1591 toast_manager_.Show(std::string(path_status.message()),
1593 restore_after_failure();
1594 return;
1595 }
1596 auto selection_status = ValidateProjectRomSelection(rom_path);
1597 if (!selection_status.ok()) {
1599 absl::StrFormat("Failed to update project ROM: %s",
1600 selection_status.message()),
1602 restore_after_failure();
1603 return;
1604 }
1605 auto status = LoadProjectWithRom();
1606 if (!status.ok()) {
1608 absl::StrFormat("Failed to load project ROM: %s",
1609 status.message()),
1611 restore_after_failure();
1612 }
1613 });
1614 return absl::OkStatus();
1615#else
1618 if (rom_path.empty()) {
1619 return absl::CancelledError("Project ROM selection cancelled");
1620 }
1621 RETURN_IF_ERROR(session_coordinator_->CheckBackingFileAvailable(rom_path));
1622 project_rom_path = rom_path;
1623 persist_selected_rom = true;
1624 RETURN_IF_ERROR(rom_file_manager_.LoadRom(&temp_rom, rom_path));
1625#endif
1626 }
1627
1628 auto policy_status = rom_lifecycle_.CheckRomOpenPolicy(&temp_rom);
1629 if (!policy_status.ok()) {
1630 abandon_selected_rom();
1631 return policy_status;
1632 }
1633
1634 if (persist_selected_rom) {
1635 current_project_.rom_filename = project_rom_path;
1636 }
1637
1638 const size_t previous_session_count =
1639 session_coordinator_->GetTotalSessionCount();
1640 auto session_or = session_coordinator_->CreateSessionFromRom(
1641 std::move(temp_rom), project_rom_path);
1642 if (!session_or.ok()) {
1643 abandon_selected_rom();
1644 return session_or.status();
1645 }
1646 RomSession* session = *session_or;
1647
1651
1652 // Auto-enable custom object rendering when a project defines custom object
1653 // data but the stale feature flag is off.
1654 if (ProjectUsesCustomObjects(current_project_) &&
1657 LOG_WARN("EditorManager",
1658 "Project has custom object data but 'enable_custom_objects' was "
1659 "disabled. Enabling at runtime.");
1660 toast_manager_.Show("Custom object rendering auto-enabled for this project",
1662 }
1663 std::string legacy_mapping_warning;
1664 if (SeedLegacyTrackObjectMapping(&current_project_,
1665 &legacy_mapping_warning)) {
1666 LOG_WARN("EditorManager", "%s", legacy_mapping_warning.c_str());
1668 "Seeded default custom object mapping for object 0x31 (save project "
1669 "to persist)",
1671 }
1672
1673 // Apply project feature flags to both session and global singleton.
1677#if !defined(NDEBUG)
1678 LOG_INFO(
1679 "EditorManager",
1680 "Feature flags applied: kEnableCustomObjects=%s, "
1681 "custom_objects_folder='%s', custom_object_files=%zu entries",
1685#endif
1686
1689 if (version_manager_) {
1690 // Opening a project never creates a Git repository; the Initialize Git
1691 // action in Project Management is the opt-in. Adopt one that exists.
1693 current_project_.git_repository = session->project_context->git_repository;
1694 }
1695
1696 if (auto* rom = GetCurrentRom(); rom && rom->is_loaded()) {
1697 if (IsRomHashMismatch()) {
1699 "Project ROM hash mismatch detected. Check ROM Identity settings.",
1701 }
1702 auto warnings = ValidateRomAddressOverrides(
1704 if (!warnings.empty()) {
1705 for (const auto& warning : warnings) {
1706 LOG_WARN("EditorManager", "%s", warning.c_str());
1707 }
1708 toast_manager_.Show(absl::StrFormat("ROM override warnings: %d (see log)",
1709 warnings.size()),
1711 }
1712 }
1713
1714 // Update test manager with current ROM for ROM-dependent tests (only when
1715 // tests are enabled)
1716#ifdef YAZE_ENABLE_TESTING
1717 LOG_DEBUG("EditorManager", "Setting ROM in TestManager - %p ('%s')",
1718 (void*)GetCurrentRom(),
1719 GetCurrentRom() ? GetCurrentRom()->title().c_str() : "null");
1721#endif
1722
1723 if (auto* editor_set = GetCurrentEditorSet();
1724 editor_set && !current_project_.code_folder.empty()) {
1725 const std::string absolute_code_folder =
1727 // iOS: avoid blocking the main thread during project open / scene updates.
1728#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
1729 editor_set->OpenAssemblyFolder(absolute_code_folder);
1730#endif
1731 // Also set the sidebar file browser path (refresh happens during UI draw).
1732 window_manager_.SetFileBrowserPath("Assembly", absolute_code_folder);
1733 }
1734
1735 // Initialize labels before loading editor assets so room lists / command
1736 // palette entries resolve project registry labels on first render.
1738
1739 auto asset_status = LoadAssetsForMode();
1740 if (!asset_status.ok()) {
1741 abandon_selected_rom();
1742 auto rollback_status =
1743 DiscardProvisionalSessionCreatedSince(previous_session_count);
1744 if (!rollback_status.ok()) {
1745 return absl::InternalError(
1746 absl::StrFormat("%s; session rollback failed: %s",
1747 asset_status.message(), rollback_status.message()));
1748 }
1749 return asset_status;
1750 }
1751
1752 if (persist_selected_rom) {
1753 auto save_status = current_project_.Save();
1754 if (!save_status.ok()) {
1755 abandon_selected_rom();
1756 auto rollback_status =
1757 DiscardProvisionalSessionCreatedSince(previous_session_count);
1758 if (!rollback_status.ok()) {
1759 return absl::InternalError(
1760 absl::StrFormat("%s; session rollback failed: %s",
1761 save_status.message(), rollback_status.message()));
1762 }
1763 return save_status;
1764 }
1766 }
1767
1769
1771
1772 // RestoreProjectContextForSession() already applied the project's workspace
1773 // settings as a UserSettings overlay.
1776
1784 project_management_panel_->SetBuildLogOutput("");
1785 }
1787 active_project_build_.reset();
1789
1790 // Publish project context for hack workflow panels and other consumers
1792
1793 // Add to recent files
1795 manager.AddFile(current_project_.filepath);
1796 manager.Save();
1797
1798 // Update project management panel with loaded project
1801 project_management_panel_->SetVersionManager(version_manager_);
1803 }
1804
1805 toast_manager_.Show(absl::StrFormat("Project '%s' loaded successfully",
1808
1809 // Async ROM selection can return before a project opens. Discard only at
1810 // completed adoption, and preserve drafts belonging to another session.
1811 if (replaced_project_session_id.has_value() &&
1812 GetCurrentSessionId() == *replaced_project_session_id) {
1813 if (auto* editors = GetCurrentEditorSet()) {
1814 editors->DiscardPlanningProjectDraft();
1815 }
1816 }
1819 return absl::OkStatus();
1820}
1821
1823 Rom&& rom, const std::string& filepath) {
1824 if (!session_coordinator_) {
1825 return absl::FailedPreconditionError("No session coordinator");
1826 }
1827 auto* session = session_coordinator_->GetActiveRomSession();
1828 if (!session) {
1829 return absl::FailedPreconditionError("No active ROM session");
1830 }
1831
1832 // Lazy asset loading can defer OverworldEditor::Load after replacement.
1833 // Refuse a changed image while its read-only canvas still owns ROM-backed
1834 // maps and textures, before changing bytes, dirty state or undo history.
1835 if (auto* overworld_editor = static_cast<OverworldEditor*>(
1836 session->editors.GetExistingEditor(EditorType::kOverworld))) {
1837 RETURN_IF_ERROR(overworld_editor->CheckRomReplacement(rom));
1838 }
1839
1840 // Reject invalid shared assets before invalidating live editor models and
1841 // their Undo histories. Rebuilding the old ROM on failure cannot recover
1842 // model-only edits. Keep this preflight CPU-only and leave lazy, unloaded
1843 // sessions lazy; editor-specific refresh failures still use rollback below.
1844 if (session->game_data_loaded || asset_load_mode_ != AssetLoadMode::kLazy) {
1845 zelda3::GameData replacement_data;
1846 zelda3::LoadOptions options;
1847 options.load_graphics = false;
1848 options.expand_rom = false;
1849 RETURN_IF_ERROR(zelda3::LoadGameData(rom, replacement_data, options));
1850 }
1851
1852 Rom previous_rom = session->rom;
1853 const std::string previous_filepath = session->filepath;
1854 const bool game_data_was_loaded = session->game_data_loaded;
1855 auto* dungeon_editor = static_cast<DungeonEditorV2*>(
1856 session->editors.GetExistingEditor(EditorType::kDungeon));
1857 const size_t dungeon_index = EditorTypeIndex(EditorType::kDungeon);
1858 const bool dungeon_was_initialized =
1859 session->editor_initialized[dungeon_index];
1860 const bool dungeon_assets_were_loaded =
1861 session->editor_assets_loaded[dungeon_index];
1862 auto* screen_editor = static_cast<ScreenEditor*>(
1863 session->editors.GetExistingEditor(EditorType::kScreen));
1864 const size_t screen_index = EditorTypeIndex(EditorType::kScreen);
1865 const bool screen_was_initialized = session->editor_initialized[screen_index];
1866 const bool screen_assets_were_loaded =
1867 session->editor_assets_loaded[screen_index];
1868 auto restore_dungeon_asset_state = [&]() {
1869 if (dungeon_editor) {
1870 session->editor_initialized[dungeon_index] =
1871 session->editor_initialized[dungeon_index] || dungeon_was_initialized;
1872 session->editor_assets_loaded[dungeon_index] =
1873 session->editor_assets_loaded[dungeon_index] ||
1874 dungeon_assets_were_loaded;
1875 }
1876 };
1877 auto restore_screen_asset_state = [&]() {
1878 if (screen_editor) {
1879 session->editor_initialized[screen_index] =
1880 session->editor_initialized[screen_index] || screen_was_initialized;
1881 session->editor_assets_loaded[screen_index] =
1882 session->editor_assets_loaded[screen_index] ||
1883 screen_assets_were_loaded;
1884 }
1885 };
1886
1887 session->editors.InvalidateScreenRomBackedState();
1888 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
1889 session->rom = std::move(rom);
1890 session->filepath = filepath;
1891 session->game_data.Clear();
1892 session->game_data.set_rom(&session->rom);
1893 session->editors.SetGameData(&session->game_data);
1894 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
1895 ResetAssetState(session);
1896 ConfigureSession(session);
1898 // Retain the prior baseline until the replacement and its assets succeed.
1899 auto load_status = LoadAssetsForMode();
1900 if (load_status.ok() && game_data_was_loaded && !session->game_data_loaded) {
1901 load_status = EnsureGameDataLoaded();
1902 }
1903 if (load_status.ok() && screen_editor && screen_assets_were_loaded) {
1904 load_status = screen_editor->RefreshRomBackedState();
1905 }
1906 if (load_status.ok()) {
1907 restore_screen_asset_state();
1908 }
1909 if (load_status.ok() && dungeon_editor) {
1910 load_status = dungeon_editor->RefreshRomBackedState();
1911 if (load_status.ok()) {
1912 restore_dungeon_asset_state();
1913 }
1914 }
1915 if (!load_status.ok()) {
1916 // Loading editors is fallible. Restore the prior ROM and rebuild its
1917 // assets so a failed reload never leaves a half-initialized live session.
1918 session->editors.InvalidateScreenRomBackedState();
1919 gfx::PaletteManager::Get().ReleaseSession(&session->game_data);
1920 session->rom = std::move(previous_rom);
1921 session->filepath = previous_filepath;
1922 session->game_data.Clear();
1923 session->game_data.set_rom(&session->rom);
1924 session->editors.SetGameData(&session->game_data);
1925 gfx::PaletteManager::Get().ActivateSession(&session->game_data);
1926 ResetAssetState(session);
1927 ConfigureSession(session);
1930 auto rollback_status = LoadAssetsForMode();
1931 if (game_data_was_loaded && !session->game_data_loaded) {
1932 auto game_data_status = EnsureGameDataLoaded();
1933 if (rollback_status.ok()) {
1934 rollback_status = game_data_status;
1935 }
1936 }
1937 if (screen_editor && screen_assets_were_loaded) {
1938 auto screen_status = screen_editor->RefreshRomBackedState();
1939 if (screen_status.ok()) {
1940 restore_screen_asset_state();
1941 }
1942 if (rollback_status.ok()) {
1943 rollback_status = screen_status;
1944 }
1945 } else {
1946 restore_screen_asset_state();
1947 }
1948 if (dungeon_editor) {
1949 auto dungeon_status = dungeon_editor->RefreshRomBackedState();
1950 if (dungeon_status.ok()) {
1951 restore_dungeon_asset_state();
1952 }
1953 if (rollback_status.ok()) {
1954 rollback_status = dungeon_status;
1955 }
1956 }
1958 if (!rollback_status.ok()) {
1959 return absl::InternalError(absl::StrFormat(
1960 "ROM reload failed (%s) and restoring prior assets failed (%s)",
1961 load_status.message(), rollback_status.message()));
1962 }
1963 return load_status;
1964 }
1967 return absl::OkStatus();
1968}
1969
1971 // Avoid a wall of onboarding toasts after load. Each subsequent load
1972 // advances to the next unseen, context-relevant tip.
1974 return;
1975 }
1976 Rom* rom = GetCurrentRom();
1977 if (rom && rom->is_loaded()) {
1981 return;
1982 }
1983 }
1985}
1986
1989 return SaveProjectAs();
1990 }
1991 if (!GetCurrentRom() || !GetCurrentRom()->is_loaded()) {
1992 return absl::FailedPreconditionError("No active ROM session");
1993 }
1995 return absl::OkStatus();
1996}
1997
1998} // namespace yaze::editor
static Json parse(const std::string &)
Definition json.h:36
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
auto filename() const
Definition rom.h:219
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:1036
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
static TimingManager & Get()
Definition timing.h:20
float GetElapsedTime() const
Get total elapsed time since first update.
Definition timing.h:70
static Flags & get()
Definition features.h:122
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
bool loaded() const
Check if the manifest has been loaded.
static absl::StatusOr< LyraTerrainDocument > Parse(std::string_view text, const LyraTerrainProfile &expected_profile)
DungeonEditorV2 - Simplified dungeon editor using component delegation.
std::optional< RomLoadOptionsOwner > rom_load_options_owner_
std::unique_ptr< SessionCoordinator > session_coordinator_
RomLifecycleManager rom_lifecycle_
void SwitchToEditor(EditorType editor_type, bool force_visible=false, bool from_dialog=false) override
bool RomLoadOptionsOwnerMatchesCurrentSession(const RomLoadOptionsOwner &owner) const
project::YazeProject current_project_
absl::Status LoadAssetsForMode(uint64_t loading_handle=0)
absl::Status CheckRomWritePolicy(const std::optional< std::string > &target_filename=std::nullopt)
Save the current ROM file.
Rom * GetCurrentRom() const override
absl::Status RetireNativeSaveTransfer(const std::string &state, const std::string &editing_source_path="")
void ResetAssetState(RomSession *session)
core::VersionManager * version_manager_
void SyncEditorContextForCategory(const std::string &category)
Editor * ResolveEditorForCategory(const std::string &category)
void FinishRawRomOpen(bool remembered_load_options, bool ask_for_load_options, bool is_first_time_rom_path)
std::optional< size_t > runtime_feature_flags_session_id_
absl::Status FinalizeNewProject(const std::string &project_name, const std::string &project_path=std::string())
absl::Status RecordCompletedNativeSave(const project::YazeProject &project, const std::string &saved_rom, bool save_as, bool rom_written=true)
FeatureHintManager feature_hint_manager_
auto GetCurrentEditorSet() const -> EditorSet *
absl::Status LoadProjectWithRom(bool allow_dialogs=true)
void HandleSessionRomLoaded(size_t index, Rom *rom)
WorkspaceWindowManager window_manager_
absl::Status SaveRomInternal(const std::optional< std::string > &save_as_filename)
std::optional< size_t > pending_project_open_previous_session_id_
std::optional< size_t > active_project_context_session_id_
RomLoadOptionsDialog rom_load_options_dialog_
bool ActivateExistingSessionForOpen(const std::string &filename)
void BindProjectContextToSession(RomSession *session, const project::YazeProject &project)
absl::Status OpenRomOrProjectInternal(const std::string &filename, bool allow_dialogs=true, LoadOptionsPrompt prompt=LoadOptionsPrompt::kAsk)
size_t GetCurrentSessionId() const
Stable workspace identity; unlike the UI index, it does not compact.
absl::Status ValidateProjectRomSelection(const std::string &rom_path)
void ConfigureSession(RomSession *session) override
std::unique_ptr< BackgroundCommandTask > active_project_build_
yaze::zelda3::Overworld * overworld() const
void ConfigureEditorDependencies(EditorSet *editor_set, Rom *rom, size_t session_id)
absl::StatusOr< const project::YazeProject * > PrepareActiveProjectContextForSave()
std::unique_ptr< ProjectManagementPanel > project_management_panel_
void RestoreProjectContextAfterFailedOpen(std::optional< size_t > previous_session_id)
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
void CloseRomLoadOptions(const char *reason)
absl::Status DiscardProvisionalSessionCreatedSince(size_t previous_session_count)
absl::Status EnsureEditorAssetsLoaded(EditorType type)
std::unique_ptr< PopupManager > popup_manager_
bool RestoreProjectContextForSession(RomSession *session)
void CaptureCurrentRomIdentity(bool preserve_editing_source=false)
std::unique_ptr< UICoordinator > ui_coordinator_
std::optional< PendingProjectRomSelection > pending_project_rom_selection_
absl::Status ReplaceActiveSessionRom(Rom &&rom, const std::string &filepath)
static EditorType GetEditorTypeFromCategory(const std::string &category)
static EditorExperimentPolicy GetExperimentPolicy(EditorType type)
Interface for editor classes.
Definition editor.h:252
virtual absl::Status BeginSaveTransaction()
Definition editor.h:279
EditorType type() const
Definition editor.h:355
bool MaybeShow(std::string_view id)
static std::vector< std::string > GetDefaultWindows(EditorType type)
Main UI class for editing overworld maps in A Link to the Past.
absl::Status SaveRom(Rom *rom, const std::filesystem::path &collision_source={})
absl::Status LoadRom(Rom *rom, const std::string &filename)
absl::Status SaveRomAs(Rom *rom, const std::string &filename)
Manages ROM and project persistence state.
void SetProjectContext(const project::YazeProject *project)
absl::Status CheckRomOpenPolicy(Rom *rom)
Validate that the loaded ROM is a safe project target to open/edit.
const std::vector< core::WriteConflict > & pending_write_conflicts() const
void SetPendingWriteConflicts(std::vector< core::WriteConflict > conflicts)
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.
void Open(Rom *rom, const std::string &rom_filename)
Open the dialog after ROM detection.
void OpenForReview(Rom *rom, const core::FeatureFlags::Flags &active, const std::string &preset_name, bool remembered)
Reopen the dialog for a ROM that is already loaded.
void SetApplyGuard(std::function< bool()> guard)
static std::optional< RememberedChoice > DecodeRememberedChoice(std::string_view encoded)
static std::string RomChoiceKey(const std::string &rom_path)
void SetCancelCallback(std::function< void()> callback)
Set callback for Cancel or the title-bar close button.
static void ApplyToFeatureFlags(const LoadOptions &options, core::FeatureFlags::Flags &flags)
static std::string EncodeRememberedChoice(const LoadOptions &options, int zso_version_byte)
void SetConfirmCallback(std::function< void(const LoadOptions &)> callback)
Set callback for when options are confirmed.
The ScreenEditor class allows the user to edit a variety of screens in the game or create a custom me...
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)
Manages user preferences and settings persistence.
void EnableFileBrowser(const std::string &category, const std::string &root_path="")
std::vector< WindowDescriptor > GetWindowsInCategory(size_t session_id, const std::string &category) const
void SetEditorResolver(std::function< Editor *(const std::string &)> resolver)
void RestoreVisibilityState(size_t session_id, const std::unordered_map< std::string, bool > &state, bool publish_events=false)
Restore panel visibility state from persistence.
void SetPanelBrowserCategoryWidthChangedCallback(std::function< void(float)> cb)
void SetSidebarStateChangedCallback(std::function< void(bool, bool)> cb)
static constexpr const char * kDashboardCategory
void SetCategoryChangedCallback(std::function< void(const std::string &)> cb)
void SetFileBrowserPath(const std::string &category, const std::string &path)
void SetOnWindowCategorySelectedCallback(std::function< void(const std::string &)> callback)
void SetOnWindowClickedCallback(std::function< void(const std::string &)> callback)
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void SetSidePanelWidthChangedCallback(std::function< void(float)> cb)
void SetFileClickedCallback(std::function< void(const std::string &category, const std::string &path)> callback)
ScopedEditorSaveTransactions & operator=(const ScopedEditorSaveTransactions &)=delete
void ActivateSession(zelda3::GameData *game_data)
Select the palette state for a ROM session without resnapshotting it.
static PaletteManager & Get()
Get the singleton instance.
void ReleaseSession(const zelda3::GameData *game_data)
Forget all palette tracking for a closing or reloaded ROM session.
static RecentFilesManager & GetInstance()
Definition project.h:632
void SetCurrentRom(Rom *rom)
static TestManager & Get()
static void ShowOpenFileDialogAsync(const FileDialogOptions &options, std::function< void(const std::string &)> callback)
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static absl::StatusOr< std::filesystem::path > GetExecutablePath()
static const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
static DrawRoutineRegistry & Get()
static OverworldVersion GetVersion(const Rom &rom)
Detect ROM version from ASM marker byte.
static uint8_t GetAsmVersion(const Rom &rom)
Get raw ASM version byte from ROM.
bool lyra_a0_terrain_writable() const
Definition overworld.h:457
auto is_loaded() const
Definition overworld.h:836
int main(int argc, char **argv)
Definition emu.cc:43
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
constexpr char kOverworldExpandedPtrHigh[]
constexpr char kOverworldEntrancePosExpanded[]
constexpr char kExpandedMusicHook[]
constexpr char kExpandedMusicMain[]
constexpr char kOverworldExpandedPtrMagic[]
constexpr char kExpandedMessageEnd[]
constexpr char kOverworldExpandedPtrMarker[]
constexpr char kOverworldEntranceMapExpanded[]
constexpr char kOverworldEntranceFlagExpanded[]
constexpr char kExpandedMessageStart[]
constexpr char kOverworldEntranceIdExpanded[]
constexpr char kOverworldExpandedPtrLow[]
constexpr char kExpandedMusicAux[]
absl::Status RetireNativeSaveContext(const NativeSaveContext &, const std::string &)
absl::StatusOr< std::string > ComputeSourceArtifactFileSha256(const fs::path &path)
absl::StatusOr< NativeSaveContext > GetNativeSaveContext(const std::string &, const std::string &)
absl::StatusOr< NativeSaveReceipt > PublishNativeSaveReceipt(const NativeSaveReceiptRequest &)
void SetEditorWindowContext(const std::string &category, Editor *editor)
void SetBuildWorkflowStatus(const ProjectWorkflowStatus &status)
void SetBuildWorkflowLog(const std::string &output)
void SetRunWorkflowStatus(const ProjectWorkflowStatus &status)
void SetCurrentProject(::yaze::project::YazeProject *project)
Set the current project instance.
constexpr const char * kZScreamZso
constexpr const char * kZScreamWelcome
constexpr const char * kZScreamProjects
constexpr const char * kSaveAs
constexpr const char * kWriteConflictWarning
std::map< std::string, std::string > ProjectSaveSources(const project::YazeProject &project)
bool HasAnyOverride(const core::RomAddressOverrides &overrides, std::initializer_list< const char * > keys)
bool SeedLegacyTrackObjectMapping(project::YazeProject *project, std::string *warning)
std::optional< RomLoadOptionsDialog::RememberedChoice > FindRememberedRomLoadChoice(const UserSettings &settings, const Rom *rom)
std::vector< std::string > ValidateRomAddressOverrides(const core::RomAddressOverrides &overrides, const Rom &rom)
Editors are the view controllers for the application.
std::string ComputeRomFileBytesSha256(const Rom &rom)
size_t EditorTypeIndex(EditorType type)
Definition editor.h:242
void PostOverlayCommand(const char *command)
DiffSummary ComputeDiffRanges(const std::vector< uint8_t > &before, const std::vector< uint8_t > &after)
Definition rom_diff.cc:11
FileDialogOptions MakeRomFileDialogOptions(bool include_all_files)
Definition file_util.cc:127
absl::Status SaveGfxGroups(Rom &rom, const GameData &data)
Definition game_data.cc:362
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
constexpr int kExpandedPtrTableMarker
Definition overworld.h:187
@ kVanilla
0xFF in ROM, no ZScream ASM applied
constexpr uint8_t kExpandedPtrTableMagic
Definition overworld.h:188
constexpr int kOverworldEntranceExpandedFlagPos
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::vector< std::string > binding_errors
std::optional< NativeSaveContext > context
std::vector< NativeSaveArtifact > artifacts
std::unordered_map< std::string, uint32_t > addresses
std::optional< uint32_t > GetAddress(const std::string &key) const
Represents a single session, containing a ROM and its associated editors.
core::FeatureFlags::Flags feature_flags
std::optional< project::YazeProject > project_context
std::unordered_map< std::string, std::string > rom_load_choices
std::unordered_map< std::string, std::unordered_map< std::string, bool > > panel_visibility_state
std::vector< std::string > annotations
Definition project.h:140
RomWritePolicy write_policy
Definition project.h:229
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
std::string rom_backup_folder
Definition project.h:309
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:358
std::string custom_objects_folder
Definition project.h:330
absl::Status ValidateSavedSourceBindings(const std::string &content) const
Definition project.cc:804
bool project_opened() const
Definition project.h:538
std::string sprite_catalog_file
Definition project.h:334
std::string git_repository
Definition project.h:390
core::HackManifest hack_manifest
Definition project.h:376
std::string cutscene_shots
Definition project.h:321
std::vector< std::string > additional_roms
Definition project.h:310
std::string lyra_terrain_file
Definition project.h:342
std::string terrain_catalog_file
Definition project.h:339
std::string labels_filename
Definition project.h:317
std::string hack_manifest_file
Definition project.h:332
std::string GetDisplayName() const
Definition project.cc:2648
WorkspaceSettings workspace_settings
Definition project.h:362
std::vector< SpriteAssetBinding > sprite_assets
Definition project.h:336
std::string custom_collision_json
Definition project.h:328
std::string palette_catalog_file
Definition project.h:355
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
std::string overworld_graphics_preview
Definition project.h:323
std::string sprite_source_root
Definition project.h:335
std::string dungeon_tile_patterns
Definition project.h:325
core::FeatureFlags::Flags feature_flags
Definition project.h:361
core::RomAddressOverrides rom_address_overrides
Definition project.h:367
std::string symbols_filename
Definition project.h:318