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#include <fstream>
3#include <iomanip>
4#include <sstream>
5#include "absl/status/status.h"
6#include "absl/strings/str_format.h"
7#include "absl/strings/str_split.h"
8#include "imgui/imgui.h"
9#include "imgui/misc/cpp/imgui_stdlib.h"
10
11#include <filesystem>
12#include <iostream>
13#include <regex>
14#include "app/gui/core/icons.h"
15#include "app/gui/core/input.h"
17#include "util/log.h"
20
21namespace yaze::editor {
22
23namespace {
24constexpr int kTrackSlotCount = 32;
25constexpr int kDefaultTrackRoom = 0x89;
26constexpr int kDefaultTrackX = 0x1300;
27constexpr int kDefaultTrackY = 0x1100;
28
29std::string FormatHexList(const std::vector<uint16_t>& values) {
30 std::string out;
31 for (size_t i = 0; i < values.size(); ++i) {
32 if (i > 0) {
33 out += ", ";
34 }
35 out += absl::StrFormat("0x%X", values[i]);
36 }
37 return out;
38}
39
40std::vector<uint16_t> ParseHexList(const std::string& input) {
41 std::vector<uint16_t> out;
42 for (absl::string_view token_view : absl::StrSplit(
43 input, absl::ByAnyChar(", \n\t"), absl::SkipEmpty())) {
44 if (token_view.empty()) {
45 continue;
46 }
47 std::string token(token_view);
48 if (token[0] == '$') {
49 token = "0x" + token.substr(1);
50 }
51 int base = 10;
52 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
53 token = token.substr(2);
54 base = 16;
55 }
56 try {
57 auto value = std::stoul(token, nullptr, base);
58 if (value <= 0xFFFF) {
59 out.push_back(static_cast<uint16_t>(value));
60 }
61 } catch (...) {
62 continue;
63 }
64 }
65 return out;
66}
67} // namespace
68
69void MinecartTrackEditorPanel::SetProjectRoot(const std::string& root) {
70 if (project_root_ != root) {
71 project_root_ = root;
72 loaded_ = false; // Trigger reload on next draw
73 audit_dirty_ = true;
74 }
75}
76
93
95 const char* label, std::string& input, std::vector<uint16_t>& target) {
96 bool changed = ImGui::InputText(label, &input);
97 if (changed && ImGui::IsItemDeactivatedAfterEdit()) {
98 target = ParseHexList(input);
99 audit_dirty_ = true;
100 return true;
101 }
102 return false;
103}
104
106 if (!project_) {
107 return;
108 }
109
111
112 if (!ImGui::CollapsingHeader(ICON_MD_TUNE " Overlay Config",
113 ImGuiTreeNodeFlags_DefaultOpen)) {
114 return;
115 }
116
117 ImGui::TextDisabled("Empty list = defaults. Use hex (0xB0) or decimal.");
118 ImGui::TextDisabled(
119 "Defaults: Track 0xB0-0xBE | Stop 0xB7-0xBA | Switch 0xD0-0xD3 | "
120 "Track Obj 0x31 | Cart Sprite 0xA3");
121
122 bool changed = false;
123 changed |= UpdateOverlayList("Track Tiles",
126 changed |= UpdateOverlayList("Stop Tiles",
129 changed |= UpdateOverlayList("Switch Tiles",
132 changed |= UpdateOverlayList("Track Object IDs",
135 changed |= UpdateOverlayList("Minecart Sprite IDs",
138
139 if (ImGui::Button("Reset Overlay Defaults")) {
150 audit_dirty_ = true;
151 changed = true;
152 }
153
154 if (changed) {
155 ImGui::TextDisabled("Remember to save the project to persist changes.");
156 }
157}
158
159const std::vector<MinecartTrack>& MinecartTrackEditorPanel::GetTracks() {
160 if (!loaded_) {
161 LoadTracks();
162 }
163 return tracks_;
164}
165
167 uint16_t camera_x,
168 uint16_t camera_y) {
170 picking_track_index_ < static_cast<int>(tracks_.size())) {
171 tracks_[picking_track_index_].room_id = room_id;
172 tracks_[picking_track_index_].start_x = camera_x;
173 tracks_[picking_track_index_].start_y = camera_y;
174
175 last_picked_x_ = camera_x;
176 last_picked_y_ = camera_y;
177 has_picked_coords_ = true;
178 audit_dirty_ = true;
179
181 absl::StrFormat("Track %d: Set to Room $%04X, Pos ($%04X, $%04X)",
182 picking_track_index_, room_id, camera_x, camera_y);
183 show_success_ = true;
184 }
185
186 // Exit picking mode
187 picking_mode_ = false;
189}
190
192 picking_mode_ = true;
193 picking_track_index_ = track_index;
194 status_message_ = absl::StrFormat(
195 "Click on the dungeon canvas to set Track %d position", track_index);
196 show_success_ = false;
197}
198
204
206 const MinecartTrack& track) const {
207 return track.room_id == kDefaultTrackRoom &&
208 track.start_x == kDefaultTrackX && track.start_y == kDefaultTrackY;
209}
210
212 room_audit_.clear();
213 track_usage_rooms_.clear();
214 track_subtype_used_.assign(kTrackSlotCount, false);
215
216 if (!rooms_) {
217 audit_dirty_ = false;
218 return;
219 }
220
221 std::array<bool, 256> track_tiles{};
222 std::array<bool, 256> stop_tiles{};
223 std::array<bool, 256> switch_tiles{};
224 auto apply_list = [](std::array<bool, 256>& dest,
225 const std::vector<uint16_t>& values) {
226 dest.fill(false);
227 for (uint16_t value : values) {
228 if (value < dest.size()) {
229 dest[value] = true;
230 }
231 }
232 };
233
234 if (project_ && !project_->dungeon_overlay.track_tiles.empty()) {
235 apply_list(track_tiles, project_->dungeon_overlay.track_tiles);
236 } else {
237 std::vector<uint16_t> default_track_tiles;
238 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
239 default_track_tiles.push_back(tile);
240 }
241 apply_list(track_tiles, default_track_tiles);
242 }
243
245 apply_list(stop_tiles, project_->dungeon_overlay.track_stop_tiles);
246 } else {
247 apply_list(stop_tiles, {0xB7, 0xB8, 0xB9, 0xBA});
248 }
249
251 apply_list(switch_tiles, project_->dungeon_overlay.track_switch_tiles);
252 } else {
253 apply_list(switch_tiles, {0xD0, 0xD1, 0xD2, 0xD3});
254 }
255
256 std::vector<uint16_t> track_object_ids = {0x31};
257 std::vector<uint16_t> minecart_sprite_ids = {0xA3};
258 if (project_) {
260 track_object_ids = project_->dungeon_overlay.track_object_ids;
261 }
263 minecart_sprite_ids = project_->dungeon_overlay.minecart_sprite_ids;
264 }
265 }
266
267 std::unordered_map<int, bool> track_object_id_map;
268 for (uint16_t id : track_object_ids) {
269 track_object_id_map[static_cast<int>(id)] = true;
270 }
271 std::unordered_map<int, bool> minecart_sprite_id_map;
272 for (uint16_t id : minecart_sprite_ids) {
273 minecart_sprite_id_map[static_cast<int>(id)] = true;
274 }
275
276 for (int room_id = 0; room_id < static_cast<int>(rooms_->size()); ++room_id) {
277 auto& room = (*rooms_)[room_id];
278 RoomTrackAudit audit;
279
280 if (room.GetTileObjects().empty()) {
281 room.LoadObjects();
282 }
283 if (room.GetSprites().empty()) {
284 room.LoadSprites();
285 }
286
287 std::array<bool, kTrackSlotCount> seen_subtype{};
288
289 for (const auto& obj : room.GetTileObjects()) {
290 if (!track_object_id_map[static_cast<int>(obj.id_)]) {
291 continue;
292 }
293 int subtype = obj.size_ & 0x1F;
294 if (subtype >= 0 && subtype < kTrackSlotCount) {
295 if (!seen_subtype[static_cast<size_t>(subtype)]) {
296 seen_subtype[static_cast<size_t>(subtype)] = true;
297 track_subtype_used_[static_cast<size_t>(subtype)] = true;
298 track_usage_rooms_[subtype].push_back(room_id);
299 audit.track_subtypes.push_back(subtype);
300 }
301 }
302 }
303
304 std::unordered_map<int, bool> stop_positions;
305 auto map_or = zelda3::LoadCustomCollisionMap(room.rom(), room_id);
306 if (map_or.ok() && map_or.value().has_data) {
307 const auto& map = map_or.value().tiles;
308 for (int y = 0; y < 64; ++y) {
309 for (int x = 0; x < 64; ++x) {
310 uint8_t tile = map[static_cast<size_t>(y * 64 + x)];
311 if (track_tiles[tile] || stop_tiles[tile] || switch_tiles[tile]) {
312 audit.has_track_collision = true;
313 }
314 if (stop_tiles[tile]) {
315 audit.has_stop_tiles = true;
316 stop_positions[y * 64 + x] = true;
317 }
318 }
319 }
320 }
321
322 if (audit.has_track_collision) {
323 for (const auto& sprite : room.GetSprites()) {
324 if (!minecart_sprite_id_map[static_cast<int>(sprite.id())]) {
325 continue;
326 }
327 audit.has_minecart_sprite = true;
328 int tile_x = sprite.x() * 2;
329 int tile_y = sprite.y() * 2;
330 if (tile_x >= 0 && tile_x < 64 && tile_y >= 0 && tile_y < 64) {
331 int idx = tile_y * 64 + tile_x;
332 if (stop_positions[idx]) {
333 audit.has_minecart_on_stop = true;
334 }
335 }
336 }
337 }
338
339 if (audit.has_track_collision || !audit.track_subtypes.empty() ||
340 audit.has_minecart_sprite) {
341 room_audit_[room_id] = audit;
342 }
343 }
344
345 audit_dirty_ = false;
346}
347
349 if (project_root_.empty()) {
350 ImGui::TextColored(ImVec4(1, 0, 0, 1), "Project root not set.");
351 return;
352 }
353
354 if (!loaded_) {
355 LoadTracks();
356 }
357
358 if (audit_dirty_) {
360 }
361
362 ImGui::Text("Minecart Track Editor");
363 if (ImGui::Button(ICON_MD_SAVE " Save Tracks")) {
364 SaveTracks();
365 }
366 ImGui::SameLine();
367 const bool can_save_project = project_ && project_->project_opened();
368 if (!can_save_project) {
369 ImGui::BeginDisabled();
370 }
371 if (ImGui::Button(ICON_MD_SAVE " Save Project")) {
372 auto status = project_->Save();
373 if (status.ok()) {
374 status_message_ = "Project saved.";
375 show_success_ = true;
376 } else {
378 absl::StrFormat("Project save failed: %s", status.message());
379 show_success_ = false;
380 }
381 }
382 if (!can_save_project) {
383 ImGui::EndDisabled();
384 }
385
386 // Show picking mode indicator
387 if (picking_mode_) {
388 ImGui::SameLine();
389 if (ImGui::Button(ICON_MD_CANCEL " Cancel Pick")) {
391 }
392 ImGui::SameLine();
393 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
394 ICON_MD_MY_LOCATION " Picking for Track %d...",
396 }
397
398 if (!status_message_.empty() && !picking_mode_) {
399 ImGui::SameLine();
400 ImGui::TextColored(show_success_ ? ImVec4(0, 1, 0, 1) : ImVec4(1, 0, 0, 1),
401 "%s", status_message_.c_str());
402 }
403
405 ImGui::Separator();
406
407 // Coordinate format help
408 ImGui::TextDisabled(
409 "Camera coordinates use $1XXX format (base $1000 + room offset + local "
410 "position)");
411 ImGui::TextDisabled(
412 "Hover over dungeon canvas to see coordinates, or click 'Pick' button.");
413 ImGui::Separator();
414
415 if (ImGui::BeginTable("TracksTable", 7,
416 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
417 ImGuiTableFlags_Resizable)) {
418 ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 30.0f);
419 ImGui::TableSetupColumn("Room ID", ImGuiTableColumnFlags_WidthFixed, 80.0f);
420 ImGui::TableSetupColumn("Camera X", ImGuiTableColumnFlags_WidthFixed,
421 80.0f);
422 ImGui::TableSetupColumn("Camera Y", ImGuiTableColumnFlags_WidthFixed,
423 80.0f);
424 ImGui::TableSetupColumn("Pick", ImGuiTableColumnFlags_WidthFixed, 50.0f);
425 ImGui::TableSetupColumn("Go", ImGuiTableColumnFlags_WidthFixed, 40.0f);
426 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60.0f);
427 ImGui::TableHeadersRow();
428
429 for (auto& track : tracks_) {
430 ImGui::TableNextRow();
431
432 const bool is_default = IsDefaultTrack(track);
433 const bool used_in_rooms =
434 track.id >= 0 &&
435 track.id < static_cast<int>(track_subtype_used_.size()) &&
436 track_subtype_used_[track.id];
437 const bool missing_start = used_in_rooms && is_default;
438
439 if (missing_start) {
440 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
441 IM_COL32(120, 40, 40, 120));
442 } else if (is_default) {
443 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
444 IM_COL32(60, 60, 60, 80));
445 }
446
447 // Highlight the row being picked
448 if (picking_mode_ && track.id == picking_track_index_) {
449 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
450 IM_COL32(80, 80, 0, 100));
451 }
452
453 ImGui::TableNextColumn();
454 ImGui::Text("%d", track.id);
455
456 ImGui::TableNextColumn();
457 uint16_t room_id = static_cast<uint16_t>(track.room_id);
459 absl::StrFormat("##Room%d", track.id).c_str(), &room_id, 60.0f)) {
460 track.room_id = room_id;
461 }
462
463 ImGui::TableNextColumn();
464 uint16_t start_x = static_cast<uint16_t>(track.start_x);
466 absl::StrFormat("##StartX%d", track.id).c_str(), &start_x,
467 60.0f)) {
468 track.start_x = start_x;
469 }
470
471 ImGui::TableNextColumn();
472 uint16_t start_y = static_cast<uint16_t>(track.start_y);
474 absl::StrFormat("##StartY%d", track.id).c_str(), &start_y,
475 60.0f)) {
476 track.start_y = start_y;
477 }
478
479 // Pick button to select coordinates from canvas
480 ImGui::TableNextColumn();
481 ImGui::PushID(track.id);
482 bool is_picking_this = picking_mode_ && picking_track_index_ == track.id;
483 {
484 std::optional<gui::StyleColorGuard> pick_guard;
485 if (is_picking_this) {
486 pick_guard.emplace(ImGuiCol_Button,
487 ImVec4(0.8f, 0.6f, 0.0f, 1.0f));
488 }
489 if (ImGui::SmallButton(ICON_MD_MY_LOCATION)) {
490 if (is_picking_this) {
492 } else {
493 StartCoordinatePicking(track.id);
494 }
495 }
496 }
497 if (ImGui::IsItemHovered()) {
498 ImGui::SetTooltip(is_picking_this ? "Cancel picking"
499 : "Pick coordinates from canvas");
500 }
501 ImGui::PopID();
502
503 // Go to room button
504 ImGui::TableNextColumn();
505 ImGui::PushID(track.id + 1000);
506 if (ImGui::SmallButton(ICON_MD_ARROW_FORWARD)) {
508 room_navigation_callback_(track.room_id);
509 }
510 }
511 if (ImGui::IsItemHovered()) {
512 ImGui::SetTooltip("Navigate to room $%04X", track.room_id);
513 }
514 ImGui::PopID();
515
516 // Status column
517 ImGui::TableNextColumn();
518 if (missing_start) {
519 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
521 } else if (is_default) {
522 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MD_INFO);
523 } else if (used_in_rooms) {
524 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f),
526 } else {
527 ImGui::Text("-");
528 }
529
530 if (ImGui::IsItemHovered()) {
531 ImGui::BeginTooltip();
532 if (missing_start) {
533 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
534 "Used in rooms but still default");
535 } else if (is_default) {
536 ImGui::Text("Default filler slot");
537 } else if (used_in_rooms) {
538 ImGui::Text("Used in rooms");
539 } else {
540 ImGui::Text("No usage detected");
541 }
542
543 auto rooms_it = track_usage_rooms_.find(track.id);
544 if (rooms_it != track_usage_rooms_.end()) {
545 ImGui::Separator();
546 ImGui::Text("Rooms:");
547 for (int room_id : rooms_it->second) {
548 ImGui::BulletText("0x%03X", room_id);
549 }
550 }
551 ImGui::EndTooltip();
552 }
553 }
554
555 ImGui::EndTable();
556 }
557
558 // Summary + room audit
559 int default_count = 0;
560 int used_count = 0;
561 int missing_start_count = 0;
562 for (const auto& track : tracks_) {
563 bool is_default = IsDefaultTrack(track);
564 bool used_in_rooms =
565 track.id >= 0 &&
566 track.id < static_cast<int>(track_subtype_used_.size()) &&
567 track_subtype_used_[track.id];
568 if (is_default) {
569 default_count++;
570 }
571 if (used_in_rooms) {
572 used_count++;
573 }
574 if (used_in_rooms && is_default) {
575 missing_start_count++;
576 }
577 }
578
579 ImGui::Separator();
580 ImGui::Text("Usage Summary: used %d/%d, default %d, missing starts %d",
581 used_count, kTrackSlotCount, default_count, missing_start_count);
582
583 if (!room_audit_.empty()) {
584 ImGui::Separator();
585 ImGui::Text("Rooms with track objects:");
586
587 // "Generate All" button: batch-generate collision for all rooms that have
588 // rail objects but no collision data yet.
589 if (rom_ && rooms_) {
590 // Count rooms that need generation
591 int rooms_needing_collision = 0;
592 for (const auto& [rid, audit] : room_audit_) {
593 if (!audit.track_subtypes.empty() && !audit.has_track_collision) {
594 rooms_needing_collision++;
595 }
596 }
597
598 if (rooms_needing_collision > 0) {
599 if (ImGui::Button(
600 absl::StrFormat(ICON_MD_AUTO_FIX_HIGH
601 " Generate All (%d rooms)",
602 rooms_needing_collision).c_str())) {
603 int generated_rooms = 0;
604 int total_tiles = 0;
605 bool had_error = false;
606
607 for (auto& [rid, audit] : room_audit_) {
608 if (audit.track_subtypes.empty() || audit.has_track_collision) {
609 continue;
610 }
611
612 auto& target_room = (*rooms_)[rid];
614 auto gen_result =
615 zelda3::GenerateTrackCollision(&target_room, opts);
616 if (!gen_result.ok()) {
617 status_message_ = absl::StrFormat(
618 "Generate failed for room 0x%03X: %s", rid,
619 gen_result.status().message());
620 show_success_ = false;
621 had_error = true;
622 break;
623 }
624
625 auto write_status = zelda3::WriteTrackCollision(
626 rom_, rid, gen_result->collision_map);
627 if (!write_status.ok()) {
628 status_message_ = absl::StrFormat(
629 "Write failed for room 0x%03X: %s", rid,
630 write_status.message());
631 show_success_ = false;
632 had_error = true;
633 break;
634 }
635
636 generated_rooms++;
637 total_tiles += gen_result->tiles_generated;
638 }
639
640 if (!had_error) {
641 status_message_ = absl::StrFormat(
642 "Generated collision for %d rooms (%d tiles total)",
643 generated_rooms, total_tiles);
644 show_success_ = true;
645 }
646 audit_dirty_ = true;
647 }
648 if (ImGui::IsItemHovered()) {
649 ImGui::SetTooltip(
650 "Generate collision for all %d rooms with rail objects "
651 "but no collision data",
652 rooms_needing_collision);
653 }
654 }
655 }
656
657 ImGui::BeginChild("##TrackAuditRooms", ImVec2(0, 160), true);
658 for (const auto& [room_id, audit] : room_audit_) {
659 if (audit.track_subtypes.empty() && !audit.has_track_collision) {
660 continue;
661 }
662
663 // Status icon
664 if (!audit.has_track_collision) {
665 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.1f, 1.0f),
666 ICON_MD_ERROR " Room 0x%03X (no collision)",
667 room_id);
668 } else if (!audit.has_minecart_on_stop) {
669 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
670 ICON_MD_WARNING_AMBER " Room 0x%03X (no cart on stop)",
671 room_id);
672 } else {
673 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f),
674 ICON_MD_CHECK_CIRCLE " Room 0x%03X", room_id);
675 }
676
677 ImGui::SameLine();
678 ImGui::PushID(room_id);
679 if (ImGui::SmallButton(ICON_MD_ARROW_FORWARD)) {
682 }
683 }
684 if (ImGui::IsItemHovered()) {
685 ImGui::SetTooltip("Navigate to room 0x%03X", room_id);
686 }
687
688 // Generate Collision button (only if rom available and no collision yet)
689 if (rom_ && rooms_ && !audit.has_track_collision) {
690 ImGui::SameLine();
691 if (ImGui::SmallButton(
692 absl::StrFormat(ICON_MD_AUTO_FIX_HIGH " Generate##%d",
693 room_id).c_str())) {
694 auto& target_room = (*rooms_)[room_id];
696 auto gen_result =
697 zelda3::GenerateTrackCollision(&target_room, opts);
698 if (gen_result.ok()) {
699 auto write_status = zelda3::WriteTrackCollision(
700 rom_, room_id, gen_result->collision_map);
701 if (write_status.ok()) {
702 status_message_ = absl::StrFormat(
703 "Room 0x%03X: Generated %d tiles (%d stops, %d corners)",
704 room_id, gen_result->tiles_generated,
705 gen_result->stop_count, gen_result->corner_count);
706 show_success_ = true;
707 audit_dirty_ = true;
708 } else {
709 status_message_ = absl::StrFormat(
710 "Write failed: %s", write_status.message());
711 show_success_ = false;
712 }
713 } else {
714 status_message_ = absl::StrFormat(
715 "Generate failed: %s", gen_result.status().message());
716 show_success_ = false;
717 }
718 }
719 if (ImGui::IsItemHovered()) {
720 ImGui::SetTooltip(
721 "Auto-generate collision tiles from rail objects in this room");
722 }
723 }
724
725 ImGui::PopID();
726 }
727 ImGui::EndChild();
728 }
729}
730
732 std::filesystem::path path = std::filesystem::path(project_root_) /
733 "Sprites/Objects/data/minecart_tracks.asm";
734
735 if (!std::filesystem::exists(path)) {
736 status_message_ = "File not found: " + path.string();
737 show_success_ = false;
738 loaded_ = true; // Prevent retry loop
739 tracks_.clear();
740 return;
741 }
742
743 std::ifstream file(path);
744 std::stringstream buffer;
745 buffer << file.rdbuf();
746 std::string content = buffer.str();
747
748 std::vector<int> rooms;
749 std::vector<int> xs;
750 std::vector<int> ys;
751
752 if (!ParseSection(content, ".TrackStartingRooms", rooms) ||
753 !ParseSection(content, ".TrackStartingX", xs) ||
754 !ParseSection(content, ".TrackStartingY", ys)) {
755 status_message_ = "Error parsing file format.";
756 show_success_ = false;
757 } else {
758 tracks_.clear();
759 size_t count = std::min({rooms.size(), xs.size(), ys.size()});
760 for (size_t i = 0; i < count; ++i) {
761 tracks_.push_back({(int)i, rooms[i], xs[i], ys[i]});
762 }
763 while (tracks_.size() < kTrackSlotCount) {
764 tracks_.push_back({static_cast<int>(tracks_.size()), kDefaultTrackRoom,
765 kDefaultTrackX, kDefaultTrackY});
766 }
767 status_message_ = "";
768 show_success_ = true;
769 }
770 audit_dirty_ = true;
771 loaded_ = true;
772}
773
774bool MinecartTrackEditorPanel::ParseSection(const std::string& content,
775 const std::string& label,
776 std::vector<int>& out_values) {
777 size_t pos = content.find(label);
778 if (pos == std::string::npos)
779 return false;
780
781 // Start searching after the label
782 size_t start = pos + label.length();
783
784 // Find lines starting with 'dw'
785 std::regex dw_regex(R"(dw\s+((?:\$[0-9A-Fa-f]{4}(?:,\s*)?)+))");
786
787 // Create a substring from start to end or next label (simplified: just search until next dot label or end)
788 // Actually, searching line by line is safer.
789 std::stringstream ss(content.substr(start));
790 std::string line;
791 while (std::getline(ss, line)) {
792 // Stop if we hit another label
793 size_t trimmed_start = line.find_first_not_of(" \t");
794 if (trimmed_start != std::string::npos && line[trimmed_start] == '.')
795 break;
796
797 std::smatch match;
798 if (std::regex_search(line, match, dw_regex)) {
799 std::string values_str = match[1];
800 std::stringstream val_ss(values_str);
801 std::string segment;
802 while (std::getline(val_ss, segment, ',')) {
803 // Trim
804 segment.erase(0, segment.find_first_not_of(" \t$"));
805 // Parse hex
806 try {
807 out_values.push_back(std::stoi(segment, nullptr, 16));
808 } catch (...) {}
809 }
810 }
811 }
812 return true;
813}
814
816 std::filesystem::path path = std::filesystem::path(project_root_) /
817 "Sprites/Objects/data/minecart_tracks.asm";
818
819 std::ofstream file(path);
820 if (!file.is_open()) {
821 status_message_ = "Failed to open file for writing.";
822 show_success_ = false;
823 return;
824 }
825
826 std::vector<int> rooms, xs, ys;
827 for (const auto& t : tracks_) {
828 rooms.push_back(t.room_id);
829 xs.push_back(t.start_x);
830 ys.push_back(t.start_y);
831 }
832
833 file << " ; This is which room each track should start in if it hasn't "
834 "already\n";
835 file << " ; been given a track.\n";
836 file << FormatSection(".TrackStartingRooms", rooms);
837 file << "\n";
838
839 file << " ; This is where within the room each track should start in if it "
840 "hasn't\n";
841 file << " ; already been given a position. This is necessary to allow for "
842 "more\n";
843 file << " ; than one stopping point to be in one room.\n";
844 file << FormatSection(".TrackStartingX", xs);
845 file << "\n";
846
847 file << FormatSection(".TrackStartingY", ys);
848
849 status_message_ = "Tracks saved successfully!";
850 show_success_ = true;
851}
852
854 const std::string& label, const std::vector<int>& values) {
855 std::stringstream ss;
856 ss << " " << label << "\n";
857
858 for (size_t i = 0; i < values.size(); i += 8) {
859 ss << " dw ";
860 for (size_t j = 0; j < 8 && i + j < values.size(); ++j) {
861 if (j > 0)
862 ss << ", ";
863 ss << absl::StrFormat("$%04X", values[i + j]);
864 }
865 ss << "\n";
866 }
867 return ss.str();
868}
869
870} // namespace yaze::editor
std::string FormatSection(const std::string &label, const std::vector< int > &values)
bool IsDefaultTrack(const MinecartTrack &track) const
void Draw(bool *p_open) override
Draw the panel content.
const std::vector< MinecartTrack > & GetTracks()
std::unordered_map< int, RoomTrackAudit > room_audit_
std::array< zelda3::Room, 0x128 > * rooms_
std::unordered_map< int, std::vector< int > > track_usage_rooms_
bool ParseSection(const std::string &content, const std::string &label, std::vector< int > &out_values)
void SetPickedCoordinates(int room_id, uint16_t camera_x, uint16_t camera_y)
bool UpdateOverlayList(const char *label, std::string &input, std::vector< uint16_t > &target)
#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_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_SAVE
Definition icons.h:1644
Editors are the view controllers for the application.
bool InputHexWordCustom(const char *label, uint16_t *data, float input_width)
Definition input.cc:726
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)
std::vector< uint16_t > track_object_ids
Definition project.h:98
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:99
std::vector< uint16_t > track_stop_tiles
Definition project.h:94
std::vector< uint16_t > track_tiles
Definition project.h:93
std::vector< uint16_t > track_switch_tiles
Definition project.h:95
bool project_opened() const
Definition project.h:285
DungeonOverlaySettings dungeon_overlay
Definition project.h:150