10#include <initializer_list>
20#include <TargetConditionals.h>
23#include "absl/status/status.h"
24#include "absl/strings/match.h"
25#include "absl/strings/str_format.h"
65#ifdef YAZE_ENABLE_TESTING
75 std::map<std::string, std::string> sources;
76 const auto add = [&](
const std::string& role,
const std::string& path) {
81 add(
"dependency.additional_roms." + std::to_string(i),
87 add(
"dependency.overworld_graphics_preview",
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()
106 add(
"dependency.object_" + std::to_string(
id) +
"_" + std::to_string(i),
113 const auto prefix =
"dependency.sprite_" + std::to_string(i);
115 add(prefix +
"_zsm", asset.zsm_path);
116 if (!asset.source_path.empty())
117 add(prefix +
"_source", (std::filesystem::path(project.
GetAbsolutePath(
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);
139 for (
auto it = editors_.rbegin(); it != editors_.rend(); ++it) {
140 (*it)->RollbackSaveTransaction();
146 if (editor ==
nullptr) {
147 return absl::OkStatus();
150 editors_.push_back(editor);
151 return absl::OkStatus();
155 for (
Editor* editor : editors_) {
156 editor->CommitSaveTransaction();
163 bool committed_ =
false;
167 std::initializer_list<const char*> keys) {
168 for (
const char* key : keys) {
184 std::string* warning) {
185 if (project ==
nullptr) {
196 const auto& defaults =
199 if (defaults.empty()) {
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).",
215 std::vector<std::string> warnings;
220 const auto rom_size = rom.
size();
221 auto warn = [&](
const std::string& message) {
222 warnings.push_back(message);
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));
233 for (
const auto& [key, value] : overrides.
addresses) {
234 check_range(key, value, 1);
241 constexpr uint32_t kExpandedMessageStartDefault = 0x178000;
242 constexpr uint32_t kExpandedMessageEndDefault = 0x17FFFF;
243 const uint32_t start =
245 .value_or(kExpandedMessageStartDefault);
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,
261 if (*hook < rom_size) {
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,
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]));
307 const uint32_t flag_addr =
311 check_range(
"overworld_entrance_flag_expanded", flag_addr, 1);
312 if (flag_addr < rom_size && rom.
data()[flag_addr] == 0xB8) {
314 absl::StrFormat(
"Overworld entrance flag at 0x%X is still 0xB8 "
315 "(vanilla); expanded entrance tables may be ignored",
325std::optional<RomLoadOptionsDialog::RememberedChoice>
327 if (rom ==
nullptr || !rom->
is_loaded()) {
333 if (it == choices.end()) {
337 if (!choice.has_value() ||
338 choice->zso_version_byte !=
361 session->feature_flags = flags;
371 if (!rom_key.empty()) {
378 choices.erase(rom_key);
405 ?
"ROM load options applied and remembered for this file"
406 :
"ROM load options applied",
412 LOG_INFO(
"EditorManager",
"ROM load options applied: preset=%s",
469 absl::StrFormat(
"ROM load options closed without changes because %s",
481 if (!choice.has_value()) {
508 bool ask_for_load_options,
509 bool is_first_time_rom_path) {
520 if (remembered_load_options) {
527 if (rom ==
nullptr || !rom->
is_loaded()) {
534 "This project's save options come from its project file",
538 const auto remembered = FindRememberedRomLoadChoice(
user_settings_, rom);
541 remembered.has_value() ? remembered->options.selected_preset
543 remembered.has_value());
552 [
this](
bool visible,
bool expanded) {
564 [
this](
float width) {
571 const std::string& category) {
572 if (category.empty() ||
581 if (it != prefs.panel_visibility_state.end()) {
591 bool any_visible =
false;
592 for (
const auto& desc :
594 if (desc.visibility_flag && *desc.visibility_flag) {
614 [
this](
const std::string& category) ->
Editor* {
621 [
this](
const std::string& category) {
629 [
this](
const std::string& category) {
634 if (category ==
"Agent") {
635#ifdef YAZE_BUILD_AGENT_UI
650 [
this](
const std::string& category,
const std::string& path) {
651 if (category ==
"Assembly") {
653 editor_set->ChangeActiveAssemblyFile(path);
661 auto load_from_path = [
this](
const std::string& file_name) -> absl::Status {
662 if (file_name.empty()) {
663 return absl::OkStatus();
666 return absl::OkStatus();
670 if (absl::EndsWith(file_name,
".yaze") ||
671 absl::EndsWith(file_name,
".zsproj") ||
672 absl::EndsWith(file_name,
".yazeproj")) {
678 if (!path_status.ok()) {
688 const std::optional<size_t> previous_session_id =
690 const size_t previous_session_count =
693 std::move(temp_rom), file_name);
694 if (!session_or.ok()) {
695 return session_or.status();
708 const bool remembered_load_options =
715#ifdef YAZE_ENABLE_TESTING
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) ==
725 if (!asset_status.ok()) {
726 auto rollback_status =
729 if (!rollback_status.ok()) {
730 return absl::InternalError(
731 absl::StrFormat(
"%s; session rollback failed: %s",
732 asset_status.message(), rollback_status.message()));
737 manager.AddFile(file_name);
742 remembered_load_options,
744 is_first_time_rom_path);
747 return absl::OkStatus();
750#if defined(__APPLE__) && TARGET_OS_IOS == 1
757 return absl::OkStatus();
761 return load_from_path(file_name);
766 const std::optional<std::string>& save_as_filename) {
769 if (!current_rom || !current_editor_set) {
770 return absl::FailedPreconditionError(
"No ROM or editor set loaded");
775 return absl::FailedPreconditionError(
776 "Publish project graphics before Save ROM (Edit / Import Project "
783 const auto lyra_layout = current_rom->InspectLyraLayout();
784 const bool lyra_terrain_save =
786 lyra_layout.a0_terrain_write_supported;
787 if (!lyra_terrain_save) {
789 }
else if (!save_as_filename.has_value()) {
790 return absl::FailedPreconditionError(
"Lyra A0 terrain requires Save As");
792 if (save_as_filename.has_value() && save_as_filename->empty()) {
793 return absl::InvalidArgumentError(
"No filename provided for save as");
795 if (save_as_filename.has_value()) {
812 struct LifecycleProjectContextGuard {
815 ~LifecycleProjectContextGuard() {
831 const bool pending_palettes =
832 save_session !=
nullptr &&
833 palette_manager.HasUnsavedChanges(&save_session->game_data);
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.");
845 const bool pending_gfx_groups =
846 save_session !=
nullptr && save_session->HasPendingGfxGroupChanges();
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.");
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.");
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())) {
871 for (
const auto& write : plan.writes) {
872 predicted_screen_write_ranges.emplace_back(
873 write.pc_address, write.pc_address + write.data.size());
879 return absl::CancelledError(
"Save pending confirmation");
884 if (lyra_terrain_save) {
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");
899 return absl::FailedPreconditionError(
900 "Open Lyra A0 terrain before Save As");
906 session->filepath = *save_as_filename;
907 session->backup_restore_pending =
false;
911 recent.AddFile(*save_as_filename);
923 const auto experiment_policy =
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",
940 force_experimental_backup,
959 const auto predicted_dungeon_write_ranges =
960 current_editor_set->CollectDungeonWriteRanges();
963 ScopedEditorSaveTransactions editor_transactions;
965 auto save_editor = [&editor_transactions](
Editor* editor) -> absl::Status {
966 if (editor ==
nullptr) {
967 return absl::OkStatus();
970 return editor->Save();
976 if (screen_editor !=
nullptr && (screen_editor->IsRomBackedStateValid() ||
977 screen_editor->HasPendingScreenChanges())) {
995 if (pending_palettes &&
996 palette_manager.HasUnsavedChanges(&save_session->game_data)) {
1029 std::vector<std::pair<uint32_t, uint32_t>> write_ranges;
1030 bool diff_computed =
false;
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();
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()));
1043 diff_computed =
true;
1045 current_rom->vector());
1046 if (!diff.ranges.empty()) {
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);
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());
1064 if (!write_ranges.empty()) {
1067 if (!conflicts.empty()) {
1074 "Save paused: %zu write conflict(s) with ASM hooks",
1077 return absl::CancelledError(
"Write conflict confirmation required");
1090 std::filesystem::path collision_source;
1091 if (!save_as_filename.has_value() && save_project->
project_opened() &&
1094 std::error_code same_ec;
1095 if (std::filesystem::equivalent(
1097 current_rom->filename(), same_ec)) {
1108 save_as_filename.has_value()
1111 if (save_status.ok()) {
1112 editor_transactions.Commit();
1113 rom_transaction.
Commit();
1117 session->backup_restore_pending =
false;
1123 if (save_as_filename.has_value()) {
1126 session->filepath = *save_as_filename;
1131 manager.AddFile(*save_as_filename);
1138 *save_project, save_as_filename.value_or(current_rom->filename()),
1139 save_as_filename.has_value());
1146 bool save_as,
bool rom_written) {
1147#if defined(__EMSCRIPTEN__) || !defined(YAZE_WITH_JSON)
1149 return absl::OkStatus();
1152 const auto finish = [&]() -> absl::Status {
1153 std::error_code error;
1154 const auto owner = std::filesystem::weakly_canonical(
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());
1165 return absl::FailedPreconditionError(
"Saved ROM session is unavailable");
1166 const auto rom_path = std::filesystem::canonical(saved_rom, error);
1168 return absl::FailedPreconditionError(
"Cannot resolve saved ROM");
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 "
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 "
1192 request.
artifacts.push_back({
"rom", rom_path, on_disk});
1196 static const auto producer =
1197 []() -> absl::StatusOr<std::pair<std::filesystem::path, std::string>> {
1202 return std::make_pair(std::move(executable), std::move(hash));
1206 {
"producer", producer->first, producer->second});
1208 const auto bind = [&](
const std::string& role,
const std::string& path) {
1212 role +
": " + std::string(digest.status().message()));
1214 request.
artifacts.push_back({role, path, *digest});
1219 const auto live_sources = ProjectSaveSources(project);
1220 std::ifstream descriptor(project.
filepath, std::ios::binary);
1222 const std::string bytes{std::istreambuf_iterator<char>(descriptor), {}};
1224 if (!binding_status.ok())
1226 std::string(binding_status.message()));
1228 for (
const auto& [role, path] : live_sources)
1232 if (asset.source_path.empty())
1234 const auto role =
"dependency.sprite_" + std::to_string(i) +
"_source";
1235 const auto binding = std::find_if(
1237 [&](
const auto& artifact) { return artifact.role == role; });
1240 binding->expected_sha256 != asset.source_sha256))
1241 request.
binding_errors.push_back(role +
": imported source is stale");
1244#ifdef YAZE_WITH_JSON
1250 std::ifstream input(path);
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>())
1259 }
catch (
const std::exception& exception) {
1261 std::string(
"Cannot bind graphics preview transfers: ") +
1268 "Graphics preview dependency binding requires JSON support");
1274 request.
binding_errors.push_back(
"Terrain document is not loaded");
1276 auto context = project;
1277 const auto valid =
overworld->lyra_canvas_.ValidateSave(
1281 std::ifstream input(
1283 const std::string bytes{std::istreambuf_iterator<char>(input), {}};
1284 const auto* live =
overworld->lyra_canvas_.document();
1287 if (!saved.ok() || saved->Serialize() != live->Serialize())
1289 "Saved terrain differs from the live document");
1297 "More than 256 source bindings; incomplete");
1304 " additional binding errors");
1307 if (diagnostic.size() > 1024) {
1310 (
static_cast<unsigned char>(diagnostic[end]) & 0xC0) == 0x80)
1312 diagnostic = diagnostic.substr(0, end) +
" [truncated]";
1315 const auto descriptor = std::find_if(
1317 [](
const auto& artifact) { return artifact.role ==
"project"; });
1318 const std::string project_path =
1321 : std::filesystem::absolute(descriptor->path)
1330 " saved; build inputs are incomplete: " +
1334 return absl::OkStatus();
1336 absl::Status status;
1339 }
catch (
const std::exception& exception) {
1340 status = absl::InternalError(exception.what());
1343 return absl::FailedPreconditionError(
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()));
1353 session->native_save_completion_pending =
false;
1354 session->native_save_completion_save_as =
false;
1364 session && session->rom_baseline &&
1365 !session->rom_baseline->editing_source_path.empty()) {
1366 return session->rom_baseline->editing_source_path;
1374 const std::string& state,
const std::string& editing_source_path) {
1375#if defined(__EMSCRIPTEN__) || !defined(YAZE_WITH_JSON)
1376 return absl::OkStatus();
1378 const auto origin = editing_source_path.empty()
1380 : editing_source_path;
1382 return absl::OkStatus();
1383 std::string project_path;
1384 std::error_code error;
1391 if (error == std::errc::no_such_file_or_directory) {
1395 return absl::FailedPreconditionError(
1396 "Cannot resolve native-save project context: " + error.message());
1405 const auto replaced_project_session_id =
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) {
1427 if (project_rom_path.empty()) {
1428 if (!allow_dialogs) {
1429 return absl::FailedPreconditionError(
"Project has no ROM configured");
1433 "Project has no ROM file configured. Please select a ROM.",
1435#if defined(__APPLE__) && TARGET_OS_IOS == 1
1443 auto bundle_parent =
1445 if (bundle_parent.extension() ==
".yazeproj") {
1447 "ROM is downloading from iCloud. Reopen the project in a few "
1450 return absl::UnavailableError(
1451 "Project ROM is not available from iCloud yet");
1455 const std::optional<size_t> target_session_id =
1460 [
this, target_project_filepath, target_session_id,
1461 selection_generation](
const std::string& rom_path) {
1465 toast_manager_.Show(
1466 "ROM selection ignored because the active project changed",
1467 ToastType::kWarning);
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) {
1479 if (rom_path.empty()) {
1480 restore_after_failure();
1485 if (!path_status.ok()) {
1488 restore_after_failure();
1492 if (!selection_status.ok()) {
1494 absl::StrFormat(
"Failed to update project ROM: %s",
1495 selection_status.message()),
1497 restore_after_failure();
1503 absl::StrFormat(
"Failed to load project ROM: %s",
1506 restore_after_failure();
1509 return absl::OkStatus();
1513 if (rom_path.empty()) {
1514 return absl::CancelledError(
"Project ROM selection cancelled");
1517 project_rom_path = rom_path;
1518 persist_selected_rom =
true;
1522 auto backing_status =
1524 if (!backing_status.ok()) {
1525 abandon_selected_rom();
1526 return backing_status;
1532 if (!load_status.ok()) {
1533 if (!allow_dialogs) {
1534 abandon_selected_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();
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 "
1555 return absl::UnavailableError(
1556 "Project ROM is still downloading from iCloud");
1560 const std::optional<size_t> target_session_id =
1565 [
this, target_project_filepath, target_session_id,
1566 selection_generation](
const std::string& rom_path) {
1570 toast_manager_.Show(
1571 "ROM selection ignored because the active project changed",
1572 ToastType::kWarning);
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) {
1584 if (rom_path.empty()) {
1585 restore_after_failure();
1590 if (!path_status.ok()) {
1593 restore_after_failure();
1597 if (!selection_status.ok()) {
1599 absl::StrFormat(
"Failed to update project ROM: %s",
1600 selection_status.message()),
1602 restore_after_failure();
1608 absl::StrFormat(
"Failed to load project ROM: %s",
1611 restore_after_failure();
1614 return absl::OkStatus();
1618 if (rom_path.empty()) {
1619 return absl::CancelledError(
"Project ROM selection cancelled");
1622 project_rom_path = rom_path;
1623 persist_selected_rom =
true;
1629 if (!policy_status.ok()) {
1630 abandon_selected_rom();
1631 return policy_status;
1634 if (persist_selected_rom) {
1638 const size_t previous_session_count =
1641 std::move(temp_rom), project_rom_path);
1642 if (!session_or.ok()) {
1643 abandon_selected_rom();
1644 return session_or.status();
1658 "Project has custom object data but 'enable_custom_objects' was "
1659 "disabled. Enabling at runtime.");
1663 std::string legacy_mapping_warning;
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 "
1680 "Feature flags applied: kEnableCustomObjects=%s, "
1681 "custom_objects_folder='%s', custom_object_files=%zu entries",
1699 "Project ROM hash mismatch detected. Check ROM Identity settings.",
1702 auto warnings = ValidateRomAddressOverrides(
1704 if (!warnings.empty()) {
1705 for (
const auto& warning : warnings) {
1706 LOG_WARN(
"EditorManager",
"%s", warning.c_str());
1716#ifdef YAZE_ENABLE_TESTING
1717 LOG_DEBUG(
"EditorManager",
"Setting ROM in TestManager - %p ('%s')",
1725 const std::string absolute_code_folder =
1728#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
1729 editor_set->OpenAssemblyFolder(absolute_code_folder);
1740 if (!asset_status.ok()) {
1741 abandon_selected_rom();
1742 auto rollback_status =
1744 if (!rollback_status.ok()) {
1745 return absl::InternalError(
1746 absl::StrFormat(
"%s; session rollback failed: %s",
1747 asset_status.message(), rollback_status.message()));
1749 return asset_status;
1752 if (persist_selected_rom) {
1754 if (!save_status.ok()) {
1755 abandon_selected_rom();
1756 auto rollback_status =
1758 if (!rollback_status.ok()) {
1759 return absl::InternalError(
1760 absl::StrFormat(
"%s; session rollback failed: %s",
1761 save_status.message(), rollback_status.message()));
1811 if (replaced_project_session_id.has_value() &&
1814 editors->DiscardPlanningProjectDraft();
1819 return absl::OkStatus();
1823 Rom&& rom,
const std::string& filepath) {
1825 return absl::FailedPreconditionError(
"No session coordinator");
1829 return absl::FailedPreconditionError(
"No active ROM session");
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;
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];
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;
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;
1887 session->editors.InvalidateScreenRomBackedState();
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);
1900 if (load_status.ok() && game_data_was_loaded && !session->game_data_loaded) {
1903 if (load_status.ok() && screen_editor && screen_assets_were_loaded) {
1904 load_status = screen_editor->RefreshRomBackedState();
1906 if (load_status.ok()) {
1907 restore_screen_asset_state();
1909 if (load_status.ok() && dungeon_editor) {
1910 load_status = dungeon_editor->RefreshRomBackedState();
1911 if (load_status.ok()) {
1912 restore_dungeon_asset_state();
1915 if (!load_status.ok()) {
1918 session->editors.InvalidateScreenRomBackedState();
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);
1931 if (game_data_was_loaded && !session->game_data_loaded) {
1933 if (rollback_status.ok()) {
1934 rollback_status = game_data_status;
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();
1942 if (rollback_status.ok()) {
1943 rollback_status = screen_status;
1946 restore_screen_asset_state();
1948 if (dungeon_editor) {
1949 auto dungeon_status = dungeon_editor->RefreshRomBackedState();
1950 if (dungeon_status.ok()) {
1951 restore_dungeon_asset_state();
1953 if (rollback_status.ok()) {
1954 rollback_status = dungeon_status;
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()));
1967 return absl::OkStatus();
1992 return absl::FailedPreconditionError(
"No active ROM session");
1995 return absl::OkStatus();
static Json parse(const std::string &)
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::StatusOr< uint8_t > ReadByte(int offset) const
static TimingManager & Get()
float GetElapsedTime() const
Get total elapsed time since first update.
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)
bool AdoptExistingRepository()
DungeonEditorV2 - Simplified dungeon editor using component delegation.
std::optional< RomLoadOptionsOwner > rom_load_options_owner_
absl::Status SaveProjectAs()
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)
float settings_dirty_timestamp_
absl::Status CheckRomWritePolicy(const std::optional< std::string > &target_filename=std::nullopt)
Save the current ROM file.
bool active_project_build_reported_
absl::Status EnsureGameDataLoaded()
bool pending_project_open_transition_
void BindRomLoadOptionsToCurrentSession()
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())
void SetupDialogCallbacks()
absl::Status RecordCompletedNativeSave(const project::YazeProject &project, const std::string &saved_rom, bool save_as, bool rom_written=true)
ToastManager toast_manager_
FeatureHintManager feature_hint_manager_
absl::Status SaveCurrentSessionAs()
auto GetCurrentEditorSet() const -> EditorSet *
uint64_t project_rom_selection_generation_
absl::Status LoadProjectWithRom(bool allow_dialogs=true)
void HandleSessionRomLoaded(size_t index, Rom *rom)
void RestoreCurrentRomIdentity()
WorkspaceWindowManager window_manager_
bool show_rom_load_options_
absl::Status SaveRomInternal(const std::optional< std::string > &save_as_filename)
void MaybeEmitZScreamRomLoadHints()
std::optional< size_t > pending_project_open_previous_session_id_
void CaptureActiveProjectContext()
std::string NativeSaveEditingSourcePath() const
std::optional< size_t > active_project_context_session_id_
RomLoadOptionsDialog rom_load_options_dialog_
void ShowRomLoadOptions()
bool RomLoadOptionsMatchCurrentSession() const
bool ActivateExistingSessionForOpen(const std::string &filename)
void BindProjectContextToSession(RomSession *session, const project::YazeProject &project)
bool ApplyRememberedRomLoadChoice()
void ApplyCurrentProjectRuntimeContext()
void RefreshHackWorkflowBackend()
void SetupSidebarCallbacks()
size_t GetCurrentSessionIndex() const
absl::Status OpenRomOrProjectInternal(const std::string &filename, bool allow_dialogs=true, LoadOptionsPrompt prompt=LoadOptionsPrompt::kAsk)
void ShowPostLoadSurface()
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
bool IsRomHashMismatch() const
void RefreshResourceLabelProvider()
std::unique_ptr< BackgroundCommandTask > active_project_build_
UserSettings user_settings_
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)
void AnnounceRememberedRomLoadChoice()
absl::Status CheckOracleRomSafetyPreSave(Rom *rom)
absl::Status LoadRomInternal()
void CloseRomLoadOptions(const char *reason)
AssetLoadMode asset_load_mode_
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_
RomFileManager rom_file_manager_
void CaptureRuntimeFeatureFlags()
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.
virtual absl::Status BeginSaveTransaction()
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)
bool ShouldBypassWriteConflict() const
void CancelRomWriteConfirm()
absl::Status CheckRomOpenPolicy(Rom *rom)
Validate that the loaded ROM is a safe project target to open/edit.
void ConsumeWriteConflictBypass()
const std::vector< core::WriteConflict > & pending_write_conflicts() const
void SetPendingWriteConflicts(std::vector< core::WriteConflict > conflicts)
bool IsRomWriteConfirmPending() const
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.
const std::string & rom_filename() const
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.
int zso_version_byte() const
The ScreenEditor class allows the user to edit a variety of screens in the game or create a custom me...
void ClearProjectWorkflowStatus()
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="")
size_t GetActiveSessionId() const
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
absl::Status Begin(Editor *editor)
ScopedEditorSaveTransactions()=default
~ScopedEditorSaveTransactions()
std::vector< Editor * > editors_
ScopedEditorSaveTransactions(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()
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 const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
void RefreshFeatureFlagMappings()
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
int main(int argc, char **argv)
#define LOG_DEBUG(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
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 ClearWorkflowHistory()
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
std::map< std::string, std::string > ProjectSaveSources(const project::YazeProject &project)
bool ProjectUsesCustomObjects(const project::YazeProject &project)
bool HasAnyOverride(const core::RomAddressOverrides &overrides, std::initializer_list< const char * > keys)
constexpr int kTrackCustomObjectId
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)
DiffSummary ComputeDiffRanges(const std::vector< uint8_t > &before, const std::vector< uint8_t > &after)
FileDialogOptions MakeRomFileDialogOptions(bool include_all_files)
absl::Status SaveGfxGroups(Rom &rom, const GameData &data)
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
constexpr int kExpandedPtrTableMarker
@ kVanilla
0xFF in ROM, no ZScream ASM applied
constexpr uint8_t kExpandedPtrTableMagic
constexpr int kOverworldEntranceExpandedFlagPos
#define RETURN_IF_ERROR(expr)
bool kEnableCustomObjects
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::filesystem::path receipt_path
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
std::string selected_preset
Represents a single session, containing a ROM and its associated editors.
core::FeatureFlags::Flags feature_flags
std::optional< project::YazeProject > project_context
bool show_experimental_editors
std::unordered_map< std::string, std::string > rom_load_choices
float sidebar_panel_width
std::string sidebar_active_category
std::unordered_map< std::string, std::unordered_map< std::string, bool > > panel_visibility_state
float panel_browser_category_width
std::vector< std::string > annotations
int backup_keep_daily_days
int backup_retention_count
Modern project structure with comprehensive settings consolidation.
std::string rom_backup_folder
std::unordered_map< int, std::vector< std::string > > custom_object_files
std::string custom_objects_folder
absl::Status ValidateSavedSourceBindings(const std::string &content) const
bool project_opened() const
std::string sprite_catalog_file
std::string git_repository
core::HackManifest hack_manifest
std::string cutscene_shots
std::vector< std::string > additional_roms
std::string lyra_terrain_file
std::string terrain_catalog_file
std::string labels_filename
std::string hack_manifest_file
std::string GetDisplayName() const
WorkspaceSettings workspace_settings
std::vector< SpriteAssetBinding > sprite_assets
std::string custom_collision_json
std::string palette_catalog_file
std::string GetAbsolutePath(const std::string &relative_path) const
std::string overworld_graphics_preview
std::string sprite_source_root
std::string dungeon_tile_patterns
core::FeatureFlags::Flags feature_flags
core::RomAddressOverrides rom_address_overrides
std::string symbols_filename