yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_coverage_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <chrono>
6#include <cstdio>
7#include <cstring>
8#include <fstream>
9#include <iterator>
10#include <optional>
11#include <string>
12#include <vector>
13
14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
16#include "absl/time/clock.h"
17#include "absl/time/time.h"
21#include "imgui/imgui.h"
22#include "rom/rom.h"
23#include "util/file_util.h"
24#include "util/i18n/tr.h"
25#include "util/platform_paths.h"
26#include "util/rom_hash.h"
31#include "zelda3/dungeon/room.h"
33
34namespace yaze {
35namespace editor {
36namespace {
37
38constexpr char kEvidenceSubdir[] = "dungeon_object_evidence";
39
55
56std::string SanitizeForFilename(const std::string& value) {
57 std::string out;
58 out.reserve(value.size());
59 for (char ch : value) {
60 const unsigned char uch = static_cast<unsigned char>(ch);
61 out.push_back(std::isalnum(uch) ? static_cast<char>(std::tolower(uch))
62 : '_');
63 }
64 return out.empty() ? std::string("unnamed") : out;
65}
66
67bool ContainsCaseInsensitive(const std::string& haystack, const char* needle) {
68 if (needle == nullptr || needle[0] == '\0') {
69 return true;
70 }
71 const std::string lower_needle = SanitizeForFilename(needle);
72 const std::string lower_haystack = SanitizeForFilename(haystack);
73 return lower_haystack.find(lower_needle) != std::string::npos;
74}
75
76// ObjectOccurrence::layer is the room object list, not a BG number.
77const char* ObjectListLabel(int list_index) {
78 switch (list_index) {
80 return "main list";
82 return "BG2 overlay";
84 return "BG1 overlay";
85 }
86 return "list ?";
87}
88
89// Continues the current line when `width` more pixels fit before
90// `right_edge` (screen X); otherwise the next item starts a new line. The
91// Workbench inspector is narrow, so rows of controls must wrap.
92void SameLineIfFits(float width, float right_edge) {
93 const float line_end =
94 ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + width;
95 if (line_end <= right_edge) {
96 ImGui::SameLine();
97 }
98}
99
101 return ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
102}
103
104float ButtonWidth(const char* label) {
105 return ImGui::CalcTextSize(label, nullptr, true).x +
106 ImGui::GetStyle().FramePadding.x * 2.0f;
107}
108
109float CheckboxWidth(const char* label) {
110 return ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x +
111 ImGui::CalcTextSize(label, nullptr, true).x;
112}
113
114std::string NowUtc() {
115 return absl::FormatTime("%Y-%m-%dT%H:%M:%SZ", absl::Now(),
116 absl::UTCTimeZone());
117}
118
119} // namespace
120
122 project_ = project;
123 loaded_context_.clear();
124}
125
127 rooms_ = rooms;
128 index_dirty_ = true;
129}
130
132 if (project_ != nullptr && !project_->name.empty()) {
133 return "project_" + SanitizeForFilename(project_->name);
134 }
135 if (!rom_sha1_.empty()) {
136 return "rom_" + rom_sha1_.substr(0, 12);
137 }
138 return std::string();
139}
140
143 custom_code_.clear();
144 rom_sha1_.clear();
145 auto& registry = zelda3::DrawRoutineRegistry::Get();
146 registry.Initialize();
147 review_order_ = OrderObjectsForReview(registry.GetMappedObjectIds());
148 index_dirty_ = false;
149
150 if (rooms_ == nullptr || rooms_->rom() == nullptr ||
151 !rooms_->rom()->is_loaded()) {
152 return;
153 }
154 const auto& rom_bytes = rooms_->rom()->vector();
155 rom_sha1_ = util::ComputeSha1Hex(rom_bytes.data(), rom_bytes.size());
156 const std::vector<core::ProtectedRegion> no_hooks;
159 project_ != nullptr && project_->hack_manifest.loaded()
161 : no_hooks);
162
163 // Opened rooms carry unsaved edits, so they win over the ROM copy; other
164 // rooms are parsed into a temporary, as the minecart audit does.
165 for (int room_id = 0; room_id < static_cast<int>(rooms_->size()); ++room_id) {
166 if (auto* room = rooms_->GetIfMaterialized(room_id)) {
167 room->EnsureObjectsLoaded();
168 usage_.AddRoom(room_id, room->GetTileObjects());
169 continue;
170 }
171 zelda3::Room room(room_id, rooms_->rom(), rooms_->game_data());
172 room.EnsureObjectsLoaded();
173 usage_.AddRoom(room_id, room.GetTileObjects());
174 }
175 // The ROM may have changed; recheck that the captures still belong to it.
176 if (!loaded_context_.empty()) {
179 }
180}
181
183 const std::string context = EvidenceContextName();
184 if (context.empty() || context == loaded_context_) {
185 return;
186 }
187 loaded_context_ = context;
190 if (!app_data.ok()) {
191 evidence_path_.clear();
192 status_message_ = std::string(app_data.status().message());
193 status_is_error_ = true;
194 return;
195 }
196 // The context name comes from the project name or the ROM hash. Both are
197 // sanitized already; pin that here so nothing can escape the folder.
199 *app_data / kEvidenceSubdir / (SafeEvidenceFileName(context) + ".json");
201 if (!loaded.ok()) {
202 // Keep the unreadable file untouched: saving would overwrite it.
203 status_message_ = absl::StrCat(loaded.status().message(),
204 ". Verdicts will not be saved until it "
205 "is fixed or moved.");
206 status_is_error_ = true;
207 evidence_path_.clear();
208 return;
209 }
210 evidence_ = *std::move(loaded);
211 status_message_.clear();
212 status_is_error_ = false;
215}
216
217void ObjectCoveragePanel::SetCaptureDir(const std::string& dir) {
218 if (!IsPlainCaptureDir(std::filesystem::path(dir))) {
219 status_message_ = absl::StrCat(dir, " must not contain \"..\"");
220 status_is_error_ = true;
221 return;
222 }
224 SaveEvidence();
227}
228
230 manifest_.reset();
231 manifest_error_.clear();
233 if (manifest_dir_.empty()) {
234 return;
235 }
236 auto manifest = LoadGameCaptureManifest(manifest_dir_);
237 if (!manifest.ok()) {
238 manifest_error_ = std::string(manifest.status().message());
239 return;
240 }
241 if (manifest->rom_sha1 != rom_sha1_) {
242 manifest_error_ = absl::StrFormat(
243 "These captures are for ROM %s; the open ROM is %s. Capture this ROM "
244 "or choose another folder.",
245 manifest->rom_sha1.substr(0, 12), rom_sha1_.substr(0, 12));
246 return;
247 }
248 manifest_ = *std::move(manifest);
249}
250
252 if (!manifest_) {
253 return;
254 }
256 auto_room_errors_.clear();
257 auto_queue_ = manifest_->captured_rooms;
258 auto_next_ = 0;
259 auto_running_ = !auto_queue_.empty();
260}
261
263 if (!auto_running_) {
264 return;
265 }
266 // Rendering a room takes tens of milliseconds, so spread the rooms over
267 // frames and keep the editor responsive.
268 const auto start = std::chrono::steady_clock::now();
269 constexpr auto kFrameBudget = std::chrono::milliseconds(12);
270 while (auto_next_ < auto_queue_.size()) {
272 if (std::chrono::steady_clock::now() - start > kFrameBudget) {
273 break;
274 }
275 }
276 if (auto_next_ >= auto_queue_.size()) {
277 auto_running_ = false;
278 }
279}
280
282 const auto path = GameCaptureRoomPath(manifest_dir_, room_id);
283 std::ifstream in(path, std::ios::binary);
284 const std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(in)),
285 std::istreambuf_iterator<char>());
286 auto game = zelda3::ParseGameRoomTilemaps(bytes);
287 if (!game.ok()) {
288 auto_room_errors_.push_back(absl::StrFormat(
289 "Room 0x%03X: %s", room_id, std::string(game.status().message())));
290 return;
291 }
292 // Compare the ROM as saved; captures come from the saved ROM too.
295 room.RenderRoomGraphics();
296 const auto yaze = zelda3::ComposeYazeRoomTilemaps(room);
297 const auto& objects = room.GetTileObjects();
298 std::vector<bool> hidden(objects.size());
299 for (size_t i = 0; i < objects.size(); ++i) {
300 hidden[i] = zelda3::GameHidesObjectOnRoomLoad(static_cast<int>(room.tag1()),
301 static_cast<int>(room.tag2()),
302 objects[i]);
303 }
304 const auto owners = zelda3::ComputeObjectTileOwners(rooms_->rom(), room_id,
305 objects, yaze, hidden);
306 const auto check =
307 zelda3::CompareRoomTilemaps(room_id, *game, yaze, owners, objects);
308 auto_results_.RecordRoom(check.differences.empty());
309 for (const auto& placement : check.placements) {
310 PlacementAutoResult result;
311 result.tiles_owned = placement.tiles_owned;
312 result.tiles_different = placement.tiles_different;
313 result.difference_bits = placement.difference_bits;
314 auto_results_.Record(room_id, placement.object_index, placement.object_id,
315 result);
316 }
317}
318
320 int applied = 0;
321 for (auto& [object_id, proposal] :
323 if (proposal.state != state) {
324 continue;
325 }
326 proposal.updated_utc = NowUtc();
327 evidence_.Set(object_id, std::move(proposal));
328 ++applied;
329 }
330 SaveEvidence();
331 if (status_message_.empty()) {
332 status_message_ = absl::StrFormat("Marked %d objects as \"%s\".", applied,
334 }
335}
336
338 if (evidence_path_.empty()) {
339 return;
340 }
341 const absl::Status status = evidence_.SaveToFile(evidence_path_);
342 if (!status.ok()) {
343 status_message_ = std::string(status.message());
344 status_is_error_ = true;
345 return;
346 }
347 status_message_.clear();
348 status_is_error_ = false;
349}
350
352 ObjectEvidence evidence;
353 if (const ObjectEvidence* existing = evidence_.Find(object_id)) {
354 evidence = *existing;
355 }
356 evidence.state = state;
357 auto room_it = last_room_for_object_.find(object_id);
358 if (room_it != last_room_for_object_.end()) {
359 evidence.room_id = room_it->second;
360 }
361 evidence.rom_sha1 = rom_sha1_;
362 evidence.updated_utc = NowUtc();
363 evidence_.Set(object_id, std::move(evidence));
364 SaveEvidence();
365}
366
367void ObjectCoveragePanel::FocusObject(int object_id, int room_id) {
368 selected_object_ = object_id;
369 if (room_id >= 0) {
370 last_room_for_object_[object_id] = room_id;
371 }
372 // Clear filters that could hide the row being focused.
373 filter_text_[0] = '\0';
374 state_filter_ = -1;
375 only_focus_ = false;
376 only_placed_ = false;
377 scroll_to_selected_ = true;
378}
379
381 const ObjectOccurrence& occurrence) {
382 last_room_for_object_[object_id] = occurrence.room_id;
383 if (navigate_) {
384 navigate_(occurrence.room_id, occurrence.object_index, object_id);
385 }
386}
387
389 const std::optional<int> next =
391 if (!next) {
392 status_message_ = tr("Every placed object has a verdict.");
393 status_is_error_ = false;
394 return;
395 }
396 selected_object_ = *next;
397 scroll_to_selected_ = true;
398 if (const auto* occurrences = usage_.Find(*next);
399 occurrences != nullptr && !occurrences->empty()) {
400 GoToOccurrence(*next, occurrences->front());
401 }
402}
403
404bool ObjectCoveragePanel::PassesFilters(int object_id) const {
405 if (only_placed_ && usage_.Find(object_id) == nullptr) {
406 return false;
407 }
408 if (only_focus_ && ReleaseFocusGroupFor(object_id) < 0) {
409 return false;
410 }
412 const auto* summary = auto_results_.Summary(object_id);
413 if (summary == nullptr || summary->placements_different == 0) {
414 return false;
415 }
416 }
417 if (state_filter_ >= 0 &&
418 static_cast<int>(evidence_.StateOf(object_id)) != state_filter_) {
419 return false;
420 }
421 if (filter_text_[0] != '\0') {
422 const std::string haystack = absl::StrCat(
423 FormatObjectId(object_id), " ", zelda3::GetObjectName(object_id), " ",
424 zelda3::GetSymbologyForObject(static_cast<int16_t>(object_id)).family);
425 if (!ContainsCaseInsensitive(haystack, filter_text_)) {
426 return false;
427 }
428 }
429 return true;
430}
431
432void ObjectCoveragePanel::Draw(bool* /*p_open*/) {
433 if (rooms_ == nullptr || rooms_->rom() == nullptr ||
434 !rooms_->rom()->is_loaded()) {
435 ImGui::TextDisabled("%s", tr("Load a ROM to check dungeon objects."));
436 return;
437 }
438 if (index_dirty_) {
439 RebuildIndex();
440 }
442
444 DrawSummary();
446 ImGui::Separator();
447 DrawFilters();
448
449 const float details_height = ImGui::GetContentRegionAvail().y * 0.45f;
450 DrawObjectTable(ImGui::GetContentRegionAvail().y - details_height);
451 ImGui::Separator();
452 DrawDetails();
453}
454
456 int placed = 0;
457 int counts[std::size(kAllObjectEvidenceStates)] = {};
458 for (int object_id : review_order_) {
459 if (usage_.Find(object_id) == nullptr) {
460 continue;
461 }
462 ++placed;
463 ++counts[static_cast<int>(evidence_.StateOf(object_id))];
464 }
465 const int checked =
466 placed - counts[static_cast<int>(ObjectEvidenceState::kUntriaged)];
467
468 ImGui::TextWrapped(tr("Checked %d of %d objects placed in this ROM (%d "
469 "rooms scanned)"),
470 checked, placed, usage_.rooms_scanned());
471 ImGui::ProgressBar(placed > 0 ? static_cast<float>(checked) / placed : 0.0f,
472 ImVec2(-1.0f, 0.0f));
473
474 const float right_edge = RowRightEdge();
475 bool first = true;
477 if (state == ObjectEvidenceState::kUntriaged) {
478 continue;
479 }
480 const std::string label =
481 absl::StrFormat("%s %d", ObjectEvidenceStateLabel(state),
482 counts[static_cast<int>(state)]);
483 if (!first) {
484 SameLineIfFits(ImGui::CalcTextSize(label.c_str()).x, right_edge);
485 }
486 first = false;
487 ImGui::TextColored(StateColor(state), "%s", label.c_str());
488 }
489
490 const std::string next_label =
491 absl::StrCat(ICON_MD_SKIP_NEXT " ", tr("Next object to check"));
492 const std::string rescan_label =
493 absl::StrCat(ICON_MD_REFRESH " ", tr("Rescan rooms"));
494 if (ImGui::Button(next_label.c_str())) {
496 }
497 if (ImGui::IsItemHovered()) {
498 ImGui::SetTooltip(
499 "%s", tr("Opens the next unchecked object in release-focus order, in "
500 "a room that uses it, with the object selected."));
501 }
502 SameLineIfFits(ButtonWidth(rescan_label.c_str()), right_edge);
503 if (ImGui::Button(rescan_label.c_str())) {
504 index_dirty_ = true;
505 }
506
507 if (!status_message_.empty()) {
508 gui::StyleColorGuard status_color(ImGuiCol_Text, status_is_error_
511 ImGui::TextWrapped("%s", status_message_.c_str());
512 }
513 if (!evidence_path_.empty() && ImGui::IsItemHovered()) {
514 ImGui::SetTooltip("%s", evidence_path_.string().c_str());
515 }
516}
517
518// Compares yaze's tilemaps with the captured game tilemaps, a few rooms per
519// frame, and offers to record the matching objects as verdicts.
521 if (!ImGui::CollapsingHeader(tr("Automatic check against the game"))) {
522 return;
523 }
524 ImGui::PushID("ObjectCoverageAuto");
525 const std::string& dir = evidence_.capture_dir();
526 ImGui::TextWrapped("%s %s", tr("Game captures:"),
527 dir.empty() ? tr("none chosen") : dir.c_str());
528 if (ImGui::Button(
529 absl::StrCat(ICON_MD_FOLDER_OPEN " ", tr("Choose capture folder"))
530 .c_str())) {
531 const std::string chosen = util::FileDialogWrapper::ShowOpenFolderDialog();
532 if (!chosen.empty()) {
533 SetCaptureDir(chosen);
534 }
535 }
536 if (ImGui::IsItemHovered()) {
537 ImGui::SetTooltip(
538 "%s",
539 tr("A folder written by scripts/agents/capture-game-room-tilemaps.py "
540 "from Mesen2-OOS running this same ROM. It holds each room's "
541 "tilemaps as the game built them."));
542 }
543
544 if (!manifest_error_.empty()) {
545 gui::StyleColorGuard error_color(ImGuiCol_Text, gui::GetErrorColor());
546 ImGui::TextWrapped("%s", manifest_error_.c_str());
547 } else if (manifest_) {
548 ImGui::TextWrapped(tr("%zu rooms captured from the game for this ROM."),
549 manifest_->captured_rooms.size());
550 if (auto_running_) {
551 const float fraction =
552 auto_queue_.empty()
553 ? 1.0f
554 : static_cast<float>(auto_next_) / auto_queue_.size();
555 ImGui::ProgressBar(
556 fraction, ImVec2(-1.0f, 0.0f),
557 absl::StrFormat("%zu / %zu rooms", auto_next_, auto_queue_.size())
558 .c_str());
559 } else if (ImGui::Button(absl::StrCat(ICON_MD_PLAY_ARROW " ",
560 tr("Run automatic check"))
561 .c_str())) {
563 }
564 }
565
567 ImGui::PopID();
568}
569
570// Summary of the last automatic check, its errors, and the buttons that turn
571// its results into verdicts.
574 int objects_matching = 0;
575 int objects_differing = 0;
576 for (const auto& [object_id, summary] : auto_results_.summaries()) {
577 (summary.placements_different == 0 ? objects_matching
578 : objects_differing)++;
579 }
580 ImGui::TextWrapped(
581 tr("Compared %d rooms (%d exact). Objects matching the game in every "
582 "placement: %d. Objects with differences: %d."),
584 objects_matching, objects_differing);
585 ImGui::TextDisabled(
586 "%s", tr("Compares tiles, flips, palette rows and priority. Colors and "
587 "graphics sheets are not compared."));
588 for (const std::string& error : auto_room_errors_) {
589 ImGui::TextColored(gui::GetErrorColor(), "%s", error.c_str());
590 }
591
592 int propose_verified = 0;
593 int propose_broken = 0;
594 for (const auto& [object_id, proposal] :
596 (proposal.state == ObjectEvidenceState::kVerified ? propose_verified
597 : propose_broken)++;
598 }
599 const float right_edge = RowRightEdge();
600 const std::string verified_label = absl::StrFormat(
601 "%s %s (%d)", ICON_MD_CHECK, tr("Mark matches"), propose_verified);
602 const std::string broken_label =
603 absl::StrFormat("%s %s (%d)", ICON_MD_CLOSE,
604 tr("Mark differences as Broken"), propose_broken);
605 ImGui::BeginDisabled(propose_verified == 0);
606 if (ImGui::Button(verified_label.c_str())) {
608 }
609 ImGui::EndDisabled();
610 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
611 ImGui::SetTooltip(
612 "%s", tr("Sets \"Matches game\" on unchecked objects whose every "
613 "checked placement matches. Objects you already judged are "
614 "left alone."));
615 }
616 SameLineIfFits(ButtonWidth(broken_label.c_str()), right_edge);
617 ImGui::BeginDisabled(propose_broken == 0);
618 if (ImGui::Button(broken_label.c_str())) {
620 }
621 ImGui::EndDisabled();
622 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
623 ImGui::SetTooltip(
624 "%s", tr("Sets \"Broken\" on unchecked objects with any differing "
625 "placement, with a note naming what differs."));
626 }
627 }
628}
629
631 const float right_edge = RowRightEdge();
632 const float available = ImGui::GetContentRegionAvail().x;
633 const float spacing = ImGui::GetStyle().ItemSpacing.x;
634 // Two fields share a line when there is room for both at a usable width.
635 const bool fields_share_line = available >= 300.0f + spacing;
636 const float search_width =
637 fields_share_line ? std::min(200.0f, available - 140.0f - spacing)
638 : available;
639 ImGui::SetNextItemWidth(search_width);
640 ImGui::InputTextWithHint("##ObjectCoverageFilter", tr("Search ID or name"),
641 filter_text_, sizeof(filter_text_));
642 if (fields_share_line) {
643 ImGui::SameLine();
644 }
645 ImGui::SetNextItemWidth(fields_share_line ? 140.0f : available);
646 const char* preview =
647 state_filter_ < 0 ? tr("Any status")
649 static_cast<ObjectEvidenceState>(state_filter_));
650 if (ImGui::BeginCombo("##ObjectCoverageState", preview)) {
651 if (ImGui::Selectable(tr("Any status"), state_filter_ < 0)) {
652 state_filter_ = -1;
653 }
655 const int value = static_cast<int>(state);
656 if (ImGui::Selectable(ObjectEvidenceStateLabel(state),
657 state_filter_ == value)) {
658 state_filter_ = value;
659 }
660 }
661 ImGui::EndCombo();
662 }
663 SameLineIfFits(CheckboxWidth(tr("Placed only")), right_edge);
664 ImGui::Checkbox(tr("Placed only"), &only_placed_);
665 SameLineIfFits(CheckboxWidth(tr("Release focus only")), right_edge);
666 ImGui::Checkbox(tr("Release focus only"), &only_focus_);
667 if (auto_results_.rooms_compared() > 0) {
668 SameLineIfFits(CheckboxWidth(tr("Differs from game")), right_edge);
669 ImGui::Checkbox(tr("Differs from game"), &only_auto_differences_);
670 }
671}
672
674 std::vector<int> rows;
675 rows.reserve(review_order_.size());
676 for (int object_id : review_order_) {
677 if (PassesFilters(object_id)) {
678 rows.push_back(object_id);
679 }
680 }
681
682 constexpr ImGuiTableFlags kFlags =
683 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
684 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable |
685 ImGuiTableFlags_SizingStretchProp;
686 // The Workbench inspector is narrow; there the details pane shows the
687 // focus family instead of a column.
688 const bool show_focus_column = ImGui::GetContentRegionAvail().x >= 420.0f;
689 const bool show_auto_column = auto_results_.rooms_compared() > 0;
690 const int column_count =
691 4 + (show_focus_column ? 1 : 0) + (show_auto_column ? 1 : 0);
692 if (!ImGui::BeginTable("##ObjectCoverageTable", column_count, kFlags,
693 ImVec2(0.0f, std::max(height, 80.0f)))) {
694 return;
695 }
696 ImGui::TableSetupScrollFreeze(0, 1);
697 ImGui::TableSetupColumn(tr("ID"), ImGuiTableColumnFlags_WidthFixed, 48.0f);
698 ImGui::TableSetupColumn(tr("Name"), ImGuiTableColumnFlags_WidthStretch, 3.0f);
699 if (show_focus_column) {
700 ImGui::TableSetupColumn(tr("Focus"), ImGuiTableColumnFlags_WidthStretch,
701 1.5f);
702 }
703 ImGui::TableSetupColumn(tr("Rooms"), ImGuiTableColumnFlags_WidthFixed, 44.0f);
704 if (show_auto_column) {
705 ImGui::TableSetupColumn(tr("Game"), ImGuiTableColumnFlags_WidthFixed,
706 56.0f);
707 }
708 ImGui::TableSetupColumn(tr("Status"), ImGuiTableColumnFlags_WidthStretch,
709 1.5f);
710 ImGui::TableHeadersRow();
711
712 const auto& groups = ReleaseFocusGroups();
713 ImGuiListClipper clipper;
714 clipper.Begin(static_cast<int>(rows.size()));
715 int scroll_row = -1;
717 auto it = std::find(rows.begin(), rows.end(), *selected_object_);
718 if (it != rows.end()) {
719 scroll_row = static_cast<int>(it - rows.begin());
720 clipper.IncludeItemByIndex(scroll_row);
721 }
722 scroll_to_selected_ = false;
723 }
724 while (clipper.Step()) {
725 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
726 const int object_id = rows[static_cast<size_t>(row)];
727 ImGui::TableNextRow();
728 ImGui::PushID(object_id);
729
730 ImGui::TableSetColumnIndex(0);
731 const bool selected = selected_object_ == object_id;
732 if (ImGui::Selectable(FormatObjectId(object_id).c_str(), selected,
733 ImGuiSelectableFlags_SpanAllColumns)) {
734 selected_object_ = object_id;
735 }
736 if (row == scroll_row) {
737 ImGui::SetScrollHereY(0.3f);
738 }
739 ImGui::TableSetColumnIndex(1);
740 ImGui::TextUnformatted(zelda3::GetObjectName(object_id).c_str());
741 if (custom_code_.contains(object_id)) {
742 ImGui::SameLine();
743 ImGui::TextColored(gui::GetWarningColor(), ICON_MD_CODE);
744 if (ImGui::IsItemHovered()) {
745 ImGui::SetTooltip("%s",
746 tr("Your ROM changes this object's draw code"));
747 }
748 }
749 int column = 2;
750 if (show_focus_column) {
751 ImGui::TableSetColumnIndex(column++);
752 const int group = ReleaseFocusGroupFor(object_id);
753 if (group >= 0) {
754 ImGui::TextUnformatted(groups[static_cast<size_t>(group)].name);
755 }
756 }
757 ImGui::TableSetColumnIndex(column++);
758 const int room_count = usage_.RoomCountFor(object_id);
759 if (room_count > 0) {
760 ImGui::Text("%d", room_count);
761 } else {
762 ImGui::TextDisabled("-");
763 }
764 if (show_auto_column) {
765 ImGui::TableSetColumnIndex(column++);
766 if (const auto* summary = auto_results_.Summary(object_id)) {
767 if (summary->placements_different == 0) {
768 ImGui::TextColored(gui::GetSuccessColor(), "%s %d", ICON_MD_CHECK,
769 summary->placements_checked);
770 } else {
771 ImGui::TextColored(gui::GetErrorColor(), "%s %d/%d", ICON_MD_CLOSE,
772 summary->placements_different,
773 summary->placements_checked);
774 }
775 if (ImGui::IsItemHovered()) {
776 ImGui::SetTooltip(
777 "%s",
778 summary->placements_different == 0
779 ? tr("Every checked placement matches the game's tilemap.")
780 : absl::StrFormat(
781 "%d of %d placements differ from the game: %s",
782 summary->placements_different,
783 summary->placements_checked,
785 summary->difference_bits))
786 .c_str());
787 }
788 }
789 }
790 ImGui::TableSetColumnIndex(column);
791 const ObjectEvidenceState state = evidence_.StateOf(object_id);
792 if (selected) {
793 // State colors are unreadable on the selection highlight.
794 ImGui::TextUnformatted(ObjectEvidenceStateLabel(state));
795 } else {
796 ImGui::TextColored(StateColor(state), "%s",
798 }
799 ImGui::PopID();
800 }
801 }
802 ImGui::EndTable();
803}
804
805// Shows the selected object's routine, check hint, rooms and verdict editor.
807 if (!ImGui::BeginChild("##ObjectCoverageDetails", ImVec2(0, 0), false)) {
808 ImGui::EndChild();
809 return;
810 }
811 if (!selected_object_) {
812 ImGui::TextWrapped(
813 "%s",
814 tr("Press \"Next object to check\", or pick an object above. For each "
815 "object: open a room that uses it, compare it with the same room "
816 "in the game, then set its status."));
817 ImGui::EndChild();
818 return;
819 }
820
821 const int object_id = *selected_object_;
822 const auto symbology =
823 zelda3::GetSymbologyForObject(static_cast<int16_t>(object_id));
824 const auto& registry = zelda3::DrawRoutineRegistry::Get();
825 const auto* routine = registry.GetRoutineInfo(
826 registry.GetRoutineIdForObject(static_cast<int16_t>(object_id)));
827
828 ImGui::Text("%s %s", FormatObjectId(object_id).c_str(),
829 zelda3::GetObjectName(object_id).c_str());
830 {
831 gui::StyleColorGuard routine_color(ImGuiCol_Text, gui::GetDisabledColor());
832 ImGui::TextWrapped("%s: %s | %s", tr("Routine"),
833 routine != nullptr ? routine->name.c_str() : "-",
834 symbology.family.c_str());
835 }
836
837 if (auto it = custom_code_.find(object_id); it != custom_code_.end()) {
838 const auto& custom = it->second;
839 const std::string where =
840 custom.module.empty()
841 ? absl::StrFormat("$%06X", custom.address)
842 : absl::StrFormat("$%06X (%s)", custom.address, custom.module);
843 gui::StyleColorGuard warning_color(ImGuiCol_Text, gui::GetWarningColor());
844 ImGui::TextWrapped(
845 custom.replaced
846 ? tr(ICON_MD_CODE " Your ROM replaces this object's draw routine "
847 "with its own code at %s. yaze may not draw it "
848 "the way the game does; a mismatch here is not "
849 "necessarily a yaze bug.")
850 : tr(ICON_MD_CODE " Your ROM patches this object's draw routine "
851 "at %s. A mismatch with the game may come from "
852 "that patch, not from yaze."),
853 where.c_str());
854 }
855
856 const int group = ReleaseFocusGroupFor(object_id);
857 if (group >= 0) {
858 const auto& focus = ReleaseFocusGroups()[static_cast<size_t>(group)];
859 ImGui::TextColored(gui::GetInfoColor(), "%s:", focus.name);
860 ImGui::SameLine();
861 ImGui::TextWrapped("%s", focus.what_to_check);
862 }
863
864 if (const auto* summary = auto_results_.Summary(object_id)) {
865 std::optional<gui::StyleColorGuard> comparison_color;
866 if (summary->placements_different == 0) {
867 comparison_color.emplace(ImGuiCol_Text, gui::GetSuccessColor());
868 ImGui::TextWrapped(
869 tr(ICON_MD_CHECK " Matches the game in all %d checked placements"),
870 summary->placements_checked);
871 } else {
872 comparison_color.emplace(ImGuiCol_Text, gui::GetErrorColor());
873 const std::string what =
874 zelda3::DescribeTileWordDifference(summary->difference_bits);
875 ImGui::TextWrapped(
876 tr(ICON_MD_CLOSE " %d of %d checked placements differ from the "
877 "game: %s"),
878 summary->placements_different, summary->placements_checked,
879 what.c_str());
880 }
881 }
882
883 DrawVerdictEditor(object_id);
884 DrawObjectOccurrences(object_id);
885 ImGui::EndChild();
886}
887
888// Verdict buttons, the note field, and when the verdict was last set.
890 // Verdict buttons, current one highlighted.
891 const ObjectEvidenceState current = evidence_.StateOf(object_id);
892 const float verdict_right_edge = RowRightEdge();
893 bool first_verdict = true;
895 const bool is_current = state == current;
896 if (!first_verdict) {
897 SameLineIfFits(ButtonWidth(ObjectEvidenceStateLabel(state)),
898 verdict_right_edge);
899 }
900 first_verdict = false;
901 std::optional<gui::StyleColorGuard> verdict_color;
902 if (is_current) {
903 verdict_color.emplace(ImGuiCol_Button, StateColor(state));
904 }
905 if (ImGui::Button(ObjectEvidenceStateLabel(state))) {
906 SetVerdict(object_id, state);
907 }
908 }
909
910 // Note, saved when the field loses focus.
911 if (note_buffer_object_ != object_id) {
912 const ObjectEvidence* evidence = evidence_.Find(object_id);
913 std::snprintf(note_buffer_, sizeof(note_buffer_), "%s",
914 evidence ? evidence->note.c_str() : "");
915 note_buffer_object_ = object_id;
916 }
917 ImGui::SetNextItemWidth(-1.0f);
918 ImGui::InputTextWithHint("##ObjectCoverageNote",
919 tr("Note: what differs, which room, which size"),
920 note_buffer_, sizeof(note_buffer_));
921 if (ImGui::IsItemDeactivatedAfterEdit()) {
922 ObjectEvidence evidence;
923 if (const ObjectEvidence* existing = evidence_.Find(object_id)) {
924 evidence = *existing;
925 }
926 evidence.note = note_buffer_;
927 evidence.updated_utc = NowUtc();
928 evidence_.Set(object_id, std::move(evidence));
929 SaveEvidence();
930 }
931 if (const ObjectEvidence* evidence = evidence_.Find(object_id);
932 evidence != nullptr && evidence->room_id >= 0) {
933 ImGui::TextDisabled(tr("Last verdict in room 0x%03X at %s"),
934 evidence->room_id, evidence->updated_utc.c_str());
935 }
936}
937
938// Lists the rooms that place this object, each one a link that opens it.
940 const auto* occurrences = usage_.Find(object_id);
941 if (occurrences == nullptr) {
942 ImGui::TextDisabled(
943 "%s", tr("Not placed in any room of this ROM. Place it in a scratch "
944 "room to check it."));
945 return;
946 }
947 ImGui::Text(tr("Placed %zu times in %d rooms:"), occurrences->size(),
948 usage_.RoomCountFor(object_id));
949 for (size_t i = 0; i < occurrences->size(); ++i) {
950 const ObjectOccurrence& occurrence = (*occurrences)[i];
951 ImGui::PushID(static_cast<int>(i));
952 if (ImGui::SmallButton(tr("Go"))) {
953 GoToOccurrence(object_id, occurrence);
954 }
955 ImGui::SameLine();
956 const std::string label =
958 ImGui::Text("0x%03X %s (%d,%d) size %d, %s", occurrence.room_id,
959 label.c_str(), occurrence.x, occurrence.y, occurrence.size,
960 ObjectListLabel(occurrence.layer));
961 const PlacementAutoResult* result =
962 auto_results_.Find(occurrence.room_id, occurrence.object_index);
963 if (result != nullptr && result->object_id == object_id &&
964 result->tiles_owned > 0) {
965 ImGui::SameLine();
966 if (result->tiles_different == 0) {
967 ImGui::TextColored(gui::GetSuccessColor(), "%s", ICON_MD_CHECK);
968 } else {
969 ImGui::TextColored(
970 gui::GetErrorColor(), "%s %d/%d tiles: %s", ICON_MD_CLOSE,
971 result->tiles_different, result->tiles_owned,
973 .c_str());
974 }
975 }
976 ImGui::PopID();
977 }
978}
979
980} // namespace editor
981} // namespace yaze
const auto & vector() const
Definition rom.h:205
bool is_loaded() const
Definition rom.h:187
const std::vector< ProtectedRegion > & protected_regions() const
bool loaded() const
Check if the manifest has been loaded.
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::GameData * game_data() const
void Record(int room_id, size_t object_index, int object_id, const PlacementAutoResult &result)
const PlacementAutoResult * Find(int room_id, size_t object_index) const
const std::map< int, ObjectSummary > & summaries() const
const ObjectSummary * Summary(int object_id) const
std::optional< GameCaptureManifest > manifest_
void SetRooms(DungeonRoomStore *rooms)
void FocusObject(int object_id, int room_id)
void SetVerdict(int object_id, ObjectEvidenceState state)
void Draw(bool *p_open) override
Draw the panel content.
std::map< int, ObjectCustomDrawCode > custom_code_
void ApplyAutomaticVerdicts(ObjectEvidenceState state)
void GoToOccurrence(int object_id, const ObjectOccurrence &occurrence)
void SetCaptureDir(const std::string &dir)
std::vector< std::string > auto_room_errors_
void SetProject(project::YazeProject *project)
const std::string & capture_dir() const
const ObjectEvidence * Find(int object_id) const
absl::Status SaveToFile(const std::filesystem::path &path) const
ObjectEvidenceState StateOf(int object_id) const
static absl::StatusOr< ObjectEvidenceStore > LoadFromFile(const std::filesystem::path &path)
void Set(int object_id, ObjectEvidence evidence)
int RoomCountFor(int object_id) const
const std::vector< ObjectOccurrence > * Find(int object_id) const
void AddRoom(int room_id, const std::vector< zelda3::RoomObject > &objects)
RAII guard for ImGui style colors.
Definition style_guard.h:27
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
static absl::StatusOr< std::filesystem::path > GetAppDataDirectory()
Get the user-specific application data directory for YAZE.
static DrawRoutineRegistry & Get()
TagKey tag2() const
Definition room.h:988
void RenderRoomGraphics()
Definition room.cc:1254
TagKey tag1() const
Definition room.h:987
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void EnsureObjectsLoaded()
Definition room.cc:1026
void SetGameData(GameData *data)
Definition room.h:1088
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_SKIP_NEXT
Definition icons.h:1773
#define ICON_MD_CLOSE
Definition icons.h:418
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
bool ContainsCaseInsensitive(std::string_view haystack, std::string_view needle)
int ReleaseFocusGroupFor(int object_id)
absl::StatusOr< GameCaptureManifest > LoadGameCaptureManifest(const std::filesystem::path &dir)
const char * ObjectEvidenceStateLabel(ObjectEvidenceState state)
std::filesystem::path GameCaptureRoomPath(const std::filesystem::path &dir, int room_id)
bool IsPlainCaptureDir(const std::filesystem::path &dir)
std::string FormatObjectId(int object_id)
const std::vector< ReleaseFocusGroup > & ReleaseFocusGroups()
std::map< int, ObjectEvidence > ProposeAutomaticVerdicts(const ObjectAutoCheckResults &results, const ObjectEvidenceStore &evidence, const std::string &rom_sha1)
std::map< int, ObjectCustomDrawCode > FindObjectsWithCustomDrawCode(const std::vector< zelda3::ObjectDrawCode > &draw_code, const std::vector< core::ProtectedRegion > &hooks)
std::vector< int > OrderObjectsForReview(const std::vector< int16_t > &object_ids)
std::string SafeEvidenceFileName(std::string_view name)
std::optional< int > NextObjectToCheck(const std::vector< int > &review_order, const ObjectEvidenceStore &evidence, const ObjectUsageIndex &usage, std::optional< int > after_object_id)
constexpr ObjectEvidenceState kAllObjectEvidenceStates[]
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
ImVec4 GetDisabledColor()
Definition ui_helpers.cc:74
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
ImVec4 GetInfoColor()
Definition ui_helpers.cc:64
std::string ComputeSha1Hex(const uint8_t *data, size_t size)
Compute SHA-1 hash of data, return lowercase hex string (40 chars).
Definition rom_hash.cc:234
RoomTilemaps ComposeYazeRoomTilemaps(const Room &room)
std::string DescribeTileWordDifference(uint16_t xor_bits)
absl::StatusOr< RoomTilemaps > ParseGameRoomTilemaps(const std::vector< uint8_t > &bytes)
DrawRoutineSymbology GetSymbologyForObject(int16_t object_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
std::string GetObjectName(int object_id)
TileOwners ComputeObjectTileOwners(Rom *rom, int room_id, const std::vector< RoomObject > &objects, const RoomTilemaps &yaze, const std::vector< bool > &hidden)
RoomTilemapCheck CompareRoomTilemaps(int room_id, const RoomTilemaps &game, const RoomTilemaps &yaze, const TileOwners &owners, const std::vector< RoomObject > &objects)
std::vector< ObjectDrawCode > ReadObjectDrawCode(const Rom &rom)
bool GameHidesObjectOnRoomLoad(int tag1, int tag2, const RoomObject &object)
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
core::HackManifest hack_manifest
Definition project.h:360