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