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"
24#include "app/gui/core/icons.h"
28#ifdef YAZE_WITH_GRPC
30#endif
31#include "imgui/imgui.h"
32#include "imgui/imgui_internal.h"
33#include "util/macro.h"
34#include "util/platform_paths.h"
35#include "util/rom_hash.h"
43
44namespace yaze::editor {
45
46namespace {
47
48constexpr float kIssueReportDialogWidth = 640.0f;
49constexpr float kIssueReportDialogHeight = 620.0f;
50constexpr float kIssueReportDialogMinWidth = 360.0f;
51constexpr float kIssueReportDialogMinHeight = 340.0f;
52constexpr float kIssueReportDialogViewportMargin = 16.0f;
53constexpr float kIssueReportNotesHeight = 116.0f;
54constexpr float kIssueReportDiagnosticsHeight = 168.0f;
55constexpr char kIssueReportSummaryHint[] = "What looks wrong?";
56constexpr char kIssueReportPopupIdDiagnostics[] = "##DungeonIssueDiagnostics";
57constexpr char kIssueReportSectionPaths[] = "Local files";
58constexpr char kIssueReportSectionDiagnostics[] = "Diagnostics";
59constexpr char kIssueReportStatusIdle[] =
60 "Local draft. Nothing is uploaded or submitted.";
62 "Screenshot captured. Open Local files for its path.";
63constexpr char kIssueReportStatusSavedLog[] = "Saved to the local issue log.";
65 "Copied report to clipboard. Nothing was submitted.";
67 "Copied diagnostics to clipboard. Nothing was submitted.";
69 ICON_MD_ADD_A_PHOTO " Capture Screenshot";
71 ICON_MD_PHOTO_CAMERA " Re-capture Screenshot";
73 ICON_MD_SAVE_AS " Save to Issue Log";
75 ICON_MD_CONTENT_COPY " Copy Report";
76constexpr char kIssueReportCloseButtonLabel[] = ICON_MD_CLOSE " Close";
77
79 ImVec2 center;
81 ImVec2 min_size;
82 ImVec2 max_size;
83};
84
86 const ImGuiViewport* viewport = ImGui::GetMainViewport();
87 const ImVec2 work_size =
88 viewport != nullptr ? viewport->WorkSize : ImGui::GetIO().DisplaySize;
89 const ImVec2 center = viewport != nullptr
90 ? viewport->GetWorkCenter()
91 : ImVec2(work_size.x * 0.5f, work_size.y * 0.5f);
92 const ImVec2 max_size(
93 std::max(1.0f, work_size.x - kIssueReportDialogViewportMargin * 2.0f),
94 std::max(1.0f, work_size.y - kIssueReportDialogViewportMargin * 2.0f));
95 const ImVec2 min_size(std::min(kIssueReportDialogMinWidth, max_size.x),
96 std::min(kIssueReportDialogMinHeight, max_size.y));
97 const ImVec2 initial_size(
98 std::clamp(kIssueReportDialogWidth, min_size.x, max_size.x),
99 std::clamp(kIssueReportDialogHeight, min_size.y, max_size.y));
100 return {center, initial_size, min_size, max_size};
101}
102
103float GetIssueActionButtonWidth(const char* label) {
104 float text_width = ImGui::CalcTextSize(label).x;
105 if (label == kIssueReportCaptureButtonLabel ||
107 text_width =
108 std::max(ImGui::CalcTextSize(kIssueReportCaptureButtonLabel).x,
109 ImGui::CalcTextSize(kIssueReportRecaptureButtonLabel).x);
110 }
111 return text_width + ImGui::GetStyle().FramePadding.x * 2.0f;
112}
113
114int CountWrappedIssueActionRows(std::span<const char* const> labels,
115 float available_width) {
116 const float spacing = ImGui::GetStyle().ItemSpacing.x;
117 float used_width = 0.0f;
118 int rows = labels.empty() ? 0 : 1;
119 for (const char* label : labels) {
120 const float button_width = GetIssueActionButtonWidth(label);
121 if (used_width > 0.0f &&
122 used_width + spacing + button_width > available_width) {
123 ++rows;
124 used_width = button_width;
125 } else {
126 used_width += (used_width > 0.0f ? spacing : 0.0f) + button_width;
127 }
128 }
129 return rows;
130}
131
132void PlaceNextIssueAction(const char* label, float available_width,
133 float* used_width) {
134 const float spacing = ImGui::GetStyle().ItemSpacing.x;
135 const float button_width = GetIssueActionButtonWidth(label);
136 if (*used_width > 0.0f &&
137 *used_width + spacing + button_width <= available_width) {
138 ImGui::SameLine();
139 *used_width += spacing + button_width;
140 return;
141 }
142 *used_width = button_width;
143}
144
145const char* GetStoredPlacementLabel(const zelda3::RoomObject& object) {
146 const int layer_value = object.GetLayerValue();
147 if (!zelda3::UsesRoomObjectStream(object)) {
148 if (layer_value == 0) {
149 return "Upper layer (BG1)";
150 }
151 return layer_value == 1 ? "Lower layer (BG2)" : "Invalid selector";
152 }
153 switch (layer_value) {
154 case 0:
155 return "Primary";
156 case 1:
157 return "BG2 overlay";
158 case 2:
159 return "BG1 overlay";
160 default:
161 return "Unknown";
162 }
163}
164
165const char* GetBlocksetGroupName(uint8_t blockset) {
166 static const char* kGroupNames[] = {
167 "HC/Sewers", "Eastern", "Desert", "Hera", "A-Tower", "PoD", "Swamp",
168 "Skull", "Thieves", "Ice", "Misery", "Turtle", "GT",
169 };
170 constexpr size_t kCount = sizeof(kGroupNames) / sizeof(kGroupNames[0]);
171 return blockset < kCount ? kGroupNames[blockset] : "Custom";
172}
173
175 return absl::FormatTime("%Y%m%d-%H%M%S", absl::Now(), absl::LocalTimeZone());
176}
177
179 return absl::FormatTime("%Y-%m-%d %H:%M:%S %Z", absl::Now(),
180 absl::LocalTimeZone());
181}
182
184 const auto blocks = room.blocks();
185 if (blocks.size() < 8) {
186 return "BG sheets: unavailable";
187 }
188 return absl::StrFormat(
189 "BG sheets: [%02X %02X %02X %02X %02X %02X %02X %02X]",
190 static_cast<int>(blocks[0]), static_cast<int>(blocks[1]),
191 static_cast<int>(blocks[2]), static_cast<int>(blocks[3]),
192 static_cast<int>(blocks[4]), static_cast<int>(blocks[5]),
193 static_cast<int>(blocks[6]), static_cast<int>(blocks[7]));
194}
195
197 const auto semantics = zelda3::GetObjectLayerSemantics(obj);
199 return absl::StrFormat(
200 "storage=special-table selector=%d (%s) routine=%d "
201 "legacy_both_bgs=%s "
202 "routing=%s",
203 obj.GetLayerValue(), GetStoredPlacementLabel(obj), semantics.routine_id,
204 semantics.draws_to_both_bgs ? "yes" : "no",
205 zelda3::ObjectRenderRoutingToken(semantics.render_routing));
206 }
207 return absl::StrFormat(
208 "stream=%s draw_layer=%d effective_bg=%s routine=%d "
209 "legacy_both_bgs=%s "
210 "routing=%s",
212 static_cast<int>(
214 zelda3::EffectiveBgLayerLabel(semantics.effective_bg_layer),
215 semantics.routine_id, semantics.draws_to_both_bgs ? "yes" : "no",
216 zelda3::ObjectRenderRoutingToken(semantics.render_routing));
217}
218
219const char* TraceLayerLabel(uint8_t layer) {
220 switch (static_cast<zelda3::RoomObject::LayerType>(layer)) {
222 return "BG1";
224 return "BG2";
226 return "BG3";
227 }
228 return "BG?";
229}
230
231std::string FormatObjectTileSample(std::span<const gfx::TileInfo> tiles,
232 size_t max_count, std::string_view prefix) {
233 std::string out = absl::StrFormat("%s count=%zu", std::string(prefix).c_str(),
234 tiles.size());
235 const size_t preview_count = std::min<size_t>(tiles.size(), max_count);
236 for (size_t i = 0; i < preview_count; ++i) {
237 const auto& tile = tiles[i];
238 out += absl::StrFormat(" [%zu]=0x%03X/p%d/pr%d/h%d/v%d", i, tile.id_,
239 static_cast<int>(tile.palette_), tile.over_ ? 1 : 0,
240 tile.horizontal_mirror_ ? 1 : 0,
241 tile.vertical_mirror_ ? 1 : 0);
242 }
243 if (tiles.size() > preview_count) {
244 out += absl::StrFormat(" ... %zu more", tiles.size() - preview_count);
245 }
246 return out;
247}
248
250 std::string summary;
251 std::vector<zelda3::ObjectDrawer::TileTrace> writes;
252};
253
255 std::array<gfx::Bitmap*, 2> bitmaps;
256
258 for (gfx::Bitmap* bitmap : bitmaps) {
259 if (bitmap != nullptr) {
260 gfx::Arena::Get().RetireBitmap(*bitmap);
261 }
262 }
263 }
264};
265
267 Rom* rom, const zelda3::Room& room, size_t selected_index,
268 const gfx::PaletteGroup& palette_group) {
269 if (rom == nullptr || !rom->is_loaded()) {
270 return {.summary = "\nDrawer trace: unavailable (ROM not loaded)"};
271 }
272
273 const auto& objects = room.GetTileObjects();
274 if (selected_index >= objects.size()) {
275 return {.summary = "\nDrawer trace: unavailable (stale selection)"};
276 }
277 if (!zelda3::UsesRoomObjectStream(objects[selected_index])) {
278 return {.summary =
279 "\nDrawer trace: unavailable (special-table object uses the "
280 "dedicated room pass)"};
281 }
282
283 // The report must follow the same primary -> BG2 overlay -> BG1 overlay
284 // order as Room::RenderObjectsToBackground. Earlier object writes can own a
285 // physical tilemap entry and therefore override the matching layout word
286 // used by conditional edge/cap routines.
287 zelda3::ObjectDrawer drawer(rom, room.id(), room.get_gfx_buffer().data());
288 drawer.SetRoomFloorGraphics(room.floor1(), room.floor2());
290 gfx::BackgroundBuffer bg1(512, 512);
291 gfx::BackgroundBuffer bg2(512, 512);
292 gfx::BackgroundBuffer layout_bg1(512, 512);
293 gfx::BackgroundBuffer layout_bg2(512, 512);
294 ScopedBitmapRetirement retire_trace_bitmaps{{&bg1.bitmap(), &bg2.bitmap()}};
295 for (auto* buffer : {&bg1, &bg2}) {
296 buffer->EnsureBitmapInitialized();
297 buffer->bitmap().Fill(255);
298 buffer->ClearBuffer();
299 }
300 layout_bg1.buffer() = room.bg1_buffer().buffer();
301 layout_bg2.buffer() = room.bg2_buffer().buffer();
302
303 std::vector<zelda3::ObjectDrawer::TileTrace> trace;
304 absl::Status status = absl::NotFoundError("selected object not replayed");
305 bool selected_was_replayed = false;
306 for (uint8_t pass = 0; pass < 3 && !selected_was_replayed; ++pass) {
307 for (size_t index = 0; index < objects.size(); ++index) {
308 const auto& stored_obj = objects[index];
309 if (!zelda3::UsesRoomObjectStream(stored_obj)) {
310 continue;
311 }
312 const uint8_t list_index =
313 std::min<uint8_t>(stored_obj.GetLayerValue(), 2);
314 if (list_index != pass) {
315 continue;
316 }
317
318 zelda3::RoomObject render_obj = stored_obj;
319 render_obj.layer_ = zelda3::MapRoomObjectListIndexToDrawLayer(list_index);
320 if (index == selected_index) {
321 drawer.SetTraceCollector(&trace, /*trace_only=*/false);
322 status =
323 drawer.DrawObject(render_obj, bg1, bg2, palette_group,
324 room.GetDungeonState(), &layout_bg1, &layout_bg2);
325 drawer.ClearTraceCollector();
326 selected_was_replayed = true;
327 break;
328 }
329
330 status =
331 drawer.DrawObject(render_obj, bg1, bg2, palette_group,
332 room.GetDungeonState(), &layout_bg1, &layout_bg2);
333 if (!status.ok()) {
334 break;
335 }
336 }
337 if (!status.ok() && !selected_was_replayed) {
338 break;
339 }
340 }
341 if (!status.ok()) {
342 return {.summary = absl::StrFormat("\nDrawer trace: unavailable (%s)",
343 std::string(status.message()).c_str())};
344 }
345
346 if (trace.empty()) {
347 return {.summary = "\nDrawer trace: status=ok writes=0"};
348 }
349
350 int min_x = std::numeric_limits<int>::max();
351 int min_y = std::numeric_limits<int>::max();
352 int max_x = std::numeric_limits<int>::min();
353 int max_y = std::numeric_limits<int>::min();
354 std::array<int, 3> layer_counts = {0, 0, 0};
355 std::set<std::tuple<int, int, uint8_t>> unique_cells;
356 for (const auto& write : trace) {
357 min_x = std::min<int>(min_x, write.x_tile);
358 min_y = std::min<int>(min_y, write.y_tile);
359 max_x = std::max<int>(max_x, write.x_tile);
360 max_y = std::max<int>(max_y, write.y_tile);
361 if (write.layer < layer_counts.size()) {
362 ++layer_counts[write.layer];
363 }
364 unique_cells.insert({write.x_tile, write.y_tile, write.layer});
365 }
366
367 std::string out = absl::StrFormat(
368 "\nDrawer trace: status=ok context=room-stream-prefix+layout-tilewords "
369 "writes=%zu unique_cells=%zu "
370 "bounds_tiles=(%d,%d)..(%d,%d) layer_counts BG1=%d BG2=%d BG3=%d",
371 trace.size(), unique_cells.size(), min_x, min_y, max_x, max_y,
375
376 const size_t preview_count = std::min<size_t>(trace.size(), 12);
377 for (size_t i = 0; i < preview_count; ++i) {
378 const auto& write = trace[i];
379 const int palette = (write.flags >> 3) & 0x7;
380 out += absl::StrFormat(
381 "\n write[%zu] %s tile=(%d,%d) id=0x%03X pal=%d pri=%d h=%d v=%d", i,
382 TraceLayerLabel(write.layer), write.x_tile, write.y_tile, write.tile_id,
383 palette, (write.flags & 0x4) ? 1 : 0, (write.flags & 0x1) ? 1 : 0,
384 (write.flags & 0x2) ? 1 : 0);
385 }
386 if (trace.size() > preview_count) {
387 out += absl::StrFormat("\n ... %zu more write(s)",
388 trace.size() - preview_count);
389 }
390 return {.summary = std::move(out), .writes = std::move(trace)};
391}
392
394 std::string label;
395 int x = 0;
396 int y = 0;
397};
398
399bool PaletteIndexMatchesTracePalette(int palette_index, uint8_t trace_flags) {
400 if (palette_index < 0 || (palette_index & 0x0F) == 0) {
401 return false;
402 }
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;
405}
406
407std::optional<PaletteSamplePoint> ChooseObjectTracePaletteSample(
408 std::span<const zelda3::ObjectDrawer::TileTrace> trace,
409 const zelda3::PaletteDebugger& palette_debugger) {
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},
413 };
414
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));
422
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);
427 if (PaletteIndexMatchesTracePalette(comp.palette_index, write.flags)) {
428 return PaletteSamplePoint{.label = label, .x = sample_x, .y = sample_y};
429 }
430 }
431
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);
437 if (PaletteIndexMatchesTracePalette(comp.palette_index, write.flags)) {
438 return PaletteSamplePoint{
439 .label = label, .x = sample_x, .y = sample_y};
440 }
441 }
442 }
443 }
444
445 if (!trace.empty()) {
446 const auto& write = trace.front();
447 return PaletteSamplePoint{
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};
453 }
454
455 return std::nullopt;
456}
457
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) {
461 if (trace.empty()) {
462 return "trace: writes=0";
463 }
464
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});
476 }
477
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);
488}
489
490std::string BuildDoorIssueSummary(const zelda3::Room::Door& door, size_t index,
491 std::string_view label) {
492 const auto [tile_x, tile_y] = door.GetTileCoords();
493 const auto [pixel_x, pixel_y] = door.GetPixelCoords();
494 const auto dims = door.GetDimensions();
495 const auto editor_dims = door.GetEditorDimensions();
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();
499
500 return absl::StrFormat(
501 "\n%s[%zu]:\n"
502 "- type=%s (0x%02X)\n"
503 "- direction=%s (%d)\n"
504 "- position=%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,
510 std::string(zelda3::GetDoorTypeName(door.type)).c_str(),
511 static_cast<int>(door.type),
512 std::string(zelda3::GetDoorDirectionName(door.direction)).c_str(),
513 static_cast<int>(door.direction), static_cast<int>(door.position), tile_x,
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,
517 encoded_b2, door.byte1, door.byte2);
518}
519
520std::string BuildRomDisplayName(const Rom& rom) {
521 if (!rom.short_name().empty()) {
522 return rom.short_name();
523 }
524 if (!rom.filename().empty()) {
525 return std::filesystem::path(rom.filename()).filename().string();
526 }
527 const std::string title = rom.title();
528 return title.empty() ? "in-memory ROM" : title;
529}
530
531std::string BuildReportSessionSummary(const Rom* rom,
532 const project::YazeProject* project) {
533 std::string summary;
534 if (rom && rom->is_loaded()) {
535 const std::string rom_path =
536 rom->filename().empty()
537 ? std::string("in-memory")
539 summary += absl::StrFormat(
540 "ROM: %s | CRC32:%s | Path:%s", BuildRomDisplayName(*rom).c_str(),
541 util::ComputeRomHash(rom->data(), rom->size()).c_str(),
542 rom_path.c_str());
543 } else {
544 summary += "ROM: unavailable";
545 }
546
547 if (project && project->project_opened()) {
548 summary += absl::StrFormat(
549 "\nProject: %s | Path:%s", project->GetDisplayName().c_str(),
551 .c_str());
552 }
553
554 return summary;
555}
556
558 const zelda3::PaletteDebugEvent& event) {
559 if (event.palette_id >= 0 && event.color_count > 0) {
560 return absl::StrFormat("- [%s] palette=%d colors=%d %s",
561 event.location.c_str(), event.palette_id,
562 event.color_count, event.message.c_str());
563 }
564 if (event.palette_id >= 0) {
565 return absl::StrFormat("- [%s] palette=%d %s", event.location.c_str(),
566 event.palette_id, event.message.c_str());
567 }
568 if (event.color_count > 0) {
569 return absl::StrFormat("- [%s] colors=%d %s", event.location.c_str(),
570 event.color_count, event.message.c_str());
571 }
572 return absl::StrFormat("- [%s] %s", event.location.c_str(),
573 event.message.c_str());
574}
575
577
578} // namespace
579
581 const zelda3::Room& room, int room_id) const {
582 const int resolved_palette_id = room.ResolveDungeonPaletteId();
583 const uint8_t entrance_blockset = room.render_entrance_blockset();
584 std::string entrance_context = "Entrance:none";
585 if (current_entrance_id_ >= 0 || entrance_blockset != 0xFF) {
586 const std::string entrance_id_text =
588 ? absl::StrFormat("%02X", current_entrance_id_)
589 : std::string("--");
590 const std::string entrance_blockset_text =
591 entrance_blockset != 0xFF
592 ? absl::StrFormat("%02X", static_cast<int>(entrance_blockset))
593 : std::string("--");
594 entrance_context =
595 absl::StrFormat("Entrance:%s EB:%s", entrance_id_text.c_str(),
596 entrance_blockset_text.c_str());
597 }
598
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",
602 room_id, zelda3::GetRoomLabel(room_id).c_str(), room.blockset(),
603 room.palette(), resolved_palette_id, room.layout_id(), room.spriteset(),
604 entrance_context, GetBlocksetGroupName(room.blockset()), room.floor1(),
605 room.floor2(), room.effect(), room.tag1(), room.tag2(),
606 BuildReportSessionSummary(rom_, project_).c_str(),
607 BuildEffectiveBlockListSummary(room).c_str());
608
609 // Connectivity diagnostics: enumerate doors/staircases/holewarp without
610 // reciprocity filtering so the report captures the room's actual link
611 // configuration. Staircase configuration issues (stale headers, missing
612 // destinations, extra placed objects beyond 4 slots) are listed
613 // separately so a reader can spot phantom or unreachable connections.
614 const auto link_diagnostics =
615 CollectDungeonConnectedRoomLinkDiagnostics(room_id, room, nullptr);
616 if (!link_diagnostics.links.empty() ||
617 !link_diagnostics.staircase_issues.empty()) {
618 summary += "\nConnectivity:";
619 for (const auto& link : link_diagnostics.links) {
620 summary += "\n ";
622 }
623 for (const auto& issue : link_diagnostics.staircase_issues) {
624 summary += "\n ";
626 }
627 }
628
629 return summary;
630}
631
633 int room_id) {
634 // Context-menu actions run inside BeginCanvas, before the normal room draw
635 // pass. Rebind at the diagnostic boundary too, so palette sampling remains
636 // correct even if the surrounding canvas order changes in the future.
637 if (rooms_ != nullptr && room_id >= 0 && room_id < zelda3::kNumberOfRooms &&
638 rooms_->GetIfMaterialized(room_id) == &room) {
640 }
641
642 std::string report = "Dungeon Draw Issue Report\n";
643 report += BuildRoomMetadataSummary(room, room_id);
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",
649 IsBG1Visible(room_id) ? "on" : "off",
650 IsBG2Visible(room_id) ? "on" : "off", show_grid_ ? "on" : "off",
651 show_object_bounds_ ? "on" : "off",
652 entity_visibility_.show_sprites ? "on" : "off",
653 entity_visibility_.show_pot_items ? "on" : "off",
654 show_custom_collision_overlay_ ? "on" : "off",
655 show_camera_quadrant_overlay_ ? "on" : "off");
656
657 const auto selected = object_interaction_.GetSelectedObjectIndices();
658 if (!selected.empty()) {
659 report += absl::StrFormat("\nSelected objects: %zu", selected.size());
660 if (selected.size() == 1) {
661 const auto& objects = room.GetTileObjects();
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()) {
670 report += "\n";
671 report += FormatObjectTileSample(*tiles, 16, "Descriptor tiles:");
672 }
673 if (obj.id_ == 0x00C4 || obj.id_ == 0x00DB) {
674 const bool uses_floor1 = obj.id_ == 0x00C4;
675 const uint8_t floor_pattern =
676 uses_floor1 ? room.floor1() : room.floor2();
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));
681 if (rom_ != nullptr && rom_->is_loaded()) {
682 const auto effective_tiles = gfx::DecodeDungeonFloorTilePattern(
685 if (effective_tiles.has_value()) {
686 report += "\n";
687 report += FormatObjectTileSample(*effective_tiles,
688 effective_tiles->size(),
689 "Effective floor-copy tiles:");
690 } else {
691 report +=
692 "\nEffective floor-copy tiles: unavailable "
693 "(ROM tables out of range)";
694 }
695 } else {
696 report +=
697 "\nEffective floor-copy tiles: unavailable (ROM not loaded)";
698 }
699 }
700
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,
713 center_x, center_y);
714 const auto trace_report =
715 BuildObjectTraceReport(rom_, room, index, current_palette_group_);
716 report += trace_report.summary;
717
718 auto& palette_debugger = zelda3::PaletteDebugger::Get();
719 auto append_sample = [&](std::string_view label, int sample_x,
720 int sample_y) {
721 const auto comp = palette_debugger.SamplePixelAt(sample_x, sample_y);
722 palette_debugger.AddComparison(comp);
723 const std::string label_text(label);
724 if (comp.palette_index < 0) {
725 report += absl::StrFormat(
726 "\nComposite sample %s (%d,%d): RGB=(%d,%d,%d), after color "
727 "math; no palette index",
728 label_text, sample_x, sample_y, comp.actual_r, comp.actual_g,
729 comp.actual_b);
730 return;
731 }
732 report += absl::StrFormat(
733 "\nPalette sample %s (%d,%d): idx=%d expected=(%d,%d,%d) "
734 "actual=(%d,%d,%d) match=%s",
735 label_text.c_str(), sample_x, sample_y,
736 static_cast<int>(comp.palette_index),
737 static_cast<int>(comp.expected_r),
738 static_cast<int>(comp.expected_g),
739 static_cast<int>(comp.expected_b),
740 static_cast<int>(comp.actual_r), static_cast<int>(comp.actual_g),
741 static_cast<int>(comp.actual_b), comp.matches ? "yes" : "no");
742 };
743
744 if (auto trace_sample = ChooseObjectTracePaletteSample(
745 trace_report.writes, palette_debugger)) {
746 append_sample(trace_sample->label, trace_sample->x, trace_sample->y);
747 } else {
748 append_sample("selection-origin", bounds_x, bounds_y);
749 if (center_x != bounds_x || center_y != bounds_y) {
750 append_sample("selection-center", center_x, center_y);
751 }
752 }
753 } else {
754 report += absl::StrFormat(
755 "\nSelected object index %zu is stale; room currently has %zu "
756 "object(s). Reselect the object and report again if this issue "
757 "still reproduces.",
758 index, objects.size());
759 }
760 }
761 }
762
764 const auto selected_entity = object_interaction_.GetSelectedEntity();
765 switch (selected_entity.type) {
766 case EntityType::Door: {
767 const auto& doors = room.GetDoors();
768 if (selected_entity.index < doors.size()) {
769 const auto& door = doors[selected_entity.index];
770 report += BuildDoorIssueSummary(door, selected_entity.index,
771 "Selected door");
772 }
773 break;
774 }
775 case EntityType::Sprite: {
776 const auto& sprites = room.GetSprites();
777 if (selected_entity.index < sprites.size()) {
778 const auto& sprite = sprites[selected_entity.index];
779 report += absl::StrFormat(
780 "\nSelected entity: sprite id=0x%02X name=%s pos=(%d,%d) "
781 "layer=%d",
782 sprite.id(), zelda3::ResolveSpriteName(sprite.id()), sprite.x(),
783 sprite.y(), sprite.layer());
784 }
785 break;
786 }
787 case EntityType::Item: {
788 const auto& items = room.GetPotItems();
789 if (selected_entity.index < items.size()) {
790 const auto& item = items[selected_entity.index];
791 report +=
792 absl::StrFormat("\nSelected entity: pot item=0x%02X pos=(%d,%d)",
793 item.item, item.GetPixelX(), item.GetPixelY());
794 }
795 break;
796 }
797 default:
798 break;
799 }
800 }
801
802 const auto& palette_events = zelda3::PaletteDebugger::Get().GetEvents();
803 if (!palette_events.empty()) {
804 report +=
805 absl::StrFormat("\nPalette debug events: %zu", palette_events.size());
806 const size_t preview_count = std::min<size_t>(palette_events.size(), 4);
807 const size_t start = palette_events.size() - preview_count;
808 for (size_t i = start; i < palette_events.size(); ++i) {
809 const auto& event = palette_events[i];
810 report += "\n";
811 report += FormatPaletteDebugEventForReport(event);
812 }
813 }
814
815 return report;
816}
817
819 const zelda3::Room& room, int room_id) const {
820 std::string report = "Dungeon Selection Issue Report\n";
821 report += BuildRoomMetadataSummary(room, room_id);
822 report +=
823 absl::StrFormat("\nInteraction mode: %s\nResolved palette group: 0x%02X",
826
827 const auto selected_objects = object_interaction_.GetSelectedObjectIndices();
828 if (!selected_objects.empty()) {
829 report +=
830 absl::StrFormat("\nSelected objects: %zu", selected_objects.size());
831 const auto& objects = room.GetTileObjects();
832 const size_t preview_count = std::min<size_t>(selected_objects.size(), 4);
833 for (size_t i = 0; i < preview_count; ++i) {
834 const size_t index = selected_objects[i];
835 if (index >= objects.size()) {
836 report += absl::StrFormat(
837 "\n- object[%zu] unavailable; room currently has %zu object(s) "
838 "(selection is stale)",
839 index, objects.size());
840 continue;
841 }
842 const auto& obj = objects[index];
843 report += absl::StrFormat(
844 "\n- object[%zu] id=0x%03X name=%s pos=(%d,%d) size=0x%02X %s", index,
845 obj.id_, GetObjectName(obj.id_).c_str(), obj.x_, obj.y_, obj.size_,
846 BuildSelectedObjectSemanticsSummary(obj).c_str());
847 auto [bounds_x, bounds_y, bounds_w, bounds_h] =
849 bounds_w = std::max(bounds_w, 8);
850 bounds_h = std::max(bounds_h, 8);
851 report += absl::StrFormat(
852 "\n geometry: selection_bounds_px=(%d,%d,%d,%d) "
853 "object_origin_px=(%d,%d)",
854 bounds_x, bounds_y, bounds_w, bounds_h, obj.x_ * 8, obj.y_ * 8);
855 if (auto tiles = obj.GetTiles(); tiles.ok() && !tiles->empty()) {
856 report += "\n ";
857 report += FormatObjectTileSample(*tiles, 10, "tiles:");
858 }
859 const auto trace_report =
860 BuildObjectTraceReport(rom_, room, index, current_palette_group_);
861 report += "\n ";
862 report += BuildObjectTraceBoundsSummary(trace_report.writes, bounds_x,
863 bounds_y, bounds_w, bounds_h);
864 }
865 if (selected_objects.size() > preview_count) {
866 report += absl::StrFormat("\n- ... %zu more object(s)",
867 selected_objects.size() - preview_count);
868 }
869 return report;
870 }
871
873 const auto selected_entity = object_interaction_.GetSelectedEntity();
874 switch (selected_entity.type) {
875 case EntityType::Door: {
876 const auto& doors = room.GetDoors();
877 if (selected_entity.index < doors.size()) {
878 const auto& door = doors[selected_entity.index];
879 report += BuildDoorIssueSummary(door, selected_entity.index,
880 "Selected door");
881 return report;
882 }
883 break;
884 }
885 case EntityType::Sprite: {
886 const auto& sprites = room.GetSprites();
887 if (selected_entity.index < sprites.size()) {
888 const auto& sprite = sprites[selected_entity.index];
889 report += absl::StrFormat(
890 "\nSelected sprite[%zu]:\n"
891 "- id=0x%02X\n"
892 "- name=%s\n"
893 "- pos=(%d,%d)\n"
894 "- layer=%d",
895 selected_entity.index, sprite.id(),
896 zelda3::ResolveSpriteName(sprite.id()), sprite.x(), sprite.y(),
897 sprite.layer());
898 return report;
899 }
900 break;
901 }
902 case EntityType::Item: {
903 const auto& items = room.GetPotItems();
904 if (selected_entity.index < items.size()) {
905 const auto& item = items[selected_entity.index];
906 report += absl::StrFormat(
907 "\nSelected pot item[%zu]:\n"
908 "- item=0x%02X\n"
909 "- pixel_pos=(%d,%d)",
910 selected_entity.index, item.item, item.GetPixelX(),
911 item.GetPixelY());
912 return report;
913 }
914 break;
915 }
916 default:
917 break;
918 }
919 }
920
921 report += "\nNo active object or entity selection.";
922 return report;
923}
924
928
929 auto paths_or = ResolveDungeonIssueReportPaths();
930 if (!paths_or.ok()) {
931 SetIssueReportPopupStatus(std::string(paths_or.status().message()), true);
932 return;
933 }
934
938 util::PlatformPaths::NormalizePathForDisplay(paths_or->screenshots_dir);
939}
940
942 bool is_error) {
943 issue_report_popup_status_message_ = std::move(message);
945}
946
950
956 return;
957 }
958
959 if (!ImGui::CollapsingHeader(kIssueReportSectionPaths)) {
960 return;
961 }
963 ImGui::TextWrapped(tr("Log target: %s"),
965 }
968 ImGui::TextWrapped(tr("Screenshot dir: %s"),
970 }
972 ImGui::TextWrapped(tr("Screenshot: %s"),
974 }
976 ImGui::TextWrapped(tr("Issue log: %s"),
978 }
979}
980
982 const bool idle = issue_report_popup_status_message_.empty();
983 const std::string_view message =
984 idle ? std::string_view(tr(kIssueReportStatusIdle))
985 : std::string_view(issue_report_popup_status_message_);
986 const size_t line_end = message.find_first_of("\r\n");
987 std::string visible_line(message.substr(0, line_end));
988 if (line_end != std::string_view::npos) {
989 visible_line += "...";
990 }
991 const ImVec4 color =
992 idle ? ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)
995 const ImVec2 start = ImGui::GetCursorScreenPos();
996 const ImVec2 size(std::max(ImGui::GetContentRegionAvail().x, 1.0f),
997 ImGui::GetTextLineHeight());
998 const ImVec2 end(start.x + size.x, start.y + size.y);
999 {
1000 gui::StyleColorGuard status_color(ImGuiCol_Text, color);
1001 ImGui::RenderTextEllipsis(ImGui::GetWindowDrawList(), start, end, end.x,
1002 visible_line.c_str(), nullptr, nullptr);
1003 }
1004 ImGui::Dummy(size);
1005 if (ImGui::IsItemHovered()) {
1006 ImGui::BeginTooltip();
1007 ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
1008 ImGui::TextUnformatted(message.data(), message.data() + message.size());
1009 ImGui::PopTextWrapPos();
1010 ImGui::EndTooltip();
1011 }
1012}
1013
1015 const std::string& title, const std::string& summary,
1016 const std::string& kind_label, const std::string& diagnostics, int room_id,
1017 int default_category_index) {
1019 issue_report_popup_kind_ = kind_label;
1020 issue_report_popup_diagnostics_ = diagnostics;
1023 std::clamp(default_category_index, 0,
1024 static_cast<int>(kDungeonIssueCategoryLabels.size()) - 1);
1028 SetIssueReportPopupStatus("", false);
1030 std::snprintf(issue_report_summary_, sizeof(issue_report_summary_), "%s",
1031 summary.c_str());
1032 issue_report_notes_[0] = '\0';
1033 return absl::OkStatus();
1034}
1035
1036void DungeonCanvasViewer::OpenIssueReportPopup(const std::string& title,
1037 const std::string& summary,
1038 const std::string& kind_label,
1039 const std::string& diagnostics,
1040 int room_id,
1041 int default_category_index) {
1042 (void)PrepareIssueReportPopup(title, summary, kind_label, diagnostics,
1043 room_id, default_category_index);
1045 const IssueReportWindowLayout window_layout = GetIssueReportWindowLayout();
1046 ImGui::SetNextWindowPos(window_layout.center, ImGuiCond_Appearing,
1047 ImVec2(0.5f, 0.5f));
1048 ImGui::SetNextWindowSize(window_layout.initial_size, ImGuiCond_Appearing);
1049 ImGui::SetNextWindowSizeConstraints(window_layout.min_size,
1050 window_layout.max_size);
1051
1052 if (ImGui::BeginPopupModal(issue_report_popup_id_.c_str(), nullptr,
1053 ImGuiWindowFlags_NoSavedSettings)) {
1054#ifdef YAZE_WITH_GRPC
1055 const char* capture_label = issue_report_popup_screenshot_path_.empty()
1056 ? kIssueReportCaptureButtonLabel
1057 : kIssueReportRecaptureButtonLabel;
1058 const std::array<const char*, 4> action_labels = {
1059 kIssueReportSaveButtonLabel, capture_label,
1060 kIssueReportCopyButtonLabel, kIssueReportCloseButtonLabel};
1061#else
1062 const std::array<const char*, 3> action_labels = {
1063 kIssueReportSaveButtonLabel, kIssueReportCopyButtonLabel,
1064 kIssueReportCloseButtonLabel};
1065#endif
1066 const float footer_width =
1067 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
1068 const int action_rows = CountWrappedIssueActionRows(
1069 std::span<const char* const>(action_labels), footer_width);
1070 const ImGuiStyle& style = ImGui::GetStyle();
1071 const float action_height =
1072 action_rows * ImGui::GetFrameHeight() +
1073 std::max(0, action_rows - 1) * style.ItemSpacing.y;
1074 const float footer_height = ImGui::GetTextLineHeightWithSpacing() +
1075 action_height + style.ItemSpacing.y * 2.0f +
1076 1.0f;
1077
1078 if (ImGui::BeginChild("##DungeonIssueReportBody",
1079 ImVec2(0.0f, -footer_height), false,
1080 ImGuiWindowFlags_None)) {
1081 if (!issue_report_popup_title_.empty()) {
1082 ImGui::TextUnformatted(issue_report_popup_title_.c_str());
1083 if (!issue_report_popup_kind_.empty()) {
1084 ImGui::TextDisabled("%s", issue_report_popup_kind_.c_str());
1085 }
1086 ImGui::Separator();
1087 }
1088
1089 ImGui::TextUnformatted(tr("Category"));
1090 ImGui::SetNextItemWidth(
1091 std::max(ImGui::GetContentRegionAvail().x, 1.0f));
1092 const char* category_preview =
1094 if (ImGui::BeginCombo("##DungeonIssueCategory", category_preview)) {
1095 for (size_t i = 0; i < kDungeonIssueCategoryLabels.size(); ++i) {
1096 const bool selected =
1097 issue_report_category_index_ == static_cast<int>(i);
1098 if (ImGui::Selectable(kDungeonIssueCategoryLabels[i], selected)) {
1099 issue_report_category_index_ = static_cast<int>(i);
1101 }
1102 if (selected) {
1103 ImGui::SetItemDefaultFocus();
1104 }
1105 }
1106 ImGui::EndCombo();
1107 }
1108
1109 ImGui::TextUnformatted(tr("Summary"));
1110 ImGui::SetNextItemWidth(
1111 std::max(ImGui::GetContentRegionAvail().x, 1.0f));
1112 if (ImGui::InputTextWithHint(
1113 "##DungeonIssueSummary", kIssueReportSummaryHint,
1116 }
1117
1118 ImGui::TextUnformatted(tr("Observed issue"));
1119 if (ImGui::InputTextMultiline("##DungeonIssueObserved",
1121 sizeof(issue_report_notes_),
1122 ImVec2(-1.0f, kIssueReportNotesHeight))) {
1124 }
1125
1127
1128 if (ImGui::CollapsingHeader(kIssueReportSectionDiagnostics)) {
1129 ImGui::InputTextMultiline(
1130 kIssueReportPopupIdDiagnostics,
1133 ImVec2(-1.0f, kIssueReportDiagnosticsHeight),
1134 ImGuiInputTextFlags_ReadOnly);
1135 if (ImGui::Button(ICON_MD_ASSIGNMENT " Copy Diagnostics")) {
1136 ImGui::SetClipboardText(issue_report_popup_diagnostics_.c_str());
1137 SetIssueReportPopupStatus(kIssueReportStatusCopiedDiagnostics,
1138 false);
1139 }
1140 }
1141 }
1142 ImGui::EndChild();
1143
1144 ImGui::Separator();
1146
1147 float used_action_width = 0.0f;
1148 PlaceNextIssueAction(kIssueReportSaveButtonLabel, footer_width,
1149 &used_action_width);
1150 if (gui::PrimaryButton(kIssueReportSaveButtonLabel)) {
1151 const auto status = EnsureIssueReportPersisted();
1152 if (status.ok()) {
1153 SetIssueReportPopupStatus(kIssueReportStatusSavedLog, false);
1154 } else {
1155 SetIssueReportPopupStatus(std::string(status.message()), true);
1156 }
1157 }
1158
1159#ifdef YAZE_WITH_GRPC
1160 PlaceNextIssueAction(capture_label, footer_width, &used_action_width);
1161 if (ImGui::Button(
1162 capture_label,
1163 ImVec2(GetIssueActionButtonWidth(capture_label), 0.0f))) {
1164 const auto status = CaptureIssueReportScreenshot();
1165 if (status.ok()) {
1167 SetIssueReportPopupStatus(kIssueReportStatusSavedScreenshot, false);
1168 } else {
1169 SetIssueReportPopupStatus(std::string(status.message()), true);
1170 }
1171 }
1172#endif
1173
1174 PlaceNextIssueAction(kIssueReportCopyButtonLabel, footer_width,
1175 &used_action_width);
1176 if (ImGui::Button(kIssueReportCopyButtonLabel)) {
1177 std::string report = BuildIssueReportClipboardText();
1178 ImGui::SetClipboardText(report.c_str());
1179 SetIssueReportPopupStatus(kIssueReportStatusCopiedReport, false);
1180 }
1181
1182 PlaceNextIssueAction(kIssueReportCloseButtonLabel, footer_width,
1183 &used_action_width);
1184 if (ImGui::Button(kIssueReportCloseButtonLabel)) {
1186 ImGui::CloseCurrentPopup();
1187 }
1188 ImGui::EndPopup();
1189 return;
1190 }
1191 });
1192}
1193
1195 std::string report;
1196 if (!issue_report_popup_kind_.empty()) {
1197 report += issue_report_popup_kind_ + "\n";
1198 }
1199 report += absl::StrFormat(
1201 if (issue_report_summary_[0] != '\0') {
1202 report += absl::StrFormat("Summary: %s\n", issue_report_summary_);
1203 }
1205 report += absl::StrFormat("Screenshot: %s\n",
1207 }
1208 if (issue_report_notes_[0] != '\0') {
1209 report += absl::StrFormat("\nObserved issue:\n%s\n", issue_report_notes_);
1210 }
1211 report += "\nDiagnostics:\n";
1213 return report;
1214}
1215
1217#ifdef YAZE_WITH_GRPC
1220 return absl::FailedPreconditionError(
1221 "Room canvas region is not available for screenshot capture");
1222 }
1223
1224 const std::string timestamp = BuildIssueTimestampSlug();
1225 auto screenshot_path_or = BuildDungeonIssueScreenshotPath(
1228 if (!screenshot_path_or.ok()) {
1229 return screenshot_path_or.status();
1230 }
1231
1232 yaze::test::CaptureRegion region;
1233 region.x = canvas_capture_x_;
1234 region.y = canvas_capture_y_;
1235 region.width = canvas_capture_width_;
1236 region.height = canvas_capture_height_;
1237
1238 auto artifact_or = yaze::test::CaptureHarnessScreenshotRegion(
1239 std::optional<yaze::test::CaptureRegion>(region),
1240 screenshot_path_or->string(),
1241 /*reveal_to_user=*/false);
1242 if (!artifact_or.ok()) {
1243 return artifact_or.status();
1244 }
1245
1247 util::PlatformPaths::NormalizePathForDisplay(artifact_or->file_path);
1248 return absl::OkStatus();
1249#else
1250 return absl::UnimplementedError(
1251 "Screenshot capture is unavailable without YAZE_WITH_GRPC");
1252#endif
1253}
1254
1257 entry.timestamp_display = BuildIssueTimestampDisplay();
1266
1267 auto log_path_or = AppendDungeonIssueLogEntry(entry);
1268 if (!log_path_or.ok()) {
1269 return log_path_or.status();
1270 }
1271
1274 return absl::OkStatus();
1275}
1276
1280 return absl::OkStatus();
1281 }
1282
1285 return absl::OkStatus();
1286}
1287
1288} // 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:49
auto filename() const
Definition rom.h:207
const auto & vector() const
Definition rom.h:205
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
auto short_name() const
Definition rom.h:209
bool is_loaded() const
Definition rom.h:187
auto title() const
Definition rom.h:199
std::string BuildDrawIssueReport(const zelda3::Room &room, int room_id)
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_
gfx::Bitmap * PrepareRoomCompositeBitmap(int room_id)
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
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.
Definition bitmap.h:70
void ClosePersistentPopup(const std::string &popup_id)
Definition canvas.cc:1004
void OpenPersistentPopup(const std::string &popup_id, std::function< void()> render_callback)
Definition canvas.cc:998
RAII guard for ImGui style colors.
Definition style_guard.h:27
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.
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 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
Definition room.h:1120
uint8_t blockset() const
Definition room.h:1015
const std::vector< Door > & GetDoors() const
Definition room.h:413
TagKey tag2() const
Definition room.h:988
uint8_t floor2() const
Definition room.h:1042
uint8_t palette() const
Definition room.h:1019
TagKey tag1() const
Definition room.h:987
uint8_t spriteset() const
Definition room.h:1018
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
auto & bg1_buffer()
Definition room.h:1145
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
DungeonState * GetDungeonState()
Definition room.h:1165
auto blocks() const
Definition room.h:1080
EffectKey effect() const
Definition room.h:986
uint8_t floor1() const
Definition room.h:1041
int ResolveDungeonPaletteId() const
Definition room.cc:921
uint8_t render_entrance_blockset() const
Definition room.h:1016
uint8_t layout_id() const
Definition room.h:1026
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
auto & bg2_buffer()
Definition room.h:1146
int id() const
Definition room.h:1012
#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)
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)
ObjectTraceReport BuildObjectTraceReport(Rom *rom, const zelda3::Room &room, size_t selected_index, const gfx::PaletteGroup &palette_group)
void PlaceNextIssueAction(const char *label, float available_width, float *used_width)
std::string FormatPaletteDebugEventForReport(const zelda3::PaletteDebugEvent &event)
std::string BuildReportSessionSummary(const Rom *rom, const project::YazeProject *project)
std::string FormatObjectTileSample(std::span< const gfx::TileInfo > tiles, size_t max_count, std::string_view prefix)
int CountWrappedIssueActionRows(std::span< const char *const > labels, float available_width)
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)
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).
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
std::string ComputeRomHash(const uint8_t *data, size_t size)
Definition rom_hash.cc:80
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)
constexpr int kRoomObjectTileAddress
Definition room_object.h:48
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
constexpr int kNumberOfRooms
const char * EffectiveBgLayerLabel(EffectiveBgLayer layer)
constexpr int kRoomObjectTileAddressFloor
Definition room_object.h:49
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:287
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Represents a group of palettes.
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
bool project_opened() const
Definition project.h:505
std::string GetDisplayName() const
Definition project.cc:2001
Represents a door in a dungeon room.
Definition room.h:339
std::tuple< int, int, int, int > GetEditorBounds() const
Get editor interaction bounds (x, y, width, height in pixels)
Definition room.h:373
std::tuple< int, int, int, int > GetBounds() const
Get bounding rectangle (x, y, width, height in pixels)
Definition room.h:368
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:344
DoorDimensions GetDimensions() const
Get door dimensions in tiles.
Definition room.h:358
DoorType type
Door type (determines appearance/behavior)
Definition room.h:341
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:389
DoorDirection direction
Which wall the door is on.
Definition room.h:342
DoorDimensions GetEditorDimensions() const
Get editor interaction dimensions in tiles.
Definition room.h:363
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:340
std::pair< int, int > GetPixelCoords() const
Get pixel coordinates for this door.
Definition room.h:353
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:345
std::pair< int, int > GetTileCoords() const
Get tile coordinates for this door.
Definition room.h:348