yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
minecart_track_editor_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <charconv>
5#include <filesystem>
6#include <fstream>
7#include <iterator>
8#include <memory>
9#include <system_error>
10#include <utility>
11#include <vector>
12
13#include "absl/status/status.h"
14#include "absl/strings/ascii.h"
15#include "absl/strings/match.h"
16#include "absl/strings/str_format.h"
17#include "absl/strings/str_split.h"
19#include "imgui/imgui.h"
20#include "imgui/misc/cpp/imgui_stdlib.h"
21#include "util/i18n/tr.h"
22#include "util/macro.h"
23
24#include "app/gui/core/icons.h"
25#include "app/gui/core/input.h"
27#include "util/log.h"
30
31namespace yaze::editor {
32
33namespace {
34constexpr int kTrackSlotCount = static_cast<int>(kMinecartTrackSlotCount);
35constexpr int kDefaultTrackRoom = 0x89;
36constexpr int kDefaultTrackX = 0x1300;
37constexpr int kDefaultTrackY = 0x1100;
38#if defined(__EMSCRIPTEN__)
39constexpr bool kSourcePublishingAvailable = false;
40#else
41constexpr bool kSourcePublishingAvailable = true;
42#endif
43
45 .subject = "minecart track source",
46 .published_file = "published minecart track source",
47};
48
49std::optional<core::MinecartTrackLayout::Source> ProjectSourceIdentity(
50 const project::YazeProject* project) {
51 if (project == nullptr || !project->hack_manifest.loaded()) {
52 return std::nullopt;
53 }
55}
56
57bool IsStrictDescendant(const std::filesystem::path& path,
58 const std::filesystem::path& root) {
59 auto path_it = path.begin();
60 auto root_it = root.begin();
61 for (; root_it != root.end(); ++root_it, ++path_it) {
62 if (path_it == path.end() || *path_it != *root_it) {
63 return false;
64 }
65 }
66 return path_it != path.end();
67}
68
69absl::StatusOr<std::string> ReadSourceFile(const std::filesystem::path& path) {
70 std::ifstream input(path, std::ios::binary);
71 if (!input.is_open()) {
72 return absl::NotFoundError(
73 absl::StrFormat("Could not open minecart source: %s", path.string()));
74 }
75 std::string content{std::istreambuf_iterator<char>(input),
76 std::istreambuf_iterator<char>()};
77 if (!input.good() && !input.eof()) {
78 return absl::DataLossError(
79 absl::StrFormat("Could not read minecart source: %s", path.string()));
80 }
81 return content;
82}
83
84std::string FormatHexList(const std::vector<uint16_t>& values) {
85 std::string out;
86 for (size_t i = 0; i < values.size(); ++i) {
87 if (i > 0) {
88 out += ", ";
89 }
90 out += absl::StrFormat("0x%X", values[i]);
91 }
92 return out;
93}
94
103
104absl::StatusOr<std::vector<uint16_t>> ParseHexList(const std::string& input) {
105 std::vector<uint16_t> out;
106 const absl::string_view trimmed_input = absl::StripAsciiWhitespace(input);
107 if (trimmed_input.empty()) {
108 return out;
109 }
110
111 for (absl::string_view comma_group : absl::StrSplit(trimmed_input, ',')) {
112 comma_group = absl::StripAsciiWhitespace(comma_group);
113 if (comma_group.empty()) {
114 return absl::InvalidArgumentError("Overlay list contains an empty value");
115 }
116
117 for (absl::string_view token : absl::StrSplit(
118 comma_group, absl::ByAnyChar(" \n\t\r"), absl::SkipEmpty())) {
119 const absl::string_view original_token = token;
120 int base = 10;
121 if (absl::StartsWith(token, "$")) {
122 token.remove_prefix(1);
123 base = 16;
124 } else if (absl::StartsWith(token, "0x") ||
125 absl::StartsWith(token, "0X")) {
126 token.remove_prefix(2);
127 base = 16;
128 }
129 if (token.empty()) {
130 return absl::InvalidArgumentError(absl::StrFormat(
131 "Overlay value is missing digits: %s", original_token));
132 }
133
134 uint32_t value = 0;
135 const auto [end, error] = std::from_chars(
136 token.data(), token.data() + token.size(), value, base);
137 if (error == std::errc::result_out_of_range || value > 0xFFFF) {
138 return absl::OutOfRangeError(absl::StrFormat(
139 "Overlay value is outside 16-bit range: %s", original_token));
140 }
141 if (error != std::errc() || end != token.data() + token.size()) {
142 return absl::InvalidArgumentError(
143 absl::StrFormat("Invalid overlay value: %s", original_token));
144 }
145 out.push_back(static_cast<uint16_t>(value));
146 }
147 }
148 return out;
149}
150} // namespace
151
153 tracks_.clear();
154 loaded_tracks_.clear();
155 source_document_.reset();
157 loaded_source_path_.clear();
158 loaded_source_sha256_.clear();
159 load_attempted_ = false;
160 loaded_ = false;
162 audit_dirty_ = true;
163}
164
166 const std::string current_filepath = project_ ? project_->filepath : "";
167 const auto current_source_identity = ProjectSourceIdentity(project_);
168 if (current_filepath == bound_project_filepath_ &&
169 current_source_identity == bound_source_identity_) {
170 return absl::OkStatus();
171 }
172 if (HasUnpublishedChanges()) {
173 return absl::FailedPreconditionError(
174 "Project descriptor moved or minecart source changed; discard "
175 "minecart track drafts before rebinding the source");
176 }
177
178 bound_project_filepath_ = current_filepath;
179 bound_source_identity_ = current_source_identity;
181 overlay_inputs_model_.reset();
183 return absl::OkStatus();
184}
185
187 project::YazeProject* project) {
188 const std::string next_filepath = project ? project->filepath : "";
189 const auto next_source_identity = ProjectSourceIdentity(project);
190 if (project_ == project && bound_project_filepath_ == next_filepath &&
191 bound_source_identity_ == next_source_identity) {
192 return absl::OkStatus();
193 }
194 if (HasUnpublishedChanges()) {
195 return absl::FailedPreconditionError(
196 "Discard minecart track drafts before changing projects");
197 }
198
199 project_ = project;
200 bound_project_filepath_ = next_filepath;
201 bound_source_identity_ = next_source_identity;
203 overlay_inputs_model_.reset();
205 return absl::OkStatus();
206}
207
209 project::YazeProject* project) {
210 const absl::Status status = SetProject(project);
211 if (!status.ok()) {
212 status_message_ = std::string(status.message());
213 show_success_ = false;
214 return status;
215 }
216
217 // Project descriptors are reapplied into stable session storage. Refresh a
218 // same-pointer binding only when the model changed; otherwise preserve text
219 // currently being edited until it can be validated and committed.
220 if (project != nullptr && (!overlay_inputs_model_.has_value() ||
221 !OverlaySettingsEqual(*overlay_inputs_model_,
222 project->dungeon_overlay))) {
225 }
226 return absl::OkStatus();
227}
228
230 if (project_ == nullptr || !project_->hack_manifest.loaded()) {
231 return absl::FailedPreconditionError(
232 "A loaded hack manifest is required to publish minecart tracks");
233 }
234 const auto& current_source =
236 if (!current_source.has_value()) {
237 return absl::FailedPreconditionError(
238 "Hack manifest does not define minecart_tracks.source");
239 }
240 if (!loaded_source_identity_.has_value() ||
241 *current_source != *loaded_source_identity_) {
242 return absl::FailedPreconditionError(
243 "Hack manifest minecart_tracks.source changed after the tracks were "
244 "loaded; drafts were kept");
245 }
246 return absl::OkStatus();
247}
248
249absl::StatusOr<std::filesystem::path>
251 if (project_ != nullptr && project_->filepath != bound_project_filepath_) {
252 return absl::FailedPreconditionError(
253 "Project descriptor moved; reload minecart tracks to rebind the "
254 "source");
255 }
256 if (project_ == nullptr || bound_project_filepath_.empty()) {
257 return absl::FailedPreconditionError(
258 "An open project descriptor is required for minecart tracks");
259 }
260 if (!project_->hack_manifest.loaded()) {
261 return absl::FailedPreconditionError(
262 "A loaded hack manifest is required for minecart tracks");
263 }
264 const auto& source = project_->hack_manifest.minecart_track_layout().source;
265 if (!source.has_value()) {
266 return absl::FailedPreconditionError(
267 "Hack manifest does not define minecart_tracks.source");
268 }
269
270 std::error_code ec;
271 std::filesystem::path descriptor_path(bound_project_filepath_);
272 if (descriptor_path.is_relative()) {
273 descriptor_path = std::filesystem::absolute(descriptor_path, ec);
274 if (ec) {
275 return absl::InvalidArgumentError(absl::StrFormat(
276 "Could not resolve project descriptor path: %s", ec.message()));
277 }
278 }
279
280 const std::filesystem::path project_root =
281 std::filesystem::weakly_canonical(descriptor_path.parent_path(), ec);
282 if (ec || project_root.empty() ||
283 !std::filesystem::is_directory(project_root, ec) || ec) {
284 return absl::InvalidArgumentError(
285 "Project descriptor parent is not an existing directory");
286 }
287
288 const std::filesystem::path candidate =
289 (project_root / source->path).lexically_normal();
290 const std::filesystem::file_status candidate_status =
291 std::filesystem::symlink_status(candidate, ec);
292 if (ec) {
293 return absl::NotFoundError(
294 absl::StrFormat("Minecart source not found: %s", candidate.string()));
295 }
296 if (std::filesystem::is_symlink(candidate_status)) {
297 return absl::PermissionDeniedError(
298 "Minecart source may not be a symbolic link");
299 }
300 const std::filesystem::path resolved =
301 std::filesystem::canonical(candidate, ec);
302 if (ec) {
303 return absl::NotFoundError(
304 absl::StrFormat("Minecart source not found: %s", candidate.string()));
305 }
306 if (!IsStrictDescendant(resolved, project_root)) {
307 return absl::PermissionDeniedError(
308 "Minecart source resolves outside the project root");
309 }
310 if (!std::filesystem::is_regular_file(resolved, ec) || ec) {
311 return absl::InvalidArgumentError(
312 "Minecart source must be an existing regular file");
313 }
314 return resolved;
315}
316
320
336
338 const absl::StatusOr<bool> committed = CommitOverlayInputsForSave();
339 return committed.ok() ? absl::OkStatus() : committed.status();
340}
341
342absl::Status MinecartTrackEditorPanel::UpdateTrack(size_t track_index,
343 const MinecartTrack& track) {
344 if (!loaded_) {
345 return absl::FailedPreconditionError("Minecart tracks are not loaded");
346 }
347 if (track_index >= tracks_.size()) {
348 return absl::InvalidArgumentError("Minecart track index is out of range");
349 }
350 MinecartTrack updated = track;
351 updated.id = static_cast<int>(track_index);
352 tracks_[track_index] = updated;
353 audit_dirty_ = true;
354 return absl::OkStatus();
355}
356
358 if (!loaded_) {
359 return absl::FailedPreconditionError("Minecart tracks are not loaded");
360 }
363 audit_dirty_ = true;
364 return absl::OkStatus();
365}
366
368 const absl::Status binding_status = RefreshProjectBinding();
369 if (!binding_status.ok()) {
370 return binding_status;
371 }
372 if (HasUnpublishedChanges()) {
373 return absl::FailedPreconditionError(
374 "Discard minecart track drafts before reloading the source");
375 }
376 return LoadTracks();
377}
378
396
404
406 std::string& input,
407 OverlayListMember member) {
408 if (ImGui::InputText(label, &input)) {
409 const absl::Status draft_status = NotifyProjectDraftChanged();
410 if (!draft_status.ok()) {
411 input = FormatHexList(project_->dungeon_overlay.*member);
413 absl::StrFormat("Overlay draft rejected: %s", draft_status.message());
414 show_success_ = false;
415 return false;
416 }
417 }
418 if (ImGui::IsItemDeactivatedAfterEdit()) {
419 const absl::StatusOr<bool> changed = CommitOverlayList(input, member);
420 return changed.ok() && *changed;
421 }
422 return false;
423}
424
426 std::string& input, OverlayListMember member) {
427 if (project_ == nullptr) {
428 const absl::Status status =
429 absl::FailedPreconditionError("No project is bound to the panel");
430 status_message_ = std::string(status.message());
431 show_success_ = false;
432 return status;
433 }
434
435 const auto parsed_or = ParseHexList(input);
436 if (!parsed_or.ok()) {
437 status_message_ = absl::StrFormat("Overlay update rejected: %s",
438 parsed_or.status().message());
439 show_success_ = false;
440 return parsed_or.status();
441 }
442
444 std::vector<uint16_t>& candidate_target = candidate.*member;
445 if (*parsed_or == candidate_target) {
446 input = FormatHexList(*parsed_or);
448 status_message_.clear();
449 show_success_ = false;
450 return false;
451 }
452
453 candidate_target = *parsed_or;
454 const absl::Status notify_status = NotifyProjectChanged(candidate);
455 if (!notify_status.ok()) {
456 input = FormatHexList(project_->dungeon_overlay.*member);
458 absl::StrFormat("Overlay update rejected: %s", notify_status.message());
459 show_success_ = false;
460 return notify_status;
461 }
462
463 project_->dungeon_overlay = std::move(candidate);
464 input = FormatHexList(project_->dungeon_overlay.*member);
466 audit_dirty_ = true;
467 status_message_ = "Overlay settings updated; save the project to persist.";
468 show_success_ = true;
469 return true;
470}
471
473 if (project_ == nullptr) {
474 return absl::FailedPreconditionError("No project is bound to the panel");
475 }
476
479 auto parse_field = [](const char* field_name, const std::string& input,
480 std::vector<uint16_t>* target) -> absl::Status {
481 const auto parsed_or = ParseHexList(input);
482 if (!parsed_or.ok()) {
483 return absl::Status(
484 parsed_or.status().code(),
485 absl::StrFormat("%s: %s", field_name, parsed_or.status().message()));
486 }
487 *target = *parsed_or;
488 return absl::OkStatus();
489 };
490
491 absl::Status parse_status = parse_field(
492 "Track Tiles", overlay_track_tiles_input_, &candidate.track_tiles);
493 if (parse_status.ok()) {
494 parse_status = parse_field("Stop Tiles", overlay_track_stop_tiles_input_,
495 &candidate.track_stop_tiles);
496 }
497 if (parse_status.ok()) {
498 parse_status =
499 parse_field("Switch Tiles", overlay_track_switch_tiles_input_,
500 &candidate.track_switch_tiles);
501 }
502 if (parse_status.ok()) {
503 parse_status =
504 parse_field("Track Object IDs", overlay_track_object_ids_input_,
505 &candidate.track_object_ids);
506 }
507 if (parse_status.ok()) {
508 parse_status =
509 parse_field("Minecart Sprite IDs", overlay_minecart_sprite_ids_input_,
510 &candidate.minecart_sprite_ids);
511 }
512 if (!parse_status.ok()) {
514 absl::StrFormat("Project save blocked: %s", parse_status.message());
515 show_success_ = false;
516 return parse_status;
517 }
518
519 auto normalize_inputs = [this]() {
521 FormatHexList(project_->dungeon_overlay.track_tiles);
531 };
532
533 if (OverlaySettingsEqual(candidate, project_->dungeon_overlay)) {
534 normalize_inputs();
535 return false;
536 }
537
538 const absl::Status notify_status = NotifyProjectChanged(candidate);
539 if (!notify_status.ok()) {
541 absl::StrFormat("Project save blocked: %s", notify_status.message());
542 show_success_ = false;
543 return notify_status;
544 }
545
546 project_->dungeon_overlay = std::move(candidate);
547 normalize_inputs();
548 audit_dirty_ = true;
549 return true;
550}
551
553 if (project_ == nullptr) {
554 const absl::Status status =
555 absl::FailedPreconditionError("No project is bound to the panel");
556 status_message_ = std::string(status.message());
557 show_success_ = false;
558 return status;
559 }
560
562 const bool changed =
563 !overlay.track_tiles.empty() || !overlay.track_stop_tiles.empty() ||
564 !overlay.track_switch_tiles.empty() ||
565 !overlay.track_object_ids.empty() || !overlay.minecart_sprite_ids.empty();
566 if (!changed) {
569 status_message_.clear();
570 show_success_ = false;
571 return false;
572 }
573
574 overlay.track_tiles.clear();
575 overlay.track_stop_tiles.clear();
576 overlay.track_switch_tiles.clear();
577 overlay.track_object_ids.clear();
578 overlay.minecart_sprite_ids.clear();
579 const absl::Status notify_status = NotifyProjectChanged(overlay);
580 if (!notify_status.ok()) {
582 absl::StrFormat("Overlay reset rejected: %s", notify_status.message());
583 show_success_ = false;
584 return notify_status;
585 }
586
587 project_->dungeon_overlay = std::move(overlay);
590 audit_dirty_ = true;
591 status_message_ = "Overlay settings reset; save the project to persist.";
592 show_success_ = true;
593 return true;
594}
595
597 const project::DungeonOverlaySettings& overlay) {
598 if (project_ == nullptr) {
599 return absl::FailedPreconditionError("No project is bound to the panel");
600 }
602 return absl::FailedPreconditionError(
603 "Project change tracking is unavailable for minecart overlays");
604 }
605 return project_changed_callback_(overlay);
606}
607
609 if (project_ == nullptr) {
610 return absl::FailedPreconditionError("No project is bound to the panel");
611 }
613 return absl::FailedPreconditionError(
614 "Project draft tracking is unavailable for minecart overlays");
615 }
617}
618
620 if (project_ == nullptr || !project_->project_opened()) {
621 return absl::FailedPreconditionError("No open project to save");
622 }
624 return absl::FailedPreconditionError(
625 "Project save is unavailable outside the editor manager");
626 }
627 const absl::StatusOr<bool> committed = CommitOverlayInputsForSave();
628 if (!committed.ok()) {
629 return committed.status();
630 }
631 return project_save_callback_();
632}
633
635 if (!project_) {
636 return;
637 }
638
640
641 if (!ImGui::CollapsingHeader(ICON_MD_TUNE " Overlay Config",
642 ImGuiTreeNodeFlags_DefaultOpen)) {
643 return;
644 }
645
646 ImGui::TextDisabled(tr("Empty list = defaults. Use hex (0xB0) or decimal."));
647 ImGui::TextDisabled(
648 tr("Defaults: Track 0xB0-0xBE | Stop 0xB7-0xBA | Switch 0xD0-0xD3 | "
649 "Track Obj 0x31 | Cart Sprite 0xA3"));
650
651 bool changed = false;
652 changed |= UpdateOverlayList("Track Tiles", overlay_track_tiles_input_,
654 changed |=
657 changed |=
660 changed |=
663 changed |= UpdateOverlayList(
664 "Minecart Sprite IDs", overlay_minecart_sprite_ids_input_,
666
667 if (ImGui::Button(tr("Reset Overlay Defaults"))) {
668 const absl::StatusOr<bool> reset = ResetOverlaySettings();
669 changed |= reset.ok() && *reset;
670 }
671
672 if (changed) {
673 ImGui::TextDisabled(tr("Remember to save the project to persist changes."));
674 }
675}
676
677const std::vector<MinecartTrack>& MinecartTrackEditorPanel::GetTracks() {
678 const absl::Status binding_status = RefreshProjectBinding();
679 if (!binding_status.ok()) {
680 status_message_ = std::string(binding_status.message());
681 show_success_ = false;
682 return tracks_;
683 }
684 if (!load_attempted_) {
685 const absl::Status status = LoadTracks();
686 if (!status.ok()) {
687 status_message_ = std::string(status.message());
688 show_success_ = false;
689 }
690 }
691 return tracks_;
692}
693
695 uint16_t camera_x,
696 uint16_t camera_y) {
698 picking_track_index_ < static_cast<int>(tracks_.size())) {
699 tracks_[picking_track_index_].room_id = room_id;
700 tracks_[picking_track_index_].start_x = camera_x;
701 tracks_[picking_track_index_].start_y = camera_y;
702
703 last_picked_x_ = camera_x;
704 last_picked_y_ = camera_y;
705 has_picked_coords_ = true;
706 audit_dirty_ = true;
707
709 absl::StrFormat("Track %d: Set to Room $%04X, Pos ($%04X, $%04X)",
710 picking_track_index_, room_id, camera_x, camera_y);
711 show_success_ = true;
712 }
713
714 // Exit picking mode
715 picking_mode_ = false;
717}
718
720 picking_mode_ = true;
721 picking_track_index_ = track_index;
722 status_message_ = absl::StrFormat(
723 "Click on the dungeon canvas to set Track %d position", track_index);
724 show_success_ = false;
725}
726
732
734 const MinecartTrack& track) const {
735 return track.room_id == kDefaultTrackRoom &&
736 track.start_x == kDefaultTrackX && track.start_y == kDefaultTrackY;
737}
738
740 room_audit_.clear();
741 track_usage_rooms_.clear();
742 track_subtype_used_.assign(kTrackSlotCount, false);
743
744 if (!rooms_) {
745 audit_dirty_ = false;
746 return;
747 }
748
749 std::array<bool, 256> track_tiles{};
750 std::array<bool, 256> stop_tiles{};
751 std::array<bool, 256> switch_tiles{};
752 auto apply_list = [](std::array<bool, 256>& dest,
753 const std::vector<uint16_t>& values) {
754 dest.fill(false);
755 for (uint16_t value : values) {
756 if (value < dest.size()) {
757 dest[value] = true;
758 }
759 }
760 };
761
762 if (project_ && !project_->dungeon_overlay.track_tiles.empty()) {
763 apply_list(track_tiles, project_->dungeon_overlay.track_tiles);
764 } else {
765 std::vector<uint16_t> default_track_tiles;
766 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
767 default_track_tiles.push_back(tile);
768 }
769 apply_list(track_tiles, default_track_tiles);
770 }
771
773 apply_list(stop_tiles, project_->dungeon_overlay.track_stop_tiles);
774 } else {
775 apply_list(stop_tiles, {0xB7, 0xB8, 0xB9, 0xBA});
776 }
777
779 apply_list(switch_tiles, project_->dungeon_overlay.track_switch_tiles);
780 } else {
781 apply_list(switch_tiles, {0xD0, 0xD1, 0xD2, 0xD3});
782 }
783
784 std::vector<uint16_t> track_object_ids = {0x31};
785 std::vector<uint16_t> minecart_sprite_ids = {0xA3};
786 if (project_) {
788 track_object_ids = project_->dungeon_overlay.track_object_ids;
789 }
791 minecart_sprite_ids = project_->dungeon_overlay.minecart_sprite_ids;
792 }
793 }
794
795 std::unordered_map<int, bool> track_object_id_map;
796 for (uint16_t id : track_object_ids) {
797 track_object_id_map[static_cast<int>(id)] = true;
798 }
799 std::unordered_map<int, bool> minecart_sprite_id_map;
800 for (uint16_t id : minecart_sprite_ids) {
801 minecart_sprite_id_map[static_cast<int>(id)] = true;
802 }
803
804 for (int room_id = 0; room_id < static_cast<int>(rooms_->size()); ++room_id) {
805 auto& room = (*rooms_)[room_id];
806 RoomTrackAudit audit;
807
808 if (room.GetTileObjects().empty()) {
809 room.LoadObjects();
810 }
811 if (room.GetSprites().empty()) {
812 room.LoadSprites();
813 }
814
815 std::array<bool, kTrackSlotCount> seen_subtype{};
816
817 for (const auto& obj : room.GetTileObjects()) {
818 if (!track_object_id_map[static_cast<int>(obj.id_)]) {
819 continue;
820 }
821 int subtype = obj.size_ & 0x1F;
822 if (subtype >= 0 && subtype < kTrackSlotCount) {
823 if (!seen_subtype[static_cast<size_t>(subtype)]) {
824 seen_subtype[static_cast<size_t>(subtype)] = true;
825 track_subtype_used_[static_cast<size_t>(subtype)] = true;
826 track_usage_rooms_[subtype].push_back(room_id);
827 audit.track_subtypes.push_back(subtype);
828 }
829 }
830 }
831
832 std::unordered_map<int, bool> stop_positions;
833 auto map_or = zelda3::LoadCustomCollisionMap(room.rom(), room_id);
834 if (map_or.ok() && map_or.value().has_data) {
835 const auto& map = map_or.value().tiles;
836 for (int y = 0; y < 64; ++y) {
837 for (int x = 0; x < 64; ++x) {
838 uint8_t tile = map[static_cast<size_t>(y * 64 + x)];
839 if (track_tiles[tile] || stop_tiles[tile] || switch_tiles[tile]) {
840 audit.has_track_collision = true;
841 }
842 if (stop_tiles[tile]) {
843 audit.has_stop_tiles = true;
844 stop_positions[y * 64 + x] = true;
845 }
846 }
847 }
848 }
849
850 if (audit.has_track_collision) {
851 for (const auto& sprite : room.GetSprites()) {
852 if (!minecart_sprite_id_map[static_cast<int>(sprite.id())]) {
853 continue;
854 }
855 audit.has_minecart_sprite = true;
856 int tile_x = sprite.x() * 2;
857 int tile_y = sprite.y() * 2;
858 if (tile_x >= 0 && tile_x < 64 && tile_y >= 0 && tile_y < 64) {
859 int idx = tile_y * 64 + tile_x;
860 if (stop_positions[idx]) {
861 audit.has_minecart_on_stop = true;
862 }
863 }
864 }
865 }
866
867 if (audit.has_track_collision || !audit.track_subtypes.empty() ||
868 audit.has_minecart_sprite) {
869 room_audit_[room_id] = audit;
870 }
871 }
872
873 audit_dirty_ = false;
874}
875
877 if (project_ == nullptr) {
878 ImGui::TextColored(ImVec4(1, 0, 0, 1),
879 tr("Open a project to edit minecart tracks."));
880 return;
881 }
882
883 const absl::Status binding_status = RefreshProjectBinding();
884 if (!binding_status.ok()) {
885 status_message_ = std::string(binding_status.message());
886 show_success_ = false;
887 }
888 if (bound_project_filepath_.empty()) {
889 ImGui::TextColored(ImVec4(1, 0, 0, 1),
890 tr("Open a project to edit minecart tracks."));
891 return;
892 }
893
894 if (!load_attempted_) {
895 const absl::Status status = LoadTracks();
896 if (!status.ok()) {
897 status_message_ = std::string(status.message());
898 show_success_ = false;
899 }
900 }
901
902 if (audit_dirty_) {
904 }
905
906 ImGui::Text(tr("Minecart Track Editor"));
907 ImGui::TextDisabled(
908 tr("Publish Tracks changes the manifest-owned ASM source only; it does "
909 "not update the open ROM."));
910 ImGui::TextDisabled(
911 tr("After publishing, save pending dungeon/ROM edits to the development "
912 "ROM, rebuild the patched ROM, then reopen/reload the patched ROM in "
913 "Yaze before testing."));
914#if defined(__EMSCRIPTEN__)
915 ImGui::TextDisabled(
916 tr("Source publishing is unavailable in browser builds; drafts are "
917 "retained."));
918#endif
919 const bool has_unpublished_changes = HasUnpublishedChanges();
920 const bool can_publish =
921 has_unpublished_changes && kSourcePublishingAvailable;
922 if (!can_publish) {
923 ImGui::BeginDisabled();
924 }
925 if (ImGui::Button(ICON_MD_SAVE " Publish Tracks")) {
926 const absl::Status status = SaveTracks();
928 status.ok()
929 ? "Minecart ASM source published only. Save pending dungeon/ROM "
930 "edits to the development ROM, rebuild the patched ROM, then "
931 "reopen/reload the patched ROM in Yaze before testing."
932 : std::string(status.message());
933 show_success_ = status.ok();
934 }
935 if (!can_publish) {
936 ImGui::EndDisabled();
937 }
938 ImGui::SameLine();
939 if (!has_unpublished_changes) {
940 ImGui::BeginDisabled();
941 }
942 if (ImGui::Button(ICON_MD_RESTORE " Discard Drafts")) {
943 const absl::Status status = DiscardUnpublishedChanges();
944 status_message_ = status.ok() ? "Minecart track drafts discarded."
945 : std::string(status.message());
946 show_success_ = status.ok();
947 }
948 if (!has_unpublished_changes) {
949 ImGui::EndDisabled();
950 }
951 ImGui::SameLine();
952 if (ImGui::Button(ICON_MD_REFRESH " Reload Source")) {
953 const absl::Status status = ReloadTracks();
954 status_message_ = status.ok() ? "Minecart track source reloaded."
955 : std::string(status.message());
956 show_success_ = status.ok();
957 }
958 ImGui::SameLine();
959 const bool can_save_project =
961 if (!can_save_project) {
962 ImGui::BeginDisabled();
963 }
964 if (ImGui::Button(ICON_MD_SAVE " Save Project")) {
965 auto status = SaveProjectSettings();
966 if (status.ok()) {
967 status_message_ = has_unpublished_changes
968 ? "Project saved; minecart track drafts remain "
969 "unsaved."
970 : "Project saved.";
971 show_success_ = true;
972 } else {
974 absl::StrFormat("Project save failed: %s", status.message());
975 show_success_ = false;
976 }
977 }
978 if (!can_save_project) {
979 ImGui::EndDisabled();
980 }
981
982 // Show picking mode indicator
983 if (picking_mode_) {
984 ImGui::SameLine();
985 if (ImGui::Button(ICON_MD_CANCEL " Cancel Pick")) {
987 }
988 ImGui::SameLine();
989 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
990 ICON_MD_MY_LOCATION " Picking for Track %d...",
992 }
993
994 if (!status_message_.empty() && !picking_mode_) {
995 ImGui::SameLine();
996 ImGui::TextColored(show_success_ ? ImVec4(0, 1, 0, 1) : ImVec4(1, 0, 0, 1),
997 "%s", status_message_.c_str());
998 }
999
1001 ImGui::Separator();
1002
1003 // Coordinate format help
1004 ImGui::TextDisabled(tr(
1005 "Camera coordinates use $1XXX format (base $1000 + room offset + local "
1006 "position)"));
1007 ImGui::TextDisabled(tr(
1008 "Hover over dungeon canvas to see coordinates, or click 'Pick' button."));
1009 ImGui::Separator();
1010
1011 if (ImGui::BeginTable("TracksTable", 7,
1012 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1013 ImGuiTableFlags_Resizable)) {
1014 ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 30.0f);
1015 ImGui::TableSetupColumn("Room ID", ImGuiTableColumnFlags_WidthFixed, 80.0f);
1016 ImGui::TableSetupColumn("Camera X", ImGuiTableColumnFlags_WidthFixed,
1017 80.0f);
1018 ImGui::TableSetupColumn("Camera Y", ImGuiTableColumnFlags_WidthFixed,
1019 80.0f);
1020 ImGui::TableSetupColumn("Pick", ImGuiTableColumnFlags_WidthFixed, 50.0f);
1021 ImGui::TableSetupColumn("Go", ImGuiTableColumnFlags_WidthFixed, 40.0f);
1022 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60.0f);
1023 ImGui::TableHeadersRow();
1024
1025 for (auto& track : tracks_) {
1026 ImGui::TableNextRow();
1027
1028 const bool is_default = IsDefaultTrack(track);
1029 const bool used_in_rooms =
1030 track.id >= 0 &&
1031 track.id < static_cast<int>(track_subtype_used_.size()) &&
1032 track_subtype_used_[track.id];
1033 const bool missing_start = used_in_rooms && is_default;
1034
1035 if (missing_start) {
1036 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
1037 IM_COL32(120, 40, 40, 120));
1038 } else if (is_default) {
1039 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
1040 IM_COL32(60, 60, 60, 80));
1041 }
1042
1043 // Highlight the row being picked
1044 if (picking_mode_ && track.id == picking_track_index_) {
1045 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
1046 IM_COL32(80, 80, 0, 100));
1047 }
1048
1049 ImGui::TableNextColumn();
1050 ImGui::Text("%d", track.id);
1051
1052 ImGui::TableNextColumn();
1053 uint16_t room_id = static_cast<uint16_t>(track.room_id);
1055 absl::StrFormat("##Room%d", track.id).c_str(), &room_id, 60.0f)) {
1056 track.room_id = room_id;
1057 audit_dirty_ = true;
1058 }
1059
1060 ImGui::TableNextColumn();
1061 uint16_t start_x = static_cast<uint16_t>(track.start_x);
1063 absl::StrFormat("##StartX%d", track.id).c_str(), &start_x,
1064 60.0f)) {
1065 track.start_x = start_x;
1066 audit_dirty_ = true;
1067 }
1068
1069 ImGui::TableNextColumn();
1070 uint16_t start_y = static_cast<uint16_t>(track.start_y);
1072 absl::StrFormat("##StartY%d", track.id).c_str(), &start_y,
1073 60.0f)) {
1074 track.start_y = start_y;
1075 audit_dirty_ = true;
1076 }
1077
1078 // Pick button to select coordinates from canvas
1079 ImGui::TableNextColumn();
1080 ImGui::PushID(track.id);
1081 bool is_picking_this = picking_mode_ && picking_track_index_ == track.id;
1082 {
1083 std::optional<gui::StyleColorGuard> pick_guard;
1084 if (is_picking_this) {
1085 pick_guard.emplace(ImGuiCol_Button, ImVec4(0.8f, 0.6f, 0.0f, 1.0f));
1086 }
1087 if (ImGui::SmallButton(ICON_MD_MY_LOCATION)) {
1088 if (is_picking_this) {
1090 } else {
1091 StartCoordinatePicking(track.id);
1092 }
1093 }
1094 }
1095 if (ImGui::IsItemHovered()) {
1096 ImGui::SetTooltip(is_picking_this ? "Cancel picking"
1097 : "Pick coordinates from canvas");
1098 }
1099 ImGui::PopID();
1100
1101 // Go to room button
1102 ImGui::TableNextColumn();
1103 ImGui::PushID(track.id + 1000);
1104 if (ImGui::SmallButton(ICON_MD_ARROW_FORWARD)) {
1106 room_navigation_callback_(track.room_id);
1107 }
1108 }
1109 if (ImGui::IsItemHovered()) {
1110 ImGui::SetTooltip(tr("Navigate to room $%04X"), track.room_id);
1111 }
1112 ImGui::PopID();
1113
1114 // Status column
1115 ImGui::TableNextColumn();
1116 if (missing_start) {
1117 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
1119 } else if (is_default) {
1120 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MD_INFO);
1121 } else if (used_in_rooms) {
1122 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f),
1124 } else {
1125 ImGui::Text("-");
1126 }
1127
1128 if (ImGui::IsItemHovered()) {
1129 ImGui::BeginTooltip();
1130 if (missing_start) {
1131 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
1132 tr("Used in rooms but still default"));
1133 } else if (is_default) {
1134 ImGui::Text(tr("Default filler slot"));
1135 } else if (used_in_rooms) {
1136 ImGui::Text(tr("Used in rooms"));
1137 } else {
1138 ImGui::Text(tr("No usage detected"));
1139 }
1140
1141 auto rooms_it = track_usage_rooms_.find(track.id);
1142 if (rooms_it != track_usage_rooms_.end()) {
1143 ImGui::Separator();
1144 ImGui::Text(tr("Rooms:"));
1145 for (int room_id : rooms_it->second) {
1146 ImGui::BulletText(tr("0x%03X"), room_id);
1147 }
1148 }
1149 ImGui::EndTooltip();
1150 }
1151 }
1152
1153 ImGui::EndTable();
1154 }
1155
1156 // Summary + room audit
1157 int default_count = 0;
1158 int used_count = 0;
1159 int missing_start_count = 0;
1160 for (const auto& track : tracks_) {
1161 bool is_default = IsDefaultTrack(track);
1162 bool used_in_rooms =
1163 track.id >= 0 &&
1164 track.id < static_cast<int>(track_subtype_used_.size()) &&
1165 track_subtype_used_[track.id];
1166 if (is_default) {
1167 default_count++;
1168 }
1169 if (used_in_rooms) {
1170 used_count++;
1171 }
1172 if (used_in_rooms && is_default) {
1173 missing_start_count++;
1174 }
1175 }
1176
1177 ImGui::Separator();
1178 ImGui::Text(tr("Usage Summary: used %d/%d, default %d, missing starts %d"),
1179 used_count, kTrackSlotCount, default_count, missing_start_count);
1180
1181 if (!room_audit_.empty()) {
1182 ImGui::Separator();
1183 ImGui::Text(tr("Rooms with track objects:"));
1184
1185 // "Generate All" button: batch-generate collision for all rooms that have
1186 // rail objects but no collision data yet.
1187 if (rom_ && rooms_) {
1188 // Count rooms that need generation
1189 int rooms_needing_collision = 0;
1190 for (const auto& [rid, audit] : room_audit_) {
1191 if (!audit.track_subtypes.empty() && !audit.has_track_collision) {
1192 rooms_needing_collision++;
1193 }
1194 }
1195
1196 if (rooms_needing_collision > 0) {
1197 if (ImGui::Button(absl::StrFormat(ICON_MD_AUTO_FIX_HIGH
1198 " Generate All (%d rooms)",
1199 rooms_needing_collision)
1200 .c_str())) {
1201 int generated_rooms = 0;
1202 int total_tiles = 0;
1203 bool had_error = false;
1204
1205 for (auto& [rid, audit] : room_audit_) {
1206 if (audit.track_subtypes.empty() || audit.has_track_collision) {
1207 continue;
1208 }
1209
1210 auto& target_room = (*rooms_)[rid];
1212 auto gen_result =
1213 zelda3::GenerateTrackCollision(&target_room, opts);
1214 if (!gen_result.ok()) {
1216 absl::StrFormat("Generate failed for room 0x%03X: %s", rid,
1217 gen_result.status().message());
1218 show_success_ = false;
1219 had_error = true;
1220 break;
1221 }
1222
1223 auto write_status = zelda3::WriteTrackCollision(
1224 rom_, rid, gen_result->collision_map);
1225 if (!write_status.ok()) {
1227 absl::StrFormat("Write failed for room 0x%03X: %s", rid,
1228 write_status.message());
1229 show_success_ = false;
1230 had_error = true;
1231 break;
1232 }
1233
1234 generated_rooms++;
1235 total_tiles += gen_result->tiles_generated;
1236 }
1237
1238 if (!had_error) {
1239 status_message_ = absl::StrFormat(
1240 "Generated collision for %d rooms (%d tiles total)",
1241 generated_rooms, total_tiles);
1242 show_success_ = true;
1243 }
1244 audit_dirty_ = true;
1245 }
1246 if (ImGui::IsItemHovered()) {
1247 ImGui::SetTooltip(
1248 tr("Generate collision for all %d rooms with rail objects "
1249 "but no collision data"),
1250 rooms_needing_collision);
1251 }
1252 }
1253 }
1254
1255 ImGui::BeginChild("##TrackAuditRooms", ImVec2(0, 160), true);
1256 for (const auto& [room_id, audit] : room_audit_) {
1257 if (audit.track_subtypes.empty() && !audit.has_track_collision) {
1258 continue;
1259 }
1260
1261 // Status icon
1262 if (!audit.has_track_collision) {
1263 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.1f, 1.0f),
1264 ICON_MD_ERROR " Room 0x%03X (no collision)",
1265 room_id);
1266 } else if (!audit.has_minecart_on_stop) {
1267 ImGui::TextColored(
1268 ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
1269 ICON_MD_WARNING_AMBER " Room 0x%03X (no cart on stop)", room_id);
1270 } else {
1271 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f),
1272 ICON_MD_CHECK_CIRCLE " Room 0x%03X", room_id);
1273 }
1274
1275 ImGui::SameLine();
1276 ImGui::PushID(room_id);
1277 if (ImGui::SmallButton(ICON_MD_ARROW_FORWARD)) {
1280 }
1281 }
1282 if (ImGui::IsItemHovered()) {
1283 ImGui::SetTooltip(tr("Navigate to room 0x%03X"), room_id);
1284 }
1285
1286 // Generate Collision button (only if rom available and no collision yet)
1287 if (rom_ && rooms_ && !audit.has_track_collision) {
1288 ImGui::SameLine();
1289 if (ImGui::SmallButton(
1290 absl::StrFormat(ICON_MD_AUTO_FIX_HIGH " Generate##%d", room_id)
1291 .c_str())) {
1292 auto& target_room = (*rooms_)[room_id];
1294 auto gen_result = zelda3::GenerateTrackCollision(&target_room, opts);
1295 if (gen_result.ok()) {
1296 auto write_status = zelda3::WriteTrackCollision(
1297 rom_, room_id, gen_result->collision_map);
1298 if (write_status.ok()) {
1299 status_message_ = absl::StrFormat(
1300 "Room 0x%03X: Generated %d tiles (%d stops, %d corners)",
1301 room_id, gen_result->tiles_generated, gen_result->stop_count,
1302 gen_result->corner_count);
1303 show_success_ = true;
1304 audit_dirty_ = true;
1305 } else {
1307 absl::StrFormat("Write failed: %s", write_status.message());
1308 show_success_ = false;
1309 }
1310 } else {
1311 status_message_ = absl::StrFormat("Generate failed: %s",
1312 gen_result.status().message());
1313 show_success_ = false;
1314 }
1315 }
1316 if (ImGui::IsItemHovered()) {
1317 ImGui::SetTooltip(tr(
1318 "Auto-generate collision tiles from rail objects in this room"));
1319 }
1320 }
1321
1322 ImGui::PopID();
1323 }
1324 ImGui::EndChild();
1325 }
1326}
1327
1329 load_attempted_ = true;
1330
1331 if (project_ == nullptr || !project_->hack_manifest.loaded() ||
1333 return absl::FailedPreconditionError(
1334 "Hack manifest does not define minecart_tracks.source");
1335 }
1336 const core::MinecartTrackLayout::Source source_identity =
1338
1339 std::filesystem::path source_path;
1341 std::string source_bytes;
1342 ASSIGN_OR_RETURN(source_bytes, ReadSourceFile(source_path));
1343 auto document_or =
1344 MinecartTrackSourceDocument::Parse(std::move(source_bytes));
1345 if (!document_or.ok()) {
1346 return document_or.status();
1347 }
1348 if (!project_->hack_manifest.minecart_track_layout().source.has_value() ||
1350 source_identity) {
1351 return absl::AbortedError(
1352 "Hack manifest minecart_tracks.source changed while loading tracks");
1353 }
1354
1355 std::vector<MinecartTrack> candidate_tracks = document_or->tracks();
1356 const std::string source_sha256 =
1357 core::ComputeSourceArtifactSha256(document_or->source_bytes());
1358
1359 tracks_ = candidate_tracks;
1360 loaded_tracks_ = std::move(candidate_tracks);
1361 source_document_ = std::move(*document_or);
1362 loaded_source_identity_ = source_identity;
1363 loaded_source_path_ = std::move(source_path);
1364 loaded_source_sha256_ = source_sha256;
1365 loaded_ = true;
1366 audit_dirty_ = true;
1367 status_message_.clear();
1368 show_success_ = true;
1369 return absl::OkStatus();
1370}
1371
1373 if (!loaded_ || !source_document_.has_value() ||
1374 !loaded_source_identity_.has_value() || loaded_source_path_.empty() ||
1375 loaded_source_sha256_.empty()) {
1376 return absl::FailedPreconditionError("Minecart tracks are not loaded");
1377 }
1378 if (!HasUnpublishedChanges()) {
1379 return absl::FailedPreconditionError(
1380 "No unpublished minecart track drafts to publish");
1381 }
1382#if defined(__EMSCRIPTEN__)
1383 return absl::FailedPreconditionError(
1384 "Minecart source publishing is unavailable in browser builds because "
1385 "durable atomic filesystem publication cannot be guaranteed; drafts "
1386 "were kept");
1387#else
1390
1391 std::filesystem::path source_path;
1393 if (source_path != loaded_source_path_) {
1394 return absl::FailedPreconditionError(
1395 "Minecart source path changed after the tracks were loaded; drafts "
1396 "were kept");
1397 }
1398
1399 std::unique_ptr<core::SourceArtifactPublicationLock> publication_lock;
1400 ASSIGN_OR_RETURN(publication_lock,
1402 {source_path}, kMinecartSourcePublisherLabels));
1403
1404 // Recheck every source identity after acquiring the durable publication
1405 // lock. A manifest reload or path replacement must fail before mutation.
1407 std::filesystem::path locked_source_path;
1408 ASSIGN_OR_RETURN(locked_source_path, ResolveTrackSourcePath());
1409 if (locked_source_path != loaded_source_path_) {
1410 return absl::FailedPreconditionError(
1411 "Minecart source path changed while acquiring its publication lock; "
1412 "drafts were kept");
1413 }
1414
1415 std::string source_before;
1416 ASSIGN_OR_RETURN(source_before, ReadSourceFile(locked_source_path));
1417 const std::string source_sha256_before =
1418 core::ComputeSourceArtifactSha256(source_before);
1419 if (source_sha256_before != loaded_source_sha256_ ||
1420 source_before != source_document_->source_bytes()) {
1421 return absl::AbortedError(absl::StrFormat(
1422 "Minecart source SHA-256 CAS failed: expected %s, got %s; drafts were "
1423 "kept",
1424 loaded_source_sha256_, source_sha256_before));
1425 }
1426
1427 std::string source_after;
1428 ASSIGN_OR_RETURN(source_after, source_document_->Render(tracks_));
1429 auto published_document_or = MinecartTrackSourceDocument::Parse(source_after);
1430 if (!published_document_or.ok()) {
1431 return absl::DataLossError(
1432 absl::StrFormat("Rendered minecart source failed strict validation: %s",
1433 published_document_or.status().message()));
1434 }
1435 if (published_document_or->tracks() != tracks_) {
1436 return absl::DataLossError(
1437 "Rendered minecart source did not reproduce the draft tracks");
1438 }
1439 const std::string source_sha256_after =
1441 const std::vector<MinecartTrack> draft_tracks = tracks_;
1442
1443 std::vector<core::SourceArtifactUpdate> updates;
1444 updates.push_back(core::SourceArtifactUpdate{
1445 .target = locked_source_path,
1446 .before = source_before,
1447 .after = source_after,
1448 });
1450 *publication_lock, std::move(updates), loaded_source_sha256_,
1451 [&]() -> absl::Status {
1452 std::string reopened_source;
1453 ASSIGN_OR_RETURN(reopened_source, ReadSourceFile(locked_source_path));
1454 if (reopened_source != source_after ||
1455 core::ComputeSourceArtifactSha256(reopened_source) !=
1456 source_sha256_after) {
1457 return absl::DataLossError(
1458 "Published minecart source failed exact SHA-256 readback");
1459 }
1460 auto reopened_document_or =
1461 MinecartTrackSourceDocument::Parse(std::move(reopened_source));
1462 if (!reopened_document_or.ok()) {
1463 return absl::DataLossError(absl::StrFormat(
1464 "Published minecart source failed strict readback validation: "
1465 "%s",
1466 reopened_document_or.status().message()));
1467 }
1468 if (reopened_document_or->tracks() != draft_tracks) {
1469 return absl::DataLossError(
1470 "Published minecart source track readback did not match the "
1471 "draft");
1472 }
1473 return absl::OkStatus();
1474 }));
1475
1476 tracks_ = published_document_or->tracks();
1478 source_document_ = std::move(*published_document_or);
1479 loaded_source_path_ = std::move(locked_source_path);
1480 loaded_source_sha256_ = source_sha256_after;
1482 audit_dirty_ = true;
1483 return absl::OkStatus();
1484#endif
1485}
1486
1487} // namespace yaze::editor
const MinecartTrackLayout & minecart_track_layout() const
bool loaded() const
Check if the manifest has been loaded.
bool IsDefaultTrack(const MinecartTrack &track) const
std::optional< core::MinecartTrackLayout::Source > bound_source_identity_
bool UpdateOverlayList(const char *label, std::string &input, OverlayListMember member)
void Draw(bool *p_open) override
Draw the panel content.
const std::vector< MinecartTrack > & GetTracks()
ProjectDraftChangedCallback project_draft_changed_callback_
std::vector< uint16_t > project::DungeonOverlaySettings::* OverlayListMember
std::unordered_map< int, RoomTrackAudit > room_audit_
absl::Status NotifyProjectChanged(const project::DungeonOverlaySettings &overlay)
std::optional< MinecartTrackSourceDocument > source_document_
absl::StatusOr< bool > CommitOverlayList(std::string &input, OverlayListMember member)
absl::Status UpdateTrack(size_t track_index, const MinecartTrack &track)
absl::Status RebindProjectContext(project::YazeProject *project)
absl::StatusOr< std::filesystem::path > ResolveTrackSourcePath() const
absl::Status SetProject(project::YazeProject *project)
std::optional< core::MinecartTrackLayout::Source > loaded_source_identity_
std::unordered_map< int, std::vector< int > > track_usage_rooms_
void SetPickedCoordinates(int room_id, uint16_t camera_x, uint16_t camera_y)
std::optional< project::DungeonOverlaySettings > overlay_inputs_model_
static absl::StatusOr< MinecartTrackSourceDocument > Parse(std::string source_bytes)
#define ICON_MD_MY_LOCATION
Definition icons.h:1270
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_WARNING_AMBER
Definition icons.h:2124
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_RESTORE
Definition icons.h:1605
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_SAVE
Definition icons.h:1644
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< std::string > ReadSourceFile(const std::filesystem::path &path)
bool OverlaySettingsEqual(const project::DungeonOverlaySettings &lhs, const project::DungeonOverlaySettings &rhs)
std::optional< core::MinecartTrackLayout::Source > ProjectSourceIdentity(const project::YazeProject *project)
absl::StatusOr< std::vector< uint16_t > > ParseHexList(const std::string &input)
bool IsStrictDescendant(const std::filesystem::path &path, const std::filesystem::path &root)
Editors are the view controllers for the application.
constexpr size_t kMinecartTrackSlotCount
bool InputHexWordCustom(const char *label, uint16_t *data, float input_width)
Definition input.cc:853
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::optional< Source > source
Dungeon overlay configuration (per-project).
Definition project.h:93
std::vector< uint16_t > track_object_ids
Definition project.h:100
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:101
std::vector< uint16_t > track_stop_tiles
Definition project.h:96
std::vector< uint16_t > track_tiles
Definition project.h:95
std::vector< uint16_t > track_switch_tiles
Definition project.h:97
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
bool project_opened() const
Definition project.h:348
core::HackManifest hack_manifest
Definition project.h:212
DungeonOverlaySettings dungeon_overlay
Definition project.h:202