17#include "absl/status/status.h"
18#include "absl/strings/str_cat.h"
19#include "absl/strings/str_format.h"
20#include "absl/time/clock.h"
21#include "absl/time/time.h"
30#include "imgui/imgui.h"
31#include "imgui/imgui_internal.h"
59 "Local draft. Nothing is uploaded or submitted.";
61 "Screenshot captured. Open Local files for its path.";
64 "Copied report to clipboard. Nothing was submitted.";
66 "Copied diagnostics to clipboard. Nothing was submitted.";
85 const ImGuiViewport* viewport = ImGui::GetMainViewport();
86 const ImVec2 work_size =
87 viewport !=
nullptr ? viewport->WorkSize : ImGui::GetIO().DisplaySize;
88 const ImVec2 center = viewport !=
nullptr
89 ? viewport->GetWorkCenter()
90 : ImVec2(work_size.x * 0.5f, work_size.y * 0.5f);
91 const ImVec2 max_size(
96 const ImVec2 initial_size(
99 return {center, initial_size, min_size, max_size};
103 float text_width = ImGui::CalcTextSize(label).x;
110 return text_width + ImGui::GetStyle().FramePadding.x * 2.0f;
114 float available_width) {
115 const float spacing = ImGui::GetStyle().ItemSpacing.x;
116 float used_width = 0.0f;
117 int rows = labels.empty() ? 0 : 1;
118 for (
const char* label : labels) {
120 if (used_width > 0.0f &&
121 used_width + spacing + button_width > available_width) {
123 used_width = button_width;
125 used_width += (used_width > 0.0f ? spacing : 0.0f) + button_width;
133 const float spacing = ImGui::GetStyle().ItemSpacing.x;
135 if (*used_width > 0.0f &&
136 *used_width + spacing + button_width <= available_width) {
138 *used_width += spacing + button_width;
141 *used_width = button_width;
145 const int layer_value =
object.GetLayerValue();
147 if (layer_value == 0) {
148 return "Upper layer (BG1)";
150 return layer_value == 1 ?
"Lower layer (BG2)" :
"Invalid selector";
152 switch (layer_value) {
156 return "BG2 overlay";
158 return "BG1 overlay";
165 static const char* kGroupNames[] = {
166 "HC/Sewers",
"Eastern",
"Desert",
"Hera",
"A-Tower",
"PoD",
"Swamp",
167 "Skull",
"Thieves",
"Ice",
"Misery",
"Turtle",
"GT",
169 constexpr size_t kCount =
sizeof(kGroupNames) /
sizeof(kGroupNames[0]);
170 return blockset < kCount ? kGroupNames[blockset] :
"Custom";
174 return absl::FormatTime(
"%Y%m%d-%H%M%S", absl::Now(), absl::LocalTimeZone());
178 return absl::FormatTime(
"%Y-%m-%d %H:%M:%S %Z", absl::Now(),
179 absl::LocalTimeZone());
183 const auto blocks = room.
blocks();
184 if (blocks.size() < 8) {
185 return "BG sheets: unavailable";
187 return absl::StrFormat(
188 "BG sheets: [%02X %02X %02X %02X %02X %02X %02X %02X]",
189 static_cast<int>(blocks[0]),
static_cast<int>(blocks[1]),
190 static_cast<int>(blocks[2]),
static_cast<int>(blocks[3]),
191 static_cast<int>(blocks[4]),
static_cast<int>(blocks[5]),
192 static_cast<int>(blocks[6]),
static_cast<int>(blocks[7]));
198 return absl::StrFormat(
199 "storage=special-table selector=%d (%s) routine=%d "
200 "legacy_both_bgs=%s "
203 semantics.draws_to_both_bgs ?
"yes" :
"no",
206 return absl::StrFormat(
207 "stream=%s draw_layer=%d effective_bg=%s routine=%d "
208 "legacy_both_bgs=%s "
214 semantics.routine_id, semantics.draws_to_both_bgs ?
"yes" :
"no",
231 size_t max_count, std::string_view prefix) {
232 std::string out = absl::StrFormat(
"%s count=%zu", std::string(prefix).c_str(),
234 const size_t preview_count = std::min<size_t>(tiles.size(), max_count);
235 for (
size_t i = 0; i < preview_count; ++i) {
236 const auto& tile = tiles[i];
237 out += absl::StrFormat(
" [%zu]=0x%03X/p%d/pr%d/h%d/v%d", i, tile.id_,
238 static_cast<int>(tile.palette_), tile.over_ ? 1 : 0,
239 tile.horizontal_mirror_ ? 1 : 0,
240 tile.vertical_mirror_ ? 1 : 0);
242 if (tiles.size() > preview_count) {
243 out += absl::StrFormat(
" ... %zu more", tiles.size() - preview_count);
250 std::vector<zelda3::ObjectDrawer::TileTrace>
writes;
258 if (bitmap !=
nullptr) {
259 gfx::Arena::Get().RetireBitmap(*bitmap);
268 if (rom ==
nullptr || !rom->
is_loaded()) {
269 return {.
summary =
"\nDrawer trace: unavailable (ROM not loaded)"};
273 if (selected_index >= objects.size()) {
274 return {.summary =
"\nDrawer trace: unavailable (stale selection)"};
278 "\nDrawer trace: unavailable (special-table object uses the "
279 "dedicated room pass)"};
294 for (
auto* buffer : {&bg1, &bg2}) {
295 buffer->EnsureBitmapInitialized();
296 buffer->bitmap().Fill(255);
297 buffer->ClearBuffer();
302 std::vector<zelda3::ObjectDrawer::TileTrace> trace;
303 absl::Status status = absl::NotFoundError(
"selected object not replayed");
304 bool selected_was_replayed =
false;
305 for (uint8_t pass = 0; pass < 3 && !selected_was_replayed; ++pass) {
306 for (
size_t index = 0; index < objects.size(); ++index) {
307 const auto& stored_obj = objects[index];
311 const uint8_t list_index =
312 std::min<uint8_t>(stored_obj.GetLayerValue(), 2);
313 if (list_index != pass) {
319 if (index == selected_index) {
322 drawer.
DrawObject(render_obj, bg1, bg2, palette_group,
325 selected_was_replayed =
true;
330 drawer.
DrawObject(render_obj, bg1, bg2, palette_group,
336 if (!status.ok() && !selected_was_replayed) {
341 return {.summary = absl::StrFormat(
"\nDrawer trace: unavailable (%s)",
342 std::string(status.message()).c_str())};
346 return {.summary =
"\nDrawer trace: status=ok writes=0"};
349 int min_x = std::numeric_limits<int>::max();
350 int min_y = std::numeric_limits<int>::max();
351 int max_x = std::numeric_limits<int>::min();
352 int max_y = std::numeric_limits<int>::min();
353 std::array<int, 3> layer_counts = {0, 0, 0};
354 std::set<std::tuple<int, int, uint8_t>> unique_cells;
355 for (
const auto& write : trace) {
356 min_x = std::min<int>(min_x, write.x_tile);
357 min_y = std::min<int>(min_y, write.y_tile);
358 max_x = std::max<int>(max_x, write.x_tile);
359 max_y = std::max<int>(max_y, write.y_tile);
360 if (write.layer < layer_counts.size()) {
361 ++layer_counts[write.layer];
363 unique_cells.insert({write.x_tile, write.y_tile, write.layer});
366 std::string out = absl::StrFormat(
367 "\nDrawer trace: status=ok context=room-stream-prefix+layout-tilewords "
368 "writes=%zu unique_cells=%zu "
369 "bounds_tiles=(%d,%d)..(%d,%d) layer_counts BG1=%d BG2=%d BG3=%d",
370 trace.size(), unique_cells.size(), min_x, min_y, max_x, max_y,
375 const size_t preview_count = std::min<size_t>(trace.size(), 12);
376 for (
size_t i = 0; i < preview_count; ++i) {
377 const auto& write = trace[i];
378 const int palette = (write.flags >> 3) & 0x7;
379 out += absl::StrFormat(
380 "\n write[%zu] %s tile=(%d,%d) id=0x%03X pal=%d pri=%d h=%d v=%d", i,
381 TraceLayerLabel(write.layer), write.x_tile, write.y_tile, write.tile_id,
382 palette, (write.flags & 0x4) ? 1 : 0, (write.flags & 0x1) ? 1 : 0,
383 (write.flags & 0x2) ? 1 : 0);
385 if (trace.size() > preview_count) {
386 out += absl::StrFormat(
"\n ... %zu more write(s)",
387 trace.size() - preview_count);
389 return {.summary = std::move(out), .writes = std::move(trace)};
399 uint8_t trace_flags) {
400 if ((palette_index & 0x0F) == 0) {
403 const uint8_t trace_palette =
static_cast<uint8_t
>((trace_flags >> 3) & 0x7);
404 return static_cast<uint8_t
>((palette_index >> 4) & 0x7) == trace_palette;
408 std::span<const zelda3::ObjectDrawer::TileTrace> trace,
410 constexpr std::array<std::pair<int, int>, 6> kPreferredOffsets = {
411 std::pair{4, 4}, std::pair{3, 3}, std::pair{0, 0},
412 std::pair{7, 7}, std::pair{0, 7}, std::pair{7, 0},
415 for (
size_t i = 0; i < trace.size(); ++i) {
416 const auto& write = trace[i];
417 const int tile_origin_x = write.x_tile * 8;
418 const int tile_origin_y = write.y_tile * 8;
419 const auto label = absl::StrFormat(
420 "object-trace[%zu] tile=(%d,%d) pal=%d", i, write.x_tile, write.y_tile,
421 static_cast<int>((write.flags >> 3) & 0x7));
423 for (
const auto& [dx, dy] : kPreferredOffsets) {
424 const int sample_x = tile_origin_x + dx;
425 const int sample_y = tile_origin_y + dy;
426 const auto comp = palette_debugger.
SamplePixelAt(sample_x, sample_y);
432 for (
int dy = 0; dy < 8; ++dy) {
433 for (
int dx = 0; dx < 8; ++dx) {
434 const int sample_x = tile_origin_x + dx;
435 const int sample_y = tile_origin_y + dy;
436 const auto comp = palette_debugger.
SamplePixelAt(sample_x, sample_y);
439 .
label = label, .x = sample_x, .y = sample_y};
445 if (!trace.empty()) {
446 const auto& write = trace.front();
448 .
label = absl::StrFormat(
"object-trace[0] tile=(%d,%d) pal=%d fallback",
449 write.x_tile, write.y_tile,
450 static_cast<int>((write.flags >> 3) & 0x7)),
451 .x = write.x_tile * 8 + 4,
452 .y = write.y_tile * 8 + 4};
459 std::span<const zelda3::ObjectDrawer::TileTrace> trace,
int selection_x_px,
460 int selection_y_px,
int selection_w_px,
int selection_h_px) {
462 return "trace: writes=0";
465 int min_x = std::numeric_limits<int>::max();
466 int min_y = std::numeric_limits<int>::max();
467 int max_x = std::numeric_limits<int>::min();
468 int max_y = std::numeric_limits<int>::min();
469 std::set<std::tuple<int, int, uint8_t>> unique_cells;
470 for (
const auto& write : trace) {
471 min_x = std::min<int>(min_x, write.x_tile);
472 min_y = std::min<int>(min_y, write.y_tile);
473 max_x = std::max<int>(max_x, write.x_tile);
474 max_y = std::max<int>(max_y, write.y_tile);
475 unique_cells.insert({write.x_tile, write.y_tile, write.layer});
478 const int trace_x_px = min_x * 8;
479 const int trace_y_px = min_y * 8;
480 const int trace_w_px = (max_x - min_x + 1) * 8;
481 const int trace_h_px = (max_y - min_y + 1) * 8;
482 return absl::StrFormat(
483 "trace: writes=%zu unique_cells=%zu bounds_px=(%d,%d,%d,%d) "
484 "delta_vs_selection_px=(%+d,%+d,%+d,%+d)",
485 trace.size(), unique_cells.size(), trace_x_px, trace_y_px, trace_w_px,
486 trace_h_px, trace_x_px - selection_x_px, trace_y_px - selection_y_px,
487 trace_w_px - selection_w_px, trace_h_px - selection_h_px);
491 std::string_view label) {
496 const auto [bounds_x, bounds_y, bounds_w, bounds_h] = door.
GetBounds();
497 const auto [editor_x, editor_y, editor_w, editor_h] = door.
GetEditorBounds();
498 const auto [encoded_b1, encoded_b2] = door.
EncodeBytes();
500 return absl::StrFormat(
502 "- type=%s (0x%02X)\n"
503 "- direction=%s (%d)\n"
505 "- tile_coords=(%d,%d) pixel_coords=(%d,%d)\n"
506 "- dims_tiles=(%d,%d) editor_dims_tiles=(%d,%d)\n"
507 "- bounds_px=(%d,%d,%d,%d) editor_bounds_px=(%d,%d,%d,%d)\n"
508 "- encoded=(0x%02X,0x%02X) original=(0x%02X,0x%02X)",
509 std::string(label).c_str(), index,
511 static_cast<int>(door.
type),
514 tile_y, pixel_x, pixel_y, dims.width_tiles, dims.height_tiles,
515 editor_dims.width_tiles, editor_dims.height_tiles, bounds_x, bounds_y,
516 bounds_w, bounds_h, editor_x, editor_y, editor_w, editor_h, encoded_b1,
525 return std::filesystem::path(rom.
filename()).filename().string();
527 const std::string title = rom.
title();
528 return title.empty() ?
"in-memory ROM" : title;
535 const std::string rom_path =
537 ? std::string(
"in-memory")
539 summary += absl::StrFormat(
544 summary +=
"ROM: unavailable";
548 summary += absl::StrFormat(
560 return absl::StrFormat(
"- [%s] palette=%d colors=%d %s",
565 return absl::StrFormat(
"- [%s] palette=%d %s", event.
location.c_str(),
569 return absl::StrFormat(
"- [%s] colors=%d %s", event.
location.c_str(),
572 return absl::StrFormat(
"- [%s] %s", event.
location.c_str(),
584 std::string entrance_context =
"Entrance:none";
586 const std::string entrance_id_text =
590 const std::string entrance_blockset_text =
591 entrance_blockset != 0xFF
592 ? absl::StrFormat(
"%02X",
static_cast<int>(entrance_blockset))
595 absl::StrFormat(
"Entrance:%s EB:%s", entrance_id_text.c_str(),
596 entrance_blockset_text.c_str());
599 std::string summary = absl::StrFormat(
600 "Room 0x%03X [%s] | B:%02X P:%02X->%02X L:%02X S:%02X | %s | Group:%s | "
601 "Floor1:%d Floor2:%d Effect:%d Tag1:%d Tag2:%d\n%s\n%s",
604 entrance_context, GetBlocksetGroupName(room.
blockset()), room.
floor1(),
607 BuildEffectiveBlockListSummary(room).c_str());
614 const auto link_diagnostics =
616 if (!link_diagnostics.links.empty() ||
617 !link_diagnostics.staircase_issues.empty()) {
618 summary +=
"\nConnectivity:";
619 for (
const auto& link : link_diagnostics.links) {
623 for (
const auto& issue : link_diagnostics.staircase_issues) {
642 std::string report =
"Dungeon Draw Issue Report\n";
644 report += absl::StrFormat(
645 "\nResolved palette group: 0x%02X\nInteraction mode: %s\nView: BG1=%s "
646 "BG2=%s Grid=%s Bounds=%s Sprites=%s Pots=%s Collision=%s Camera=%s",
658 if (!selected.empty()) {
659 report += absl::StrFormat(
"\nSelected objects: %zu", selected.size());
660 if (selected.size() == 1) {
662 const size_t index = selected.front();
663 if (index < objects.size()) {
664 const auto& obj = objects[index];
665 report += absl::StrFormat(
666 "\nObject: id=0x%03X name=%s pos=(%d,%d) size=0x%02X %s", obj.id_,
667 GetObjectName(obj.id_).c_str(), obj.x_, obj.y_, obj.size_,
668 BuildSelectedObjectSemanticsSummary(obj).c_str());
669 if (
auto tiles = obj.GetTiles(); tiles.ok()) {
671 report += FormatObjectTileSample(*tiles, 16,
"Descriptor tiles:");
673 if (obj.id_ == 0x00C4 || obj.id_ == 0x00DB) {
674 const bool uses_floor1 = obj.id_ == 0x00C4;
675 const uint8_t floor_pattern =
677 report += absl::StrFormat(
678 "\nFloor copy source: Floor %d (room.floor%d) pattern=%d",
679 uses_floor1 ? 1 : 2, uses_floor1 ? 1 : 2,
680 static_cast<int>(floor_pattern));
685 if (effective_tiles.has_value()) {
687 report += FormatObjectTileSample(*effective_tiles,
688 effective_tiles->size(),
689 "Effective floor-copy tiles:");
692 "\nEffective floor-copy tiles: unavailable "
693 "(ROM tables out of range)";
697 "\nEffective floor-copy tiles: unavailable (ROM not loaded)";
701 auto [bounds_x, bounds_y, bounds_w, bounds_h] =
703 bounds_w = std::max(bounds_w, 8);
704 bounds_h = std::max(bounds_h, 8);
705 const int origin_x = obj.x_ * 8;
706 const int origin_y = obj.y_ * 8;
707 const int center_x = bounds_x + std::max(0, (bounds_w / 2) - 1);
708 const int center_y = bounds_y + std::max(0, (bounds_h / 2) - 1);
709 report += absl::StrFormat(
710 "\nObject geometry: selection_bounds_px=(%d,%d,%d,%d) "
711 "object_origin_px=(%d,%d) center_px=(%d,%d)",
712 bounds_x, bounds_y, bounds_w, bounds_h, origin_x, origin_y,
714 const auto trace_report =
716 report += trace_report.summary;
719 auto append_sample = [&](std::string_view label,
int sample_x,
721 const auto comp = palette_debugger.SamplePixelAt(sample_x, sample_y);
722 palette_debugger.AddComparison(comp);
723 const std::string label_text(label);
724 report += absl::StrFormat(
725 "\nPalette sample %s (%d,%d): idx=%d expected=(%d,%d,%d) "
726 "actual=(%d,%d,%d) match=%s",
727 label_text.c_str(), sample_x, sample_y,
728 static_cast<int>(comp.palette_index),
729 static_cast<int>(comp.expected_r),
730 static_cast<int>(comp.expected_g),
731 static_cast<int>(comp.expected_b),
732 static_cast<int>(comp.actual_r),
static_cast<int>(comp.actual_g),
733 static_cast<int>(comp.actual_b), comp.matches ?
"yes" :
"no");
736 if (
auto trace_sample = ChooseObjectTracePaletteSample(
737 trace_report.writes, palette_debugger)) {
738 append_sample(trace_sample->label, trace_sample->x, trace_sample->y);
740 append_sample(
"selection-origin", bounds_x, bounds_y);
741 if (center_x != bounds_x || center_y != bounds_y) {
742 append_sample(
"selection-center", center_x, center_y);
746 report += absl::StrFormat(
747 "\nSelected object index %zu is stale; room currently has %zu "
748 "object(s). Reselect the object and report again if this issue "
750 index, objects.size());
757 switch (selected_entity.type) {
759 const auto& doors = room.
GetDoors();
760 if (selected_entity.index < doors.size()) {
761 const auto& door = doors[selected_entity.index];
762 report += BuildDoorIssueSummary(door, selected_entity.index,
769 if (selected_entity.index < sprites.size()) {
770 const auto& sprite = sprites[selected_entity.index];
771 report += absl::StrFormat(
772 "\nSelected entity: sprite id=0x%02X name=%s pos=(%d,%d) "
775 sprite.y(), sprite.layer());
781 if (selected_entity.index < items.size()) {
782 const auto& item = items[selected_entity.index];
784 absl::StrFormat(
"\nSelected entity: pot item=0x%02X pos=(%d,%d)",
785 item.item, item.GetPixelX(), item.GetPixelY());
795 if (!palette_events.empty()) {
797 absl::StrFormat(
"\nPalette debug events: %zu", palette_events.size());
798 const size_t preview_count = std::min<size_t>(palette_events.size(), 4);
799 const size_t start = palette_events.size() - preview_count;
800 for (
size_t i = start; i < palette_events.size(); ++i) {
801 const auto&
event = palette_events[i];
803 report += FormatPaletteDebugEventForReport(event);
812 std::string report =
"Dungeon Selection Issue Report\n";
815 absl::StrFormat(
"\nInteraction mode: %s\nResolved palette group: 0x%02X",
820 if (!selected_objects.empty()) {
822 absl::StrFormat(
"\nSelected objects: %zu", selected_objects.size());
824 const size_t preview_count = std::min<size_t>(selected_objects.size(), 4);
825 for (
size_t i = 0; i < preview_count; ++i) {
826 const size_t index = selected_objects[i];
827 if (index >= objects.size()) {
828 report += absl::StrFormat(
829 "\n- object[%zu] unavailable; room currently has %zu object(s) "
830 "(selection is stale)",
831 index, objects.size());
834 const auto& obj = objects[index];
835 report += absl::StrFormat(
836 "\n- object[%zu] id=0x%03X name=%s pos=(%d,%d) size=0x%02X %s", index,
837 obj.id_, GetObjectName(obj.id_).c_str(), obj.x_, obj.y_, obj.size_,
838 BuildSelectedObjectSemanticsSummary(obj).c_str());
839 auto [bounds_x, bounds_y, bounds_w, bounds_h] =
841 bounds_w = std::max(bounds_w, 8);
842 bounds_h = std::max(bounds_h, 8);
843 report += absl::StrFormat(
844 "\n geometry: selection_bounds_px=(%d,%d,%d,%d) "
845 "object_origin_px=(%d,%d)",
846 bounds_x, bounds_y, bounds_w, bounds_h, obj.x_ * 8, obj.y_ * 8);
847 if (
auto tiles = obj.GetTiles(); tiles.ok() && !tiles->empty()) {
849 report += FormatObjectTileSample(*tiles, 10,
"tiles:");
851 const auto trace_report =
854 report += BuildObjectTraceBoundsSummary(trace_report.writes, bounds_x,
855 bounds_y, bounds_w, bounds_h);
857 if (selected_objects.size() > preview_count) {
858 report += absl::StrFormat(
"\n- ... %zu more object(s)",
859 selected_objects.size() - preview_count);
866 switch (selected_entity.type) {
868 const auto& doors = room.
GetDoors();
869 if (selected_entity.index < doors.size()) {
870 const auto& door = doors[selected_entity.index];
871 report += BuildDoorIssueSummary(door, selected_entity.index,
879 if (selected_entity.index < sprites.size()) {
880 const auto& sprite = sprites[selected_entity.index];
881 report += absl::StrFormat(
882 "\nSelected sprite[%zu]:\n"
887 selected_entity.index, sprite.id(),
896 if (selected_entity.index < items.size()) {
897 const auto& item = items[selected_entity.index];
898 report += absl::StrFormat(
899 "\nSelected pot item[%zu]:\n"
901 "- pixel_pos=(%d,%d)",
902 selected_entity.index, item.item, item.GetPixelX(),
913 report +=
"\nNo active object or entity selection.";
922 if (!paths_or.ok()) {
951 if (!ImGui::CollapsingHeader(kIssueReportSectionPaths)) {
955 ImGui::TextWrapped(tr(
"Log target: %s"),
960 ImGui::TextWrapped(tr(
"Screenshot dir: %s"),
964 ImGui::TextWrapped(tr(
"Screenshot: %s"),
968 ImGui::TextWrapped(tr(
"Issue log: %s"),
975 const std::string_view message =
976 idle ? std::string_view(tr(kIssueReportStatusIdle))
978 const size_t line_end = message.find_first_of(
"\r\n");
979 std::string visible_line(message.substr(0, line_end));
980 if (line_end != std::string_view::npos) {
981 visible_line +=
"...";
984 idle ? ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)
987 const ImVec2 start = ImGui::GetCursorScreenPos();
988 const ImVec2 size(std::max(ImGui::GetContentRegionAvail().x, 1.0f),
989 ImGui::GetTextLineHeight());
990 const ImVec2 end(start.x + size.x, start.y + size.y);
991 ImGui::PushStyleColor(ImGuiCol_Text, color);
992 ImGui::RenderTextEllipsis(ImGui::GetWindowDrawList(), start, end, end.x,
993 visible_line.c_str(),
nullptr,
nullptr);
994 ImGui::PopStyleColor();
996 if (ImGui::IsItemHovered()) {
997 ImGui::BeginTooltip();
998 ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
999 ImGui::TextUnformatted(message.data(), message.data() + message.size());
1000 ImGui::PopTextWrapPos();
1001 ImGui::EndTooltip();
1006 const std::string& title,
const std::string& summary,
1007 const std::string& kind_label,
const std::string& diagnostics,
int room_id,
1008 int default_category_index) {
1014 std::clamp(default_category_index, 0,
1024 return absl::OkStatus();
1028 const std::string& summary,
1029 const std::string& kind_label,
1030 const std::string& diagnostics,
1032 int default_category_index) {
1034 room_id, default_category_index);
1036 const IssueReportWindowLayout window_layout = GetIssueReportWindowLayout();
1037 ImGui::SetNextWindowPos(window_layout.center, ImGuiCond_Appearing,
1038 ImVec2(0.5f, 0.5f));
1039 ImGui::SetNextWindowSize(window_layout.initial_size, ImGuiCond_Appearing);
1040 ImGui::SetNextWindowSizeConstraints(window_layout.min_size,
1041 window_layout.max_size);
1044 ImGuiWindowFlags_NoSavedSettings)) {
1045#ifdef YAZE_WITH_GRPC
1047 ? kIssueReportCaptureButtonLabel
1048 : kIssueReportRecaptureButtonLabel;
1049 const std::array<const char*, 4> action_labels = {
1050 kIssueReportSaveButtonLabel, capture_label,
1051 kIssueReportCopyButtonLabel, kIssueReportCloseButtonLabel};
1053 const std::array<const char*, 3> action_labels = {
1054 kIssueReportSaveButtonLabel, kIssueReportCopyButtonLabel,
1055 kIssueReportCloseButtonLabel};
1057 const float footer_width =
1058 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
1059 const int action_rows = CountWrappedIssueActionRows(
1060 std::span<const char* const>(action_labels), footer_width);
1061 const ImGuiStyle& style = ImGui::GetStyle();
1062 const float action_height =
1063 action_rows * ImGui::GetFrameHeight() +
1064 std::max(0, action_rows - 1) * style.ItemSpacing.y;
1065 const float footer_height = ImGui::GetTextLineHeightWithSpacing() +
1066 action_height + style.ItemSpacing.y * 2.0f +
1069 if (ImGui::BeginChild(
"##DungeonIssueReportBody",
1070 ImVec2(0.0f, -footer_height),
false,
1071 ImGuiWindowFlags_None)) {
1080 ImGui::TextUnformatted(tr(
"Category"));
1081 ImGui::SetNextItemWidth(
1082 std::max(ImGui::GetContentRegionAvail().x, 1.0f));
1083 const char* category_preview =
1085 if (ImGui::BeginCombo(
"##DungeonIssueCategory", category_preview)) {
1087 const bool selected =
1094 ImGui::SetItemDefaultFocus();
1100 ImGui::TextUnformatted(tr(
"Summary"));
1101 ImGui::SetNextItemWidth(
1102 std::max(ImGui::GetContentRegionAvail().x, 1.0f));
1103 if (ImGui::InputTextWithHint(
1104 "##DungeonIssueSummary", kIssueReportSummaryHint,
1109 ImGui::TextUnformatted(tr(
"Observed issue"));
1110 if (ImGui::InputTextMultiline(
"##DungeonIssueObserved",
1113 ImVec2(-1.0f, kIssueReportNotesHeight))) {
1119 if (ImGui::CollapsingHeader(kIssueReportSectionDiagnostics)) {
1120 ImGui::InputTextMultiline(
1121 kIssueReportPopupIdDiagnostics,
1124 ImVec2(-1.0f, kIssueReportDiagnosticsHeight),
1125 ImGuiInputTextFlags_ReadOnly);
1138 float used_action_width = 0.0f;
1139 PlaceNextIssueAction(kIssueReportSaveButtonLabel, footer_width,
1140 &used_action_width);
1150#ifdef YAZE_WITH_GRPC
1151 PlaceNextIssueAction(capture_label, footer_width, &used_action_width);
1154 ImVec2(GetIssueActionButtonWidth(capture_label), 0.0f))) {
1165 PlaceNextIssueAction(kIssueReportCopyButtonLabel, footer_width,
1166 &used_action_width);
1167 if (ImGui::Button(kIssueReportCopyButtonLabel)) {
1169 ImGui::SetClipboardText(report.c_str());
1173 PlaceNextIssueAction(kIssueReportCloseButtonLabel, footer_width,
1174 &used_action_width);
1175 if (ImGui::Button(kIssueReportCloseButtonLabel)) {
1177 ImGui::CloseCurrentPopup();
1190 report += absl::StrFormat(
1196 report += absl::StrFormat(
"Screenshot: %s\n",
1202 report +=
"\nDiagnostics:\n";
1208#ifdef YAZE_WITH_GRPC
1211 return absl::FailedPreconditionError(
1212 "Room canvas region is not available for screenshot capture");
1215 const std::string timestamp = BuildIssueTimestampSlug();
1219 if (!screenshot_path_or.ok()) {
1220 return screenshot_path_or.status();
1223 yaze::test::CaptureRegion region;
1229 auto artifact_or = yaze::test::CaptureHarnessScreenshotRegion(
1230 std::optional<yaze::test::CaptureRegion>(region),
1231 screenshot_path_or->string(),
1233 if (!artifact_or.ok()) {
1234 return artifact_or.status();
1239 return absl::OkStatus();
1241 return absl::UnimplementedError(
1242 "Screenshot capture is unavailable without YAZE_WITH_GRPC");
1259 if (!log_path_or.ok()) {
1260 return log_path_or.status();
1265 return absl::OkStatus();
1271 return absl::OkStatus();
1276 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
bool issue_report_popup_status_is_error_
std::string issue_report_popup_screenshot_dir_
int issue_report_category_index_
std::string BuildDrawIssueReport(const zelda3::Room &room, int room_id)
bool has_canvas_capture_region_
void MarkIssueReportDirty()
bool show_camera_quadrant_overlay_
std::string issue_report_popup_status_message_
bool IsBG1Visible(int room_id) const
std::string issue_report_popup_id_
std::string issue_report_popup_kind_
void RefreshIssueReportOutputTargets()
std::string BuildIssueReportClipboardText() const
std::string issue_report_popup_screenshot_path_
int issue_report_popup_room_id_
void SetIssueReportPopupStatus(std::string message, bool is_error)
char issue_report_summary_[256]
bool show_custom_collision_overlay_
DungeonObjectInteraction object_interaction_
void OpenIssueReportPopup(const std::string &title, const std::string &summary, const std::string &kind_label, const std::string &diagnostics, int room_id, int default_category_index)
absl::Status AppendIssueReportToLog()
std::string BuildRoomMetadataSummary(const zelda3::Room &room, int room_id) const
void DrawIssueReportStatusMessage() const
const project::YazeProject * project_
void DrawIssueReportStorageSummary() const
absl::Status EnsureIssueReportPersisted()
gfx::Bitmap * PrepareRoomCompositeBitmap(int room_id)
int canvas_capture_height_
std::string issue_report_popup_diagnostics_
gfx::PaletteGroup current_palette_group_
DungeonRoomStore * rooms_
absl::Status PrepareIssueReportPopup(const std::string &title, const std::string &summary, const std::string &kind_label, const std::string &diagnostics, int room_id, int default_category_index)
bool issue_report_popup_persisted_
EntityVisibility entity_visibility_
bool IsBG2Visible(int room_id) const
absl::Status CaptureIssueReportScreenshot()
char issue_report_notes_[2048]
std::string issue_report_popup_last_log_path_
std::string issue_report_popup_log_target_path_
int canvas_capture_width_
std::string issue_report_popup_title_
std::string BuildSelectionIssueReport(const zelda3::Room &room, int room_id) const
SelectedEntity GetSelectedEntity() const
std::vector< size_t > GetSelectedObjectIndices() const
InteractionModeManager & mode_manager()
bool HasEntitySelection() const
zelda3::Room * GetIfMaterialized(int room_id)
const char * GetModeName() const
Get mode name for debugging/UI.
std::vector< uint16_t > & buffer()
Represents a bitmap image optimized for SNES ROM hacking.
void ClosePersistentPopup(const std::string &popup_id)
void OpenPersistentPopup(const std::string &popup_id, std::function< void()> render_callback)
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
Draws dungeon objects to background buffers using game patterns.
void SetRoomFloorGraphics(uint8_t floor1, uint8_t floor2)
void SetBG1RevealMaskSource(gfx::BG1RevealMaskSource source)
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw a room object to background buffers.
void ClearTraceCollector()
void SetTraceCollector(std::vector< TileTrace > *collector, bool trace_only=false)
static PaletteDebugger & Get()
const std::vector< PaletteDebugEvent > & GetEvents() const
ColorComparison SamplePixelAt(int x, int y) const
uint8_t GetLayerValue() const
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
const std::vector< Door > & GetDoors() const
uint8_t spriteset() const
const std::vector< zelda3::Sprite > & GetSprites() const
const std::vector< RoomObject > & GetTileObjects() const
DungeonState * GetDungeonState()
int ResolveDungeonPaletteId() const
uint8_t render_entrance_blockset() const
uint8_t layout_id() const
const std::vector< PotItem > & GetPotItems() const
#define ICON_MD_ADD_A_PHOTO
#define ICON_MD_ASSIGNMENT
#define ICON_MD_CONTENT_COPY
#define ICON_MD_PHOTO_CAMERA
std::string BuildEffectiveBlockListSummary(const zelda3::Room &room)
constexpr char kIssueReportSummaryHint[]
constexpr char kIssueReportSaveButtonLabel[]
std::string BuildDoorIssueSummary(const zelda3::Room::Door &door, size_t index, std::string_view label)
bool PaletteIndexMatchesTracePalette(uint8_t palette_index, uint8_t trace_flags)
constexpr float kIssueReportDialogMinHeight
IssueReportWindowLayout GetIssueReportWindowLayout()
std::string BuildIssueTimestampSlug()
std::string BuildObjectTraceBoundsSummary(std::span< const zelda3::ObjectDrawer::TileTrace > trace, int selection_x_px, int selection_y_px, int selection_w_px, int selection_h_px)
constexpr char kIssueReportCaptureButtonLabel[]
std::optional< PaletteSamplePoint > ChooseObjectTracePaletteSample(std::span< const zelda3::ObjectDrawer::TileTrace > trace, const zelda3::PaletteDebugger &palette_debugger)
constexpr float kIssueReportDialogWidth
constexpr char kIssueReportStatusCopiedDiagnostics[]
ObjectTraceReport BuildObjectTraceReport(Rom *rom, const zelda3::Room &room, size_t selected_index, const gfx::PaletteGroup &palette_group)
constexpr char kIssueReportPopupIdDiagnostics[]
std::string BuildRomDisplayName(const Rom &rom)
std::string BuildSelectedObjectSemanticsSummary(const zelda3::RoomObject &obj)
std::string BuildIssueTimestampDisplay()
constexpr char kIssueReportStatusSavedLog[]
void PlaceNextIssueAction(const char *label, float available_width, float *used_width)
constexpr float kIssueReportDialogMinWidth
constexpr char kIssueReportStatusIdle[]
std::string FormatPaletteDebugEventForReport(const zelda3::PaletteDebugEvent &event)
constexpr char kIssueReportSectionPaths[]
constexpr char kIssueReportRecaptureButtonLabel[]
constexpr char kIssueReportStatusCopiedReport[]
std::string BuildReportSessionSummary(const Rom *rom, const project::YazeProject *project)
constexpr char kIssueReportStatusSavedScreenshot[]
constexpr char kIssueReportCloseButtonLabel[]
constexpr float kIssueReportDiagnosticsHeight
constexpr char kIssueReportCopyButtonLabel[]
std::string FormatObjectTileSample(std::span< const gfx::TileInfo > tiles, size_t max_count, std::string_view prefix)
constexpr float kIssueReportDialogHeight
constexpr char kIssueReportSectionDiagnostics[]
const char * TraceLayerLabel(uint8_t layer)
float GetIssueActionButtonWidth(const char *label)
const char * GetBlocksetGroupName(uint8_t blockset)
const char * GetStoredPlacementLabel(const zelda3::RoomObject &object)
int CountWrappedIssueActionRows(std::span< const char *const > labels, float available_width)
constexpr float kIssueReportNotesHeight
constexpr float kIssueReportDialogViewportMargin
Editors are the view controllers for the application.
std::string FormatDungeonConnectedLinkDescription(const DungeonConnectedRoomLink &link)
DungeonConnectedRoomLinkDiagnostics CollectDungeonConnectedRoomLinkDiagnostics(int room_id, const zelda3::Room &room, const std::function< bool(int, zelda3::DoorDirection)> &has_reciprocal_door)
const char * GetIssueCategoryLabel(int index)
constexpr std::array< const char *, 6 > kDungeonIssueCategoryLabels
absl::StatusOr< std::filesystem::path > AppendDungeonIssueLogEntry(const DungeonIssueLogEntry &entry)
absl::StatusOr< std::filesystem::path > BuildDungeonIssueScreenshotPath(int room_id, const std::string &category_label, const std::string ×tamp_slug)
absl::StatusOr< DungeonIssueReportPaths > ResolveDungeonIssueReportPaths()
std::string FormatDungeonStaircaseIssueDescription(const DungeonStaircaseIssue &issue)
std::optional< std::array< TileInfo, 8 > > DecodeDungeonFloorTilePattern(const std::vector< uint8_t > &rom_data, int tile_address, int tile_address_floor, uint8_t floor_graphics)
bool PrimaryButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a primary action button (accented color).
std::string ComputeRomHash(const uint8_t *data, size_t size)
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
std::string GetRoomLabel(int id)
Convenience function to get a room label.
RoomObject::LayerType MapRoomObjectListIndexToDrawLayer(uint8_t list_index)
const char * ObjectRenderRoutingToken(ObjectRenderRouting routing)
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
bool UsesRoomObjectStream(const RoomObject &object)
constexpr int kRoomObjectTileAddress
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
constexpr int kNumberOfRooms
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
constexpr int kRoomObjectTileAddressFloor
const char * ResolveSpriteName(uint16_t id)
#define RETURN_IF_ERROR(expr)
std::string observed_issue
std::string screenshot_path
std::string timestamp_display
std::string category_label
std::vector< zelda3::ObjectDrawer::TileTrace > writes
std::array< gfx::Bitmap *, 2 > bitmaps
~ScopedBitmapRetirement()
Represents a group of palettes.
Modern project structure with comprehensive settings consolidation.
bool project_opened() const
std::string GetDisplayName() const
Represents a door in a dungeon room.
std::tuple< int, int, int, int > GetEditorBounds() const
Get editor interaction bounds (x, y, width, height in pixels)
std::tuple< int, int, int, int > GetBounds() const
Get bounding rectangle (x, y, width, height in pixels)
uint8_t byte1
Original ROM byte 1 (position data)
DoorDimensions GetDimensions() const
Get door dimensions in tiles.
DoorType type
Door type (determines appearance/behavior)
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
DoorDirection direction
Which wall the door is on.
DoorDimensions GetEditorDimensions() const
Get editor interaction dimensions in tiles.
uint8_t position
Encoded position (5-bit, 0-31)
std::pair< int, int > GetPixelCoords() const
Get pixel coordinates for this door.
uint8_t byte2
Original ROM byte 2 (type + direction)
std::pair< int, int > GetTileCoords() const
Get tile coordinates for this door.