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) {
226 for (uint16_t value : values) {
227 if (value < dest.size()) {
236 std::vector<uint16_t> default_track_tiles;
237 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
238 default_track_tiles.push_back(tile);
240 apply_list(track_tiles, default_track_tiles);
246 apply_list(stop_tiles, {0xB7, 0xB8, 0xB9, 0xBA});
252 apply_list(switch_tiles, {0xD0, 0xD1, 0xD2, 0xD3});
255 std::vector<uint16_t> track_object_ids = {0x31};
256 std::vector<uint16_t> minecart_sprite_ids = {0xA3};
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;
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;
275 for (
int room_id = 0; room_id < static_cast<int>(
rooms_->
size()); ++room_id) {
276 auto& room = (*rooms_)[room_id];
279 if (room.GetTileObjects().empty()) {
282 if (room.GetSprites().empty()) {
286 std::array<bool, kTrackSlotCount> seen_subtype{};
288 for (
const auto& obj : room.GetTileObjects()) {
289 if (!track_object_id_map[
static_cast<int>(obj.id_)]) {
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;
303 std::unordered_map<int, bool> stop_positions;
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]) {
313 if (stop_tiles[tile]) {
315 stop_positions[y * 64 + x] =
true;
322 for (
const auto& sprite : room.GetSprites()) {
323 if (!minecart_sprite_id_map[
static_cast<int>(sprite.id())]) {
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]) {
349 ImGui::TextColored(ImVec4(1, 0, 0, 1), tr(
"Project root not set."));
361 ImGui::Text(tr(
"Minecart Track Editor"));
367 if (!can_save_project) {
368 ImGui::BeginDisabled();
377 absl::StrFormat(
"Project save failed: %s", status.message());
381 if (!can_save_project) {
382 ImGui::EndDisabled();
392 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
399 ImGui::TextColored(
show_success_ ? ImVec4(0, 1, 0, 1) : ImVec4(1, 0, 0, 1),
407 ImGui::TextDisabled(tr(
408 "Camera coordinates use $1XXX format (base $1000 + room offset + local "
410 ImGui::TextDisabled(tr(
411 "Hover over dungeon canvas to see coordinates, or click 'Pick' button."));
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,
421 ImGui::TableSetupColumn(
"Camera Y", ImGuiTableColumnFlags_WidthFixed,
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();
429 ImGui::TableNextRow();
432 const bool used_in_rooms =
436 const bool missing_start = used_in_rooms && is_default;
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));
448 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
449 IM_COL32(80, 80, 0, 100));
452 ImGui::TableNextColumn();
453 ImGui::Text(
"%d", track.id);
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;
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,
467 track.start_x = start_x;
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,
475 track.start_y = start_y;
479 ImGui::TableNextColumn();
480 ImGui::PushID(track.id);
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));
488 if (is_picking_this) {
495 if (ImGui::IsItemHovered()) {
496 ImGui::SetTooltip(is_picking_this ?
"Cancel picking"
497 :
"Pick coordinates from canvas");
502 ImGui::TableNextColumn();
503 ImGui::PushID(track.id + 1000);
509 if (ImGui::IsItemHovered()) {
510 ImGui::SetTooltip(tr(
"Navigate to room $%04X"), track.room_id);
515 ImGui::TableNextColumn();
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),
528 if (ImGui::IsItemHovered()) {
529 ImGui::BeginTooltip();
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"));
538 ImGui::Text(tr(
"No usage detected"));
544 ImGui::Text(tr(
"Rooms:"));
545 for (
int room_id : rooms_it->second) {
546 ImGui::BulletText(tr(
"0x%03X"), room_id);
557 int default_count = 0;
559 int missing_start_count = 0;
560 for (
const auto& track :
tracks_) {
572 if (used_in_rooms && is_default) {
573 missing_start_count++;
578 ImGui::Text(tr(
"Usage Summary: used %d/%d, default %d, missing starts %d"),
579 used_count, kTrackSlotCount, default_count, missing_start_count);
583 ImGui::Text(tr(
"Rooms with track objects:"));
589 int rooms_needing_collision = 0;
591 if (!audit.track_subtypes.empty() && !audit.has_track_collision) {
592 rooms_needing_collision++;
596 if (rooms_needing_collision > 0) {
598 " Generate All (%d rooms)",
599 rooms_needing_collision)
601 int generated_rooms = 0;
603 bool had_error =
false;
606 if (audit.track_subtypes.empty() || audit.has_track_collision) {
610 auto& target_room = (*rooms_)[rid];
614 if (!gen_result.ok()) {
616 absl::StrFormat(
"Generate failed for room 0x%03X: %s", rid,
617 gen_result.status().message());
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());
635 total_tiles += gen_result->tiles_generated;
640 "Generated collision for %d rooms (%d tiles total)",
641 generated_rooms, total_tiles);
646 if (ImGui::IsItemHovered()) {
648 tr(
"Generate collision for all %d rooms with rail objects "
649 "but no collision data"),
650 rooms_needing_collision);
655 ImGui::BeginChild(
"##TrackAuditRooms", ImVec2(0, 160),
true);
657 if (audit.track_subtypes.empty() && !audit.has_track_collision) {
662 if (!audit.has_track_collision) {
663 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.1f, 1.0f),
666 }
else if (!audit.has_minecart_on_stop) {
668 ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
671 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f),
676 ImGui::PushID(room_id);
682 if (ImGui::IsItemHovered()) {
683 ImGui::SetTooltip(tr(
"Navigate to room 0x%03X"), room_id);
687 if (
rom_ &&
rooms_ && !audit.has_track_collision) {
689 if (ImGui::SmallButton(
692 auto& target_room = (*rooms_)[room_id];
695 if (gen_result.ok()) {
697 rom_, room_id, gen_result->collision_map);
698 if (write_status.ok()) {
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);
707 absl::StrFormat(
"Write failed: %s", write_status.message());
712 gen_result.status().message());
716 if (ImGui::IsItemHovered()) {
717 ImGui::SetTooltip(tr(
718 "Auto-generate collision tiles from rail objects in this room"));
772 const std::string& label,
773 std::vector<int>& out_values) {
774 size_t pos = content.find(label);
775 if (pos == std::string::npos)
779 size_t start = pos + label.length();
782 std::regex dw_regex(R
"(dw\s+((?:\$[0-9A-Fa-f]{4}(?:,\s*)?)+))");
786 std::stringstream ss(content.substr(start));
788 while (std::getline(ss, line)) {
790 size_t trimmed_start = line.find_first_not_of(
" \t");
791 if (trimmed_start != std::string::npos && line[trimmed_start] ==
'.')
795 if (std::regex_search(line, match, dw_regex)) {
796 std::string values_str = match[1];
797 std::stringstream val_ss(values_str);
799 while (std::getline(val_ss, segment,
',')) {
801 segment.erase(0, segment.find_first_not_of(
" \t$"));
804 out_values.push_back(std::stoi(segment,
nullptr, 16));