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