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) {
227 for (uint16_t value : values) {
228 if (value < dest.size()) {
237 std::vector<uint16_t> default_track_tiles;
238 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
239 default_track_tiles.push_back(tile);
241 apply_list(track_tiles, default_track_tiles);
247 apply_list(stop_tiles, {0xB7, 0xB8, 0xB9, 0xBA});
253 apply_list(switch_tiles, {0xD0, 0xD1, 0xD2, 0xD3});
256 std::vector<uint16_t> track_object_ids = {0x31};
257 std::vector<uint16_t> minecart_sprite_ids = {0xA3};
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;
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;
276 for (
int room_id = 0; room_id < static_cast<int>(
rooms_->size()); ++room_id) {
277 auto& room = (*rooms_)[room_id];
280 if (room.GetTileObjects().empty()) {
283 if (room.GetSprites().empty()) {
287 std::array<bool, kTrackSlotCount> seen_subtype{};
289 for (
const auto& obj : room.GetTileObjects()) {
290 if (!track_object_id_map[
static_cast<int>(obj.id_)]) {
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;
304 std::unordered_map<int, bool> stop_positions;
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]) {
314 if (stop_tiles[tile]) {
316 stop_positions[y * 64 + x] =
true;
323 for (
const auto& sprite : room.GetSprites()) {
324 if (!minecart_sprite_id_map[
static_cast<int>(sprite.id())]) {
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]) {
350 ImGui::TextColored(ImVec4(1, 0, 0, 1),
"Project root not set.");
362 ImGui::Text(
"Minecart Track Editor");
368 if (!can_save_project) {
369 ImGui::BeginDisabled();
378 absl::StrFormat(
"Project save failed: %s", status.message());
382 if (!can_save_project) {
383 ImGui::EndDisabled();
393 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
400 ImGui::TextColored(
show_success_ ? ImVec4(0, 1, 0, 1) : ImVec4(1, 0, 0, 1),
409 "Camera coordinates use $1XXX format (base $1000 + room offset + local "
412 "Hover over dungeon canvas to see coordinates, or click 'Pick' button.");
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,
422 ImGui::TableSetupColumn(
"Camera Y", ImGuiTableColumnFlags_WidthFixed,
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();
430 ImGui::TableNextRow();
433 const bool used_in_rooms =
437 const bool missing_start = used_in_rooms && is_default;
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));
449 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
450 IM_COL32(80, 80, 0, 100));
453 ImGui::TableNextColumn();
454 ImGui::Text(
"%d", track.id);
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;
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,
468 track.start_x = start_x;
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,
476 track.start_y = start_y;
480 ImGui::TableNextColumn();
481 ImGui::PushID(track.id);
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));
490 if (is_picking_this) {
497 if (ImGui::IsItemHovered()) {
498 ImGui::SetTooltip(is_picking_this ?
"Cancel picking"
499 :
"Pick coordinates from canvas");
504 ImGui::TableNextColumn();
505 ImGui::PushID(track.id + 1000);
511 if (ImGui::IsItemHovered()) {
512 ImGui::SetTooltip(
"Navigate to room $%04X", track.room_id);
517 ImGui::TableNextColumn();
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),
530 if (ImGui::IsItemHovered()) {
531 ImGui::BeginTooltip();
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");
540 ImGui::Text(
"No usage detected");
546 ImGui::Text(
"Rooms:");
547 for (
int room_id : rooms_it->second) {
548 ImGui::BulletText(
"0x%03X", room_id);
559 int default_count = 0;
561 int missing_start_count = 0;
562 for (
const auto& track :
tracks_) {
574 if (used_in_rooms && is_default) {
575 missing_start_count++;
580 ImGui::Text(
"Usage Summary: used %d/%d, default %d, missing starts %d",
581 used_count, kTrackSlotCount, default_count, missing_start_count);
585 ImGui::Text(
"Rooms with track objects:");
591 int rooms_needing_collision = 0;
593 if (!audit.track_subtypes.empty() && !audit.has_track_collision) {
594 rooms_needing_collision++;
598 if (rooms_needing_collision > 0) {
601 " Generate All (%d rooms)",
602 rooms_needing_collision).c_str())) {
603 int generated_rooms = 0;
605 bool had_error =
false;
608 if (audit.track_subtypes.empty() || audit.has_track_collision) {
612 auto& target_room = (*rooms_)[rid];
616 if (!gen_result.ok()) {
618 "Generate failed for room 0x%03X: %s", rid,
619 gen_result.status().message());
626 rom_, rid, gen_result->collision_map);
627 if (!write_status.ok()) {
629 "Write failed for room 0x%03X: %s", rid,
630 write_status.message());
637 total_tiles += gen_result->tiles_generated;
642 "Generated collision for %d rooms (%d tiles total)",
643 generated_rooms, total_tiles);
648 if (ImGui::IsItemHovered()) {
650 "Generate collision for all %d rooms with rail objects "
651 "but no collision data",
652 rooms_needing_collision);
657 ImGui::BeginChild(
"##TrackAuditRooms", ImVec2(0, 160),
true);
659 if (audit.track_subtypes.empty() && !audit.has_track_collision) {
664 if (!audit.has_track_collision) {
665 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.1f, 1.0f),
668 }
else if (!audit.has_minecart_on_stop) {
669 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
673 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f),
678 ImGui::PushID(room_id);
684 if (ImGui::IsItemHovered()) {
685 ImGui::SetTooltip(
"Navigate to room 0x%03X", room_id);
689 if (
rom_ &&
rooms_ && !audit.has_track_collision) {
691 if (ImGui::SmallButton(
694 auto& target_room = (*rooms_)[room_id];
698 if (gen_result.ok()) {
700 rom_, room_id, gen_result->collision_map);
701 if (write_status.ok()) {
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);
710 "Write failed: %s", write_status.message());
715 "Generate failed: %s", gen_result.status().message());
719 if (ImGui::IsItemHovered()) {
721 "Auto-generate collision tiles from rail objects in this room");
775 const std::string& label,
776 std::vector<int>& out_values) {
777 size_t pos = content.find(label);
778 if (pos == std::string::npos)
782 size_t start = pos + label.length();
785 std::regex dw_regex(R
"(dw\s+((?:\$[0-9A-Fa-f]{4}(?:,\s*)?)+))");
789 std::stringstream ss(content.substr(start));
791 while (std::getline(ss, line)) {
793 size_t trimmed_start = line.find_first_not_of(
" \t");
794 if (trimmed_start != std::string::npos && line[trimmed_start] ==
'.')
798 if (std::regex_search(line, match, dw_regex)) {
799 std::string values_str = match[1];
800 std::stringstream val_ss(values_str);
802 while (std::getline(val_ss, segment,
',')) {
804 segment.erase(0, segment.find_first_not_of(
" \t$"));
807 out_values.push_back(std::stoi(segment,
nullptr, 16));