yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_issue_report.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <cstdio>
7#include <filesystem>
8#include <limits>
9#include <optional>
10#include <set>
11#include <span>
12#include <string>
13#include <string_view>
14#include <tuple>
15#include <vector>
16
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"
23#include "app/gui/core/icons.h"
25#ifdef YAZE_WITH_GRPC
27#endif
28#include "imgui/imgui.h"
29#include "util/macro.h"
30#include "util/platform_paths.h"
31#include "util/rom_hash.h"
39
40namespace yaze::editor {
41
42namespace {
43
44constexpr float kIssueReportDialogWidth = 620.0f;
45constexpr float kIssueReportNotesHeight = 120.0f;
46constexpr float kIssueReportDiagnosticsHeight = 180.0f;
47constexpr char kIssueReportSummaryHint[] = "What looks wrong?";
48constexpr char kIssueReportPopupIdDiagnostics[] = "##DungeonIssueDiagnostics";
49constexpr char kIssueReportSectionPaths[] = "Paths";
50constexpr char kIssueReportSectionDiagnostics[] = "Diagnostics";
52 "Captured room screenshot to %s";
53constexpr char kIssueReportStatusSavedLog[] = "Saved issue report to %s";
54constexpr char kIssueReportStatusStartedLog[] = "Started issue report in %s";
56 "Copied report to clipboard and saved issue report to %s";
58 "Copied diagnostics and saved issue report to %s";
60 ICON_MD_ADD_A_PHOTO " Capture Screenshot";
62 ICON_MD_PHOTO_CAMERA " Re-capture Screenshot";
63
64const char* GetStoredPlacementLabel(const zelda3::RoomObject& object) {
65 const int layer_value = object.GetLayerValue();
66 if (!zelda3::UsesRoomObjectStream(object)) {
67 if (layer_value == 0) {
68 return "Upper layer (BG1)";
69 }
70 return layer_value == 1 ? "Lower layer (BG2)" : "Invalid selector";
71 }
72 switch (layer_value) {
73 case 0:
74 return "Primary";
75 case 1:
76 return "BG2 overlay";
77 case 2:
78 return "BG1 overlay";
79 default:
80 return "Unknown";
81 }
82}
83
84const char* GetBlocksetGroupName(uint8_t blockset) {
85 static const char* kGroupNames[] = {
86 "HC/Sewers", "Eastern", "Desert", "Hera", "A-Tower", "PoD", "Swamp",
87 "Skull", "Thieves", "Ice", "Misery", "Turtle", "GT",
88 };
89 constexpr size_t kCount = sizeof(kGroupNames) / sizeof(kGroupNames[0]);
90 return blockset < kCount ? kGroupNames[blockset] : "Custom";
91}
92
94 return absl::FormatTime("%Y%m%d-%H%M%S", absl::Now(), absl::LocalTimeZone());
95}
96
98 return absl::FormatTime("%Y-%m-%d %H:%M:%S %Z", absl::Now(),
99 absl::LocalTimeZone());
100}
101
103 const auto blocks = room.blocks();
104 if (blocks.size() < 8) {
105 return "BG sheets: unavailable";
106 }
107 return absl::StrFormat(
108 "BG sheets: [%02X %02X %02X %02X %02X %02X %02X %02X]",
109 static_cast<int>(blocks[0]), static_cast<int>(blocks[1]),
110 static_cast<int>(blocks[2]), static_cast<int>(blocks[3]),
111 static_cast<int>(blocks[4]), static_cast<int>(blocks[5]),
112 static_cast<int>(blocks[6]), static_cast<int>(blocks[7]));
113}
114
116 const auto semantics = zelda3::GetObjectLayerSemantics(obj);
118 return absl::StrFormat(
119 "storage=special-table selector=%d (%s) routine=%d "
120 "legacy_both_bgs=%s "
121 "routing=%s",
122 obj.GetLayerValue(), GetStoredPlacementLabel(obj), semantics.routine_id,
123 semantics.draws_to_both_bgs ? "yes" : "no",
124 zelda3::ObjectRenderRoutingToken(semantics.render_routing));
125 }
126 return absl::StrFormat(
127 "stream=%s draw_layer=%d effective_bg=%s routine=%d "
128 "legacy_both_bgs=%s "
129 "routing=%s",
131 static_cast<int>(
133 zelda3::EffectiveBgLayerLabel(semantics.effective_bg_layer),
134 semantics.routine_id, semantics.draws_to_both_bgs ? "yes" : "no",
135 zelda3::ObjectRenderRoutingToken(semantics.render_routing));
136}
137
138const char* TraceLayerLabel(uint8_t layer) {
139 switch (static_cast<zelda3::RoomObject::LayerType>(layer)) {
141 return "BG1";
143 return "BG2";
145 return "BG3";
146 }
147 return "BG?";
148}
149
150std::string FormatObjectTileSample(std::span<const gfx::TileInfo> tiles,
151 size_t max_count, std::string_view prefix) {
152 std::string out = absl::StrFormat("%s count=%zu", std::string(prefix).c_str(),
153 tiles.size());
154 const size_t preview_count = std::min<size_t>(tiles.size(), max_count);
155 for (size_t i = 0; i < preview_count; ++i) {
156 const auto& tile = tiles[i];
157 out += absl::StrFormat(" [%zu]=0x%03X/p%d/pr%d/h%d/v%d", i, tile.id_,
158 static_cast<int>(tile.palette_), tile.over_ ? 1 : 0,
159 tile.horizontal_mirror_ ? 1 : 0,
160 tile.vertical_mirror_ ? 1 : 0);
161 }
162 if (tiles.size() > preview_count) {
163 out += absl::StrFormat(" ... %zu more", tiles.size() - preview_count);
164 }
165 return out;
166}
167
169 std::string summary;
170 std::vector<zelda3::ObjectDrawer::TileTrace> writes;
171};
172
174 Rom* rom, int room_id, const zelda3::RoomObject& obj,
175 const gfx::PaletteGroup& palette_group) {
176 if (rom == nullptr || !rom->is_loaded()) {
177 return {.summary = "\nDrawer trace: unavailable (ROM not loaded)"};
178 }
179
180 zelda3::ObjectDrawer drawer(rom, room_id, /*room_gfx_buffer=*/nullptr);
181 gfx::BackgroundBuffer bg1(512, 512);
182 gfx::BackgroundBuffer bg2(512, 512);
183 std::vector<zelda3::ObjectDrawer::TileTrace> trace;
184 drawer.SetTraceCollector(&trace, /*trace_only=*/true);
185 const auto status = drawer.DrawObject(obj, bg1, bg2, palette_group);
186 drawer.ClearTraceCollector();
187 if (!status.ok()) {
188 return {.summary = absl::StrFormat("\nDrawer trace: unavailable (%s)",
189 std::string(status.message()).c_str())};
190 }
191
192 if (trace.empty()) {
193 return {.summary = "\nDrawer trace: status=ok writes=0"};
194 }
195
196 int min_x = std::numeric_limits<int>::max();
197 int min_y = std::numeric_limits<int>::max();
198 int max_x = std::numeric_limits<int>::min();
199 int max_y = std::numeric_limits<int>::min();
200 std::array<int, 3> layer_counts = {0, 0, 0};
201 std::set<std::tuple<int, int, uint8_t>> unique_cells;
202 for (const auto& write : trace) {
203 min_x = std::min<int>(min_x, write.x_tile);
204 min_y = std::min<int>(min_y, write.y_tile);
205 max_x = std::max<int>(max_x, write.x_tile);
206 max_y = std::max<int>(max_y, write.y_tile);
207 if (write.layer < layer_counts.size()) {
208 ++layer_counts[write.layer];
209 }
210 unique_cells.insert({write.x_tile, write.y_tile, write.layer});
211 }
212
213 std::string out = absl::StrFormat(
214 "\nDrawer trace: status=ok writes=%zu unique_cells=%zu "
215 "bounds_tiles=(%d,%d)..(%d,%d) layer_counts BG1=%d BG2=%d BG3=%d",
216 trace.size(), unique_cells.size(), min_x, min_y, max_x, max_y,
220
221 const size_t preview_count = std::min<size_t>(trace.size(), 12);
222 for (size_t i = 0; i < preview_count; ++i) {
223 const auto& write = trace[i];
224 const int palette = (write.flags >> 3) & 0x7;
225 out += absl::StrFormat(
226 "\n write[%zu] %s tile=(%d,%d) id=0x%03X pal=%d pri=%d h=%d v=%d", i,
227 TraceLayerLabel(write.layer), write.x_tile, write.y_tile, write.tile_id,
228 palette, (write.flags & 0x4) ? 1 : 0, (write.flags & 0x1) ? 1 : 0,
229 (write.flags & 0x2) ? 1 : 0);
230 }
231 if (trace.size() > preview_count) {
232 out += absl::StrFormat("\n ... %zu more write(s)",
233 trace.size() - preview_count);
234 }
235 return {.summary = std::move(out), .writes = std::move(trace)};
236}
237
239 std::string label;
240 int x = 0;
241 int y = 0;
242};
243
244bool PaletteIndexMatchesTracePalette(uint8_t palette_index,
245 uint8_t trace_flags) {
246 if ((palette_index & 0x0F) == 0) {
247 return false;
248 }
249 const uint8_t trace_palette = static_cast<uint8_t>((trace_flags >> 3) & 0x7);
250 return static_cast<uint8_t>((palette_index >> 4) & 0x7) == trace_palette;
251}
252
253std::optional<PaletteSamplePoint> ChooseObjectTracePaletteSample(
254 std::span<const zelda3::ObjectDrawer::TileTrace> trace,
255 const zelda3::PaletteDebugger& palette_debugger) {
256 constexpr std::array<std::pair<int, int>, 6> kPreferredOffsets = {
257 std::pair{4, 4}, std::pair{3, 3}, std::pair{0, 0},
258 std::pair{7, 7}, std::pair{0, 7}, std::pair{7, 0},
259 };
260
261 for (size_t i = 0; i < trace.size(); ++i) {
262 const auto& write = trace[i];
263 const int tile_origin_x = write.x_tile * 8;
264 const int tile_origin_y = write.y_tile * 8;
265 const auto label = absl::StrFormat(
266 "object-trace[%zu] tile=(%d,%d) pal=%d", i, write.x_tile, write.y_tile,
267 static_cast<int>((write.flags >> 3) & 0x7));
268
269 for (const auto& [dx, dy] : kPreferredOffsets) {
270 const int sample_x = tile_origin_x + dx;
271 const int sample_y = tile_origin_y + dy;
272 const auto comp = palette_debugger.SamplePixelAt(sample_x, sample_y);
273 if (PaletteIndexMatchesTracePalette(comp.palette_index, write.flags)) {
274 return PaletteSamplePoint{.label = label, .x = sample_x, .y = sample_y};
275 }
276 }
277
278 for (int dy = 0; dy < 8; ++dy) {
279 for (int dx = 0; dx < 8; ++dx) {
280 const int sample_x = tile_origin_x + dx;
281 const int sample_y = tile_origin_y + dy;
282 const auto comp = palette_debugger.SamplePixelAt(sample_x, sample_y);
283 if (PaletteIndexMatchesTracePalette(comp.palette_index, write.flags)) {
284 return PaletteSamplePoint{
285 .label = label, .x = sample_x, .y = sample_y};
286 }
287 }
288 }
289 }
290
291 if (!trace.empty()) {
292 const auto& write = trace.front();
293 return PaletteSamplePoint{
294 .label = absl::StrFormat("object-trace[0] tile=(%d,%d) pal=%d fallback",
295 write.x_tile, write.y_tile,
296 static_cast<int>((write.flags >> 3) & 0x7)),
297 .x = write.x_tile * 8 + 4,
298 .y = write.y_tile * 8 + 4};
299 }
300
301 return std::nullopt;
302}
303
305 std::span<const zelda3::ObjectDrawer::TileTrace> trace, int selection_x_px,
306 int selection_y_px, int selection_w_px, int selection_h_px) {
307 if (trace.empty()) {
308 return "trace: writes=0";
309 }
310
311 int min_x = std::numeric_limits<int>::max();
312 int min_y = std::numeric_limits<int>::max();
313 int max_x = std::numeric_limits<int>::min();
314 int max_y = std::numeric_limits<int>::min();
315 std::set<std::tuple<int, int, uint8_t>> unique_cells;
316 for (const auto& write : trace) {
317 min_x = std::min<int>(min_x, write.x_tile);
318 min_y = std::min<int>(min_y, write.y_tile);
319 max_x = std::max<int>(max_x, write.x_tile);
320 max_y = std::max<int>(max_y, write.y_tile);
321 unique_cells.insert({write.x_tile, write.y_tile, write.layer});
322 }
323
324 const int trace_x_px = min_x * 8;
325 const int trace_y_px = min_y * 8;
326 const int trace_w_px = (max_x - min_x + 1) * 8;
327 const int trace_h_px = (max_y - min_y + 1) * 8;
328 return absl::StrFormat(
329 "trace: writes=%zu unique_cells=%zu bounds_px=(%d,%d,%d,%d) "
330 "delta_vs_selection_px=(%+d,%+d,%+d,%+d)",
331 trace.size(), unique_cells.size(), trace_x_px, trace_y_px, trace_w_px,
332 trace_h_px, trace_x_px - selection_x_px, trace_y_px - selection_y_px,
333 trace_w_px - selection_w_px, trace_h_px - selection_h_px);
334}
335
336std::string BuildDoorIssueSummary(const zelda3::Room::Door& door, size_t index,
337 std::string_view label) {
338 const auto [tile_x, tile_y] = door.GetTileCoords();
339 const auto [pixel_x, pixel_y] = door.GetPixelCoords();
340 const auto dims = door.GetDimensions();
341 const auto editor_dims = door.GetEditorDimensions();
342 const auto [bounds_x, bounds_y, bounds_w, bounds_h] = door.GetBounds();
343 const auto [editor_x, editor_y, editor_w, editor_h] = door.GetEditorBounds();
344 const auto [encoded_b1, encoded_b2] = door.EncodeBytes();
345
346 return absl::StrFormat(
347 "\n%s[%zu]:\n"
348 "- type=%s (0x%02X)\n"
349 "- direction=%s (%d)\n"
350 "- position=%d\n"
351 "- tile_coords=(%d,%d) pixel_coords=(%d,%d)\n"
352 "- dims_tiles=(%d,%d) editor_dims_tiles=(%d,%d)\n"
353 "- bounds_px=(%d,%d,%d,%d) editor_bounds_px=(%d,%d,%d,%d)\n"
354 "- encoded=(0x%02X,0x%02X) original=(0x%02X,0x%02X)",
355 std::string(label).c_str(), index,
356 std::string(zelda3::GetDoorTypeName(door.type)).c_str(),
357 static_cast<int>(door.type),
358 std::string(zelda3::GetDoorDirectionName(door.direction)).c_str(),
359 static_cast<int>(door.direction), static_cast<int>(door.position), tile_x,
360 tile_y, pixel_x, pixel_y, dims.width_tiles, dims.height_tiles,
361 editor_dims.width_tiles, editor_dims.height_tiles, bounds_x, bounds_y,
362 bounds_w, bounds_h, editor_x, editor_y, editor_w, editor_h, encoded_b1,
363 encoded_b2, door.byte1, door.byte2);
364}
365
366std::string BuildRomDisplayName(const Rom& rom) {
367 if (!rom.short_name().empty()) {
368 return rom.short_name();
369 }
370 if (!rom.filename().empty()) {
371 return std::filesystem::path(rom.filename()).filename().string();
372 }
373 const std::string title = rom.title();
374 return title.empty() ? "in-memory ROM" : title;
375}
376
377std::string BuildReportSessionSummary(const Rom* rom,
378 const project::YazeProject* project) {
379 std::string summary;
380 if (rom && rom->is_loaded()) {
381 const std::string rom_path =
382 rom->filename().empty()
383 ? std::string("in-memory")
385 summary += absl::StrFormat(
386 "ROM: %s | CRC32:%s | Path:%s", BuildRomDisplayName(*rom).c_str(),
387 util::ComputeRomHash(rom->data(), rom->size()).c_str(),
388 rom_path.c_str());
389 } else {
390 summary += "ROM: unavailable";
391 }
392
393 if (project && project->project_opened()) {
394 summary += absl::StrFormat(
395 "\nProject: %s | Path:%s", project->GetDisplayName().c_str(),
397 .c_str());
398 }
399
400 return summary;
401}
402
404 const zelda3::PaletteDebugEvent& event) {
405 if (event.palette_id >= 0 && event.color_count > 0) {
406 return absl::StrFormat("- [%s] palette=%d colors=%d %s",
407 event.location.c_str(), event.palette_id,
408 event.color_count, event.message.c_str());
409 }
410 if (event.palette_id >= 0) {
411 return absl::StrFormat("- [%s] palette=%d %s", event.location.c_str(),
412 event.palette_id, event.message.c_str());
413 }
414 if (event.color_count > 0) {
415 return absl::StrFormat("- [%s] colors=%d %s", event.location.c_str(),
416 event.color_count, event.message.c_str());
417 }
418 return absl::StrFormat("- [%s] %s", event.location.c_str(),
419 event.message.c_str());
420}
421
423
424} // namespace
425
427 const zelda3::Room& room, int room_id) const {
428 const int resolved_palette_id = room.ResolveDungeonPaletteId();
429 const uint8_t entrance_blockset = room.render_entrance_blockset();
430 std::string entrance_context = "Entrance:none";
431 if (current_entrance_id_ >= 0 || entrance_blockset != 0xFF) {
432 const std::string entrance_id_text =
434 ? absl::StrFormat("%02X", current_entrance_id_)
435 : std::string("--");
436 const std::string entrance_blockset_text =
437 entrance_blockset != 0xFF
438 ? absl::StrFormat("%02X", static_cast<int>(entrance_blockset))
439 : std::string("--");
440 entrance_context =
441 absl::StrFormat("Entrance:%s EB:%s", entrance_id_text.c_str(),
442 entrance_blockset_text.c_str());
443 }
444
445 std::string summary = absl::StrFormat(
446 "Room 0x%03X [%s] | B:%02X P:%02X->%02X L:%02X S:%02X | %s | Group:%s | "
447 "Floor:%d Effect:%d Tag1:%d Tag2:%d\n%s\n%s",
448 room_id, zelda3::GetRoomLabel(room_id).c_str(), room.blockset(),
449 room.palette(), resolved_palette_id, room.layout_id(), room.spriteset(),
450 entrance_context, GetBlocksetGroupName(room.blockset()), room.floor1(),
451 room.effect(), room.tag1(), room.tag2(),
452 BuildReportSessionSummary(rom_, project_).c_str(),
453 BuildEffectiveBlockListSummary(room).c_str());
454
455 // Connectivity diagnostics: enumerate doors/staircases/holewarp without
456 // reciprocity filtering so the report captures the room's actual link
457 // configuration. Staircase configuration issues (stale headers, missing
458 // destinations, extra placed objects beyond 4 slots) are listed
459 // separately so a reader can spot phantom or unreachable connections.
460 const auto link_diagnostics =
461 CollectDungeonConnectedRoomLinkDiagnostics(room_id, room, nullptr);
462 if (!link_diagnostics.links.empty() ||
463 !link_diagnostics.staircase_issues.empty()) {
464 summary += "\nConnectivity:";
465 for (const auto& link : link_diagnostics.links) {
466 summary += "\n ";
468 }
469 for (const auto& issue : link_diagnostics.staircase_issues) {
470 summary += "\n ";
472 }
473 }
474
475 return summary;
476}
477
479 int room_id) const {
480 std::string report = "Dungeon Draw Issue Report\n";
481 report += BuildRoomMetadataSummary(room, room_id);
482 report += absl::StrFormat(
483 "\nResolved palette group: 0x%02X\nInteraction mode: %s\nView: BG1=%s "
484 "BG2=%s Grid=%s Bounds=%s Sprites=%s Pots=%s Collision=%s Camera=%s",
487 IsBG1Visible(room_id) ? "on" : "off",
488 IsBG2Visible(room_id) ? "on" : "off", show_grid_ ? "on" : "off",
489 show_object_bounds_ ? "on" : "off",
490 entity_visibility_.show_sprites ? "on" : "off",
491 entity_visibility_.show_pot_items ? "on" : "off",
492 show_custom_collision_overlay_ ? "on" : "off",
493 show_camera_quadrant_overlay_ ? "on" : "off");
494
495 const auto selected = object_interaction_.GetSelectedObjectIndices();
496 if (!selected.empty()) {
497 report += absl::StrFormat("\nSelected objects: %zu", selected.size());
498 if (selected.size() == 1) {
499 const auto& objects = room.GetTileObjects();
500 const size_t index = selected.front();
501 if (index < objects.size()) {
502 const auto& obj = objects[index];
503 report += absl::StrFormat(
504 "\nObject: id=0x%03X name=%s pos=(%d,%d) size=0x%02X %s", obj.id_,
505 GetObjectName(obj.id_).c_str(), obj.x_, obj.y_, obj.size_,
506 BuildSelectedObjectSemanticsSummary(obj).c_str());
507 if (auto tiles = obj.GetTiles(); tiles.ok()) {
508 report += "\n";
509 report += FormatObjectTileSample(*tiles, 16, "Object tiles:");
510 }
511
512 auto [bounds_x, bounds_y, bounds_w, bounds_h] =
514 bounds_w = std::max(bounds_w, 8);
515 bounds_h = std::max(bounds_h, 8);
516 const int origin_x = obj.x_ * 8;
517 const int origin_y = obj.y_ * 8;
518 const int center_x = bounds_x + std::max(0, (bounds_w / 2) - 1);
519 const int center_y = bounds_y + std::max(0, (bounds_h / 2) - 1);
520 report += absl::StrFormat(
521 "\nObject geometry: selection_bounds_px=(%d,%d,%d,%d) "
522 "object_origin_px=(%d,%d) center_px=(%d,%d)",
523 bounds_x, bounds_y, bounds_w, bounds_h, origin_x, origin_y,
524 center_x, center_y);
525 const auto trace_report =
526 BuildObjectTraceReport(rom_, room_id, obj, current_palette_group_);
527 report += trace_report.summary;
528
529 auto& palette_debugger = zelda3::PaletteDebugger::Get();
530 auto append_sample = [&](std::string_view label, int sample_x,
531 int sample_y) {
532 const auto comp = palette_debugger.SamplePixelAt(sample_x, sample_y);
533 palette_debugger.AddComparison(comp);
534 const std::string label_text(label);
535 report += absl::StrFormat(
536 "\nPalette sample %s (%d,%d): idx=%d expected=(%d,%d,%d) "
537 "actual=(%d,%d,%d) match=%s",
538 label_text.c_str(), sample_x, sample_y,
539 static_cast<int>(comp.palette_index),
540 static_cast<int>(comp.expected_r),
541 static_cast<int>(comp.expected_g),
542 static_cast<int>(comp.expected_b),
543 static_cast<int>(comp.actual_r), static_cast<int>(comp.actual_g),
544 static_cast<int>(comp.actual_b), comp.matches ? "yes" : "no");
545 };
546
547 if (auto trace_sample = ChooseObjectTracePaletteSample(
548 trace_report.writes, palette_debugger)) {
549 append_sample(trace_sample->label, trace_sample->x, trace_sample->y);
550 } else {
551 append_sample("selection-origin", bounds_x, bounds_y);
552 if (center_x != bounds_x || center_y != bounds_y) {
553 append_sample("selection-center", center_x, center_y);
554 }
555 }
556 } else {
557 report += absl::StrFormat(
558 "\nSelected object index %zu is stale; room currently has %zu "
559 "object(s). Reselect the object and report again if this issue "
560 "still reproduces.",
561 index, objects.size());
562 }
563 }
564 }
565
567 const auto selected_entity = object_interaction_.GetSelectedEntity();
568 switch (selected_entity.type) {
569 case EntityType::Door: {
570 const auto& doors = room.GetDoors();
571 if (selected_entity.index < doors.size()) {
572 const auto& door = doors[selected_entity.index];
573 report += BuildDoorIssueSummary(door, selected_entity.index,
574 "Selected door");
575 }
576 break;
577 }
578 case EntityType::Sprite: {
579 const auto& sprites = room.GetSprites();
580 if (selected_entity.index < sprites.size()) {
581 const auto& sprite = sprites[selected_entity.index];
582 report += absl::StrFormat(
583 "\nSelected entity: sprite id=0x%02X name=%s pos=(%d,%d) "
584 "layer=%d",
585 sprite.id(), zelda3::ResolveSpriteName(sprite.id()), sprite.x(),
586 sprite.y(), sprite.layer());
587 }
588 break;
589 }
590 case EntityType::Item: {
591 const auto& items = room.GetPotItems();
592 if (selected_entity.index < items.size()) {
593 const auto& item = items[selected_entity.index];
594 report +=
595 absl::StrFormat("\nSelected entity: pot item=0x%02X pos=(%d,%d)",
596 item.item, item.GetPixelX(), item.GetPixelY());
597 }
598 break;
599 }
600 default:
601 break;
602 }
603 }
604
605 const auto& palette_events = zelda3::PaletteDebugger::Get().GetEvents();
606 if (!palette_events.empty()) {
607 report +=
608 absl::StrFormat("\nPalette debug events: %zu", palette_events.size());
609 const size_t preview_count = std::min<size_t>(palette_events.size(), 4);
610 const size_t start = palette_events.size() - preview_count;
611 for (size_t i = start; i < palette_events.size(); ++i) {
612 const auto& event = palette_events[i];
613 report += "\n";
614 report += FormatPaletteDebugEventForReport(event);
615 }
616 }
617
618 return report;
619}
620
622 const zelda3::Room& room, int room_id) const {
623 std::string report = "Dungeon Selection Issue Report\n";
624 report += BuildRoomMetadataSummary(room, room_id);
625 report +=
626 absl::StrFormat("\nInteraction mode: %s\nResolved palette group: 0x%02X",
629
630 const auto selected_objects = object_interaction_.GetSelectedObjectIndices();
631 if (!selected_objects.empty()) {
632 report +=
633 absl::StrFormat("\nSelected objects: %zu", selected_objects.size());
634 const auto& objects = room.GetTileObjects();
635 const size_t preview_count = std::min<size_t>(selected_objects.size(), 4);
636 for (size_t i = 0; i < preview_count; ++i) {
637 const size_t index = selected_objects[i];
638 if (index >= objects.size()) {
639 report += absl::StrFormat(
640 "\n- object[%zu] unavailable; room currently has %zu object(s) "
641 "(selection is stale)",
642 index, objects.size());
643 continue;
644 }
645 const auto& obj = objects[index];
646 report += absl::StrFormat(
647 "\n- object[%zu] id=0x%03X name=%s pos=(%d,%d) size=0x%02X %s", index,
648 obj.id_, GetObjectName(obj.id_).c_str(), obj.x_, obj.y_, obj.size_,
649 BuildSelectedObjectSemanticsSummary(obj).c_str());
650 auto [bounds_x, bounds_y, bounds_w, bounds_h] =
652 bounds_w = std::max(bounds_w, 8);
653 bounds_h = std::max(bounds_h, 8);
654 report += absl::StrFormat(
655 "\n geometry: selection_bounds_px=(%d,%d,%d,%d) "
656 "object_origin_px=(%d,%d)",
657 bounds_x, bounds_y, bounds_w, bounds_h, obj.x_ * 8, obj.y_ * 8);
658 if (auto tiles = obj.GetTiles(); tiles.ok() && !tiles->empty()) {
659 report += "\n ";
660 report += FormatObjectTileSample(*tiles, 10, "tiles:");
661 }
662 const auto trace_report =
663 BuildObjectTraceReport(rom_, room_id, obj, current_palette_group_);
664 report += "\n ";
665 report += BuildObjectTraceBoundsSummary(trace_report.writes, bounds_x,
666 bounds_y, bounds_w, bounds_h);
667 }
668 if (selected_objects.size() > preview_count) {
669 report += absl::StrFormat("\n- ... %zu more object(s)",
670 selected_objects.size() - preview_count);
671 }
672 return report;
673 }
674
676 const auto selected_entity = object_interaction_.GetSelectedEntity();
677 switch (selected_entity.type) {
678 case EntityType::Door: {
679 const auto& doors = room.GetDoors();
680 if (selected_entity.index < doors.size()) {
681 const auto& door = doors[selected_entity.index];
682 report += BuildDoorIssueSummary(door, selected_entity.index,
683 "Selected door");
684 return report;
685 }
686 break;
687 }
688 case EntityType::Sprite: {
689 const auto& sprites = room.GetSprites();
690 if (selected_entity.index < sprites.size()) {
691 const auto& sprite = sprites[selected_entity.index];
692 report += absl::StrFormat(
693 "\nSelected sprite[%zu]:\n"
694 "- id=0x%02X\n"
695 "- name=%s\n"
696 "- pos=(%d,%d)\n"
697 "- layer=%d",
698 selected_entity.index, sprite.id(),
699 zelda3::ResolveSpriteName(sprite.id()), sprite.x(), sprite.y(),
700 sprite.layer());
701 return report;
702 }
703 break;
704 }
705 case EntityType::Item: {
706 const auto& items = room.GetPotItems();
707 if (selected_entity.index < items.size()) {
708 const auto& item = items[selected_entity.index];
709 report += absl::StrFormat(
710 "\nSelected pot item[%zu]:\n"
711 "- item=0x%02X\n"
712 "- pixel_pos=(%d,%d)",
713 selected_entity.index, item.item, item.GetPixelX(),
714 item.GetPixelY());
715 return report;
716 }
717 break;
718 }
719 default:
720 break;
721 }
722 }
723
724 report += "\nNo active object or entity selection.";
725 return report;
726}
727
731
732 auto paths_or = ResolveDungeonIssueReportPaths();
733 if (!paths_or.ok()) {
734 SetIssueReportPopupStatus(std::string(paths_or.status().message()), true);
735 return;
736 }
737
741 util::PlatformPaths::NormalizePathForDisplay(paths_or->screenshots_dir);
742}
743
745 bool is_error) {
746 issue_report_popup_status_message_ = std::move(message);
748}
749
753
759 return;
760 }
761
762 gui::SeparatorText(kIssueReportSectionPaths);
764 ImGui::TextWrapped(tr("Log target: %s"),
766 }
769 ImGui::TextWrapped(tr("Screenshot dir: %s"),
771 }
773 ImGui::TextWrapped(tr("Screenshot: %s"),
775 }
777 ImGui::TextWrapped(tr("Issue log: %s"),
779 }
780}
781
784 return;
785 }
786
787 const ImVec4 color = issue_report_popup_status_is_error_
790 ImGui::TextColored(color, "%s", issue_report_popup_status_message_.c_str());
791}
792
794 const std::string& title, const std::string& summary,
795 const std::string& kind_label, const std::string& diagnostics, int room_id,
796 int default_category_index) {
798 issue_report_popup_kind_ = kind_label;
802 std::clamp(default_category_index, 0,
803 static_cast<int>(kDungeonIssueCategoryLabels.size()) - 1);
807 SetIssueReportPopupStatus("", false);
809 std::snprintf(issue_report_summary_, sizeof(issue_report_summary_), "%s",
810 summary.c_str());
811 issue_report_notes_[0] = '\0';
812 const auto initial_persist_status = EnsureIssueReportPersisted();
813 if (initial_persist_status.ok()) {
815 absl::StrFormat(kIssueReportStatusStartedLog,
817 false);
818 } else {
819 SetIssueReportPopupStatus(std::string(initial_persist_status.message()),
820 true);
821 }
822 return initial_persist_status;
823}
824
825void DungeonCanvasViewer::OpenIssueReportPopup(const std::string& title,
826 const std::string& summary,
827 const std::string& kind_label,
828 const std::string& diagnostics,
829 int room_id,
830 int default_category_index) {
831 (void)PrepareIssueReportPopup(title, summary, kind_label, diagnostics,
832 room_id, default_category_index);
834 if (ImGui::BeginPopupModal(issue_report_popup_id_.c_str(), nullptr,
835 ImGuiWindowFlags_AlwaysAutoResize)) {
836 if (!issue_report_popup_title_.empty()) {
837 ImGui::TextUnformatted(issue_report_popup_title_.c_str());
838 if (!issue_report_popup_kind_.empty()) {
839 ImGui::TextDisabled("%s", issue_report_popup_kind_.c_str());
840 }
841 ImGui::Separator();
842 }
843
844 const char* category_preview =
846 if (ImGui::BeginCombo(tr("Category"), category_preview)) {
847 for (size_t i = 0; i < kDungeonIssueCategoryLabels.size(); ++i) {
848 const bool selected =
849 issue_report_category_index_ == static_cast<int>(i);
850 if (ImGui::Selectable(kDungeonIssueCategoryLabels[i], selected)) {
851 issue_report_category_index_ = static_cast<int>(i);
853 }
854 if (selected) {
855 ImGui::SetItemDefaultFocus();
856 }
857 }
858 ImGui::EndCombo();
859 }
860
861 ImGui::SetNextItemWidth(kIssueReportDialogWidth);
862 if (ImGui::InputTextWithHint("Summary", kIssueReportSummaryHint,
864 sizeof(issue_report_summary_))) {
866 }
867 if (ImGui::InputTextMultiline(
868 tr("Observed issue"), issue_report_notes_,
869 sizeof(issue_report_notes_),
870 ImVec2(kIssueReportDialogWidth, kIssueReportNotesHeight))) {
872 }
873
876 ImGui::TextDisabled(
877 tr("Reports auto-save on open, screenshot capture, copy, or close."));
878
879 if (ImGui::CollapsingHeader(kIssueReportSectionDiagnostics,
880 ImGuiTreeNodeFlags_DefaultOpen)) {
881 ImGui::InputTextMultiline(
882 kIssueReportPopupIdDiagnostics,
885 ImVec2(kIssueReportDialogWidth, kIssueReportDiagnosticsHeight),
886 ImGuiInputTextFlags_ReadOnly);
887 }
888
889#ifdef YAZE_WITH_GRPC
890 const char* capture_label = issue_report_popup_screenshot_path_.empty()
891 ? kIssueReportCaptureButtonLabel
892 : kIssueReportRecaptureButtonLabel;
893 if (ImGui::Button(capture_label)) {
894 const auto status = CaptureIssueReportScreenshot();
895 if (status.ok()) {
897 const auto persist_status = EnsureIssueReportPersisted();
898 if (persist_status.ok()) {
900 absl::StrFormat(kIssueReportStatusSavedScreenshot,
902 false);
903 } else {
904 SetIssueReportPopupStatus(std::string(persist_status.message()),
905 true);
906 }
907 } else {
908 SetIssueReportPopupStatus(std::string(status.message()), true);
909 }
910 }
911 ImGui::SameLine();
912#endif
913 if (ImGui::Button(ICON_MD_SAVE_AS " Save to Issue Log")) {
914 const auto status = EnsureIssueReportPersisted();
915 if (status.ok()) {
917 absl::StrFormat(kIssueReportStatusSavedLog,
919 false);
920 } else {
921 SetIssueReportPopupStatus(std::string(status.message()), true);
922 }
923 }
924 ImGui::SameLine();
925 if (ImGui::Button(ICON_MD_CONTENT_COPY " Copy Report")) {
926 std::string report = BuildIssueReportClipboardText();
927 ImGui::SetClipboardText(report.c_str());
928 const auto status = EnsureIssueReportPersisted();
929 if (status.ok()) {
931 absl::StrFormat(kIssueReportStatusCopiedReport,
933 false);
934 } else {
935 SetIssueReportPopupStatus(std::string(status.message()), true);
936 }
937 }
938 ImGui::SameLine();
939 if (ImGui::Button(ICON_MD_ASSIGNMENT " Copy Diagnostics")) {
940 ImGui::SetClipboardText(issue_report_popup_diagnostics_.c_str());
941 const auto status = EnsureIssueReportPersisted();
942 if (status.ok()) {
944 absl::StrFormat(kIssueReportStatusCopiedDiagnostics,
946 false);
947 } else {
948 SetIssueReportPopupStatus(std::string(status.message()), true);
949 }
950 }
951 ImGui::SameLine();
952 if (ImGui::Button(ICON_MD_CLOSE " Close")) {
953 const auto status = EnsureIssueReportPersisted();
954 if (!status.ok()) {
955 SetIssueReportPopupStatus(std::string(status.message()), true);
956 ImGui::EndPopup();
957 return;
958 }
960 ImGui::CloseCurrentPopup();
961 }
962 ImGui::EndPopup();
963 return;
964 }
965
968 }
969 });
970}
971
973 std::string report;
974 if (!issue_report_popup_kind_.empty()) {
975 report += issue_report_popup_kind_ + "\n";
976 }
977 report += absl::StrFormat(
979 if (issue_report_summary_[0] != '\0') {
980 report += absl::StrFormat("Summary: %s\n", issue_report_summary_);
981 }
983 report += absl::StrFormat("Screenshot: %s\n",
985 }
986 if (issue_report_notes_[0] != '\0') {
987 report += absl::StrFormat("\nObserved issue:\n%s\n", issue_report_notes_);
988 }
989 report += "\nDiagnostics:\n";
991 return report;
992}
993
995#ifdef YAZE_WITH_GRPC
998 return absl::FailedPreconditionError(
999 "Room canvas region is not available for screenshot capture");
1000 }
1001
1002 const std::string timestamp = BuildIssueTimestampSlug();
1003 auto screenshot_path_or = BuildDungeonIssueScreenshotPath(
1006 if (!screenshot_path_or.ok()) {
1007 return screenshot_path_or.status();
1008 }
1009
1010 yaze::test::CaptureRegion region;
1011 region.x = canvas_capture_x_;
1012 region.y = canvas_capture_y_;
1013 region.width = canvas_capture_width_;
1014 region.height = canvas_capture_height_;
1015
1016 auto artifact_or = yaze::test::CaptureHarnessScreenshotRegion(
1017 std::optional<yaze::test::CaptureRegion>(region),
1018 screenshot_path_or->string(),
1019 /*reveal_to_user=*/false);
1020 if (!artifact_or.ok()) {
1021 return artifact_or.status();
1022 }
1023
1025 util::PlatformPaths::NormalizePathForDisplay(artifact_or->file_path);
1026 return absl::OkStatus();
1027#else
1028 return absl::UnimplementedError(
1029 "Screenshot capture is unavailable without YAZE_WITH_GRPC");
1030#endif
1031}
1032
1035 entry.timestamp_display = BuildIssueTimestampDisplay();
1044
1045 auto log_path_or = AppendDungeonIssueLogEntry(entry);
1046 if (!log_path_or.ok()) {
1047 return log_path_or.status();
1048 }
1049
1052 return absl::OkStatus();
1053}
1054
1058 return absl::OkStatus();
1059 }
1060
1063 return absl::OkStatus();
1064}
1065
1066} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
auto filename() const
Definition rom.h:175
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
auto short_name() const
Definition rom.h:177
bool is_loaded() const
Definition rom.h:155
auto title() const
Definition rom.h:167
void SetIssueReportPopupStatus(std::string message, bool is_error)
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)
std::string BuildRoomMetadataSummary(const zelda3::Room &room, int room_id) const
const project::YazeProject * project_
std::string BuildDrawIssueReport(const zelda3::Room &room, int room_id) const
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)
std::string BuildSelectionIssueReport(const zelda3::Room &room, int room_id) const
std::vector< size_t > GetSelectedObjectIndices() const
const char * GetModeName() const
Get mode name for debugging/UI.
void ClosePersistentPopup(const std::string &popup_id)
Definition canvas.cc:884
void OpenPersistentPopup(const std::string &popup_id, std::function< void()> render_callback)
Definition canvas.cc:878
static std::string NormalizePathForDisplay(const std::filesystem::path &path)
Normalize path separators for display.
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
Draws dungeon objects to background buffers using game patterns.
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)
Draw a room object to background buffers.
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
uint8_t blockset() const
Definition room.h:903
const std::vector< Door > & GetDoors() const
Definition room.h:351
TagKey tag2() const
Definition room.h:890
uint8_t palette() const
Definition room.h:906
TagKey tag1() const
Definition room.h:889
uint8_t spriteset() const
Definition room.h:905
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:254
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:383
auto blocks() const
Definition room.h:957
EffectKey effect() const
Definition room.h:888
uint8_t floor1() const
Definition room.h:927
int ResolveDungeonPaletteId() const
Definition room.cc:727
uint8_t render_entrance_blockset() const
Definition room.h:904
uint8_t layout_id() const
Definition room.h:913
const std::vector< PotItem > & GetPotItems() const
Definition room.h:367
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_ADD_A_PHOTO
Definition icons.h:87
#define ICON_MD_ASSIGNMENT
Definition icons.h:194
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_PHOTO_CAMERA
Definition icons.h:1453
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)
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)
std::optional< PaletteSamplePoint > ChooseObjectTracePaletteSample(std::span< const zelda3::ObjectDrawer::TileTrace > trace, const zelda3::PaletteDebugger &palette_debugger)
std::string FormatPaletteDebugEventForReport(const zelda3::PaletteDebugEvent &event)
std::string BuildReportSessionSummary(const Rom *rom, const project::YazeProject *project)
ObjectTraceReport BuildObjectTraceReport(Rom *rom, int room_id, const zelda3::RoomObject &obj, const gfx::PaletteGroup &palette_group)
std::string FormatObjectTileSample(std::span< const gfx::TileInfo > tiles, size_t max_count, std::string_view prefix)
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 &timestamp_slug)
absl::StatusOr< DungeonIssueReportPaths > ResolveDungeonIssueReportPaths()
std::string FormatDungeonStaircaseIssueDescription(const DungeonStaircaseIssue &issue)
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
void SeparatorText(const char *label)
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
std::string ComputeRomHash(const uint8_t *data, size_t size)
Definition rom_hash.cc:70
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.
Definition door_types.h:204
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Represents a group of palettes.
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
bool project_opened() const
Definition project.h:348
std::string GetDisplayName() const
Definition project.cc:1464
Represents a door in a dungeon room.
Definition room.h:277
std::tuple< int, int, int, int > GetEditorBounds() const
Get editor interaction bounds (x, y, width, height in pixels)
Definition room.h:311
std::tuple< int, int, int, int > GetBounds() const
Get bounding rectangle (x, y, width, height in pixels)
Definition room.h:306
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:282
DoorDimensions GetDimensions() const
Get door dimensions in tiles.
Definition room.h:296
DoorType type
Door type (determines appearance/behavior)
Definition room.h:279
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:327
DoorDirection direction
Which wall the door is on.
Definition room.h:280
DoorDimensions GetEditorDimensions() const
Get editor interaction dimensions in tiles.
Definition room.h:301
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:278
std::pair< int, int > GetPixelCoords() const
Get pixel coordinates for this door.
Definition room.h:291
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:283
std::pair< int, int > GetTileCoords() const
Get tile coordinates for this door.
Definition room.h:286