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"
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))
65 return out.empty() ? std::string(
"unnamed") : out;
69 if (needle ==
nullptr || needle[0] ==
'\0') {
74 return lower_haystack.find(lower_needle) != std::string::npos;
94 const float line_end =
95 ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + width;
96 if (line_end <= right_edge) {
102 return ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
106 return ImGui::CalcTextSize(label,
nullptr,
true).x +
107 ImGui::GetStyle().FramePadding.x * 2.0f;
111 return ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x +
112 ImGui::CalcTextSize(label,
nullptr,
true).x;
116 return absl::FormatTime(
"%Y-%m-%dT%H:%M:%SZ", absl::Now(),
117 absl::UTCTimeZone());
134 return "project_" + SanitizeForFilename(
project_->
name);
139 return std::string();
147 registry.Initialize();
157 const std::vector<core::ProtectedRegion> no_hooks;
166 for (
int room_id = 0; room_id < static_cast<int>(
rooms_->
size()); ++room_id) {
168 room->EnsureObjectsLoaded();
191 if (!app_data.ok()) {
205 ". Verdicts will not be saved until it "
206 "is fixed or moved.");
238 if (!manifest.ok()) {
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));
269 const auto start = std::chrono::steady_clock::now();
270 constexpr auto kFrameBudget = std::chrono::milliseconds(12);
273 if (std::chrono::steady_clock::now() - start > kFrameBudget) {
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>());
290 "Room 0x%03X: %s", room_id, std::string(game.status().message())));
299 std::vector<bool> hidden(objects.size());
300 for (
size_t i = 0; i < objects.size(); ++i) {
302 static_cast<int>(room.
tag2()),
306 objects,
yaze, hidden);
310 for (
const auto& placement : check.placements) {
322 for (
auto& [object_id, proposal] :
324 if (proposal.state != state) {
327 proposal.updated_utc = NowUtc();
333 status_message_ = absl::StrFormat(
"Marked %d objects as \"%s\".", applied,
355 evidence = *existing;
357 evidence.
state = state;
360 evidence.
room_id = room_it->second;
390 const std::optional<int> next =
399 if (
const auto* occurrences =
usage_.
Find(*next);
400 occurrences !=
nullptr && !occurrences->empty()) {
414 if (summary ==
nullptr || summary->placements_different == 0) {
423 const std::string haystack = absl::StrCat(
436 ImGui::TextDisabled(
"%s", tr(
"Load a ROM to check dungeon objects."));
450 const float details_height = ImGui::GetContentRegionAvail().y * 0.45f;
469 ImGui::TextWrapped(tr(
"Checked %d of %d objects placed in this ROM (%d "
472 ImGui::ProgressBar(placed > 0 ?
static_cast<float>(checked) / placed : 0.0f,
473 ImVec2(-1.0f, 0.0f));
475 const float right_edge = RowRightEdge();
481 const std::string label =
483 counts[
static_cast<int>(state)]);
485 SameLineIfFits(ImGui::CalcTextSize(label.c_str()).x, right_edge);
488 ImGui::TextColored(StateColor(state),
"%s", label.c_str());
491 const std::string next_label =
493 const std::string rescan_label =
495 if (ImGui::Button(next_label.c_str())) {
498 if (ImGui::IsItemHovered()) {
500 "%s", tr(
"Opens the next unchecked object in release-focus order, in "
501 "a room that uses it, with the object selected."));
503 SameLineIfFits(ButtonWidth(rescan_label.c_str()), right_edge);
504 if (ImGui::Button(rescan_label.c_str())) {
522 if (!ImGui::CollapsingHeader(tr(
"Automatic check against the game"))) {
525 ImGui::PushID(
"ObjectCoverageAuto");
527 ImGui::TextWrapped(
"%s %s", tr(
"Game captures:"),
528 dir.empty() ? tr(
"none chosen") : dir.c_str());
533 if (!chosen.empty()) {
537 if (ImGui::IsItemHovered()) {
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."));
549 ImGui::TextWrapped(tr(
"%zu rooms captured from the game for this ROM."),
552 const float fraction =
557 fraction, ImVec2(-1.0f, 0.0f),
561 tr(
"Run automatic check"))
575 int objects_matching = 0;
576 int objects_differing = 0;
578 (summary.placements_different == 0 ? objects_matching
579 : objects_differing)++;
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);
587 "%s", tr(
"Compares tiles, flips, palette rows and priority. Colors and "
588 "graphics sheets are not compared."));
593 int propose_verified = 0;
594 int propose_broken = 0;
595 for (
const auto& [object_id, proposal] :
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 =
605 tr(
"Mark differences as Broken"), propose_broken);
606 ImGui::BeginDisabled(propose_verified == 0);
607 if (ImGui::Button(verified_label.c_str())) {
610 ImGui::EndDisabled();
611 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
613 "%s", tr(
"Sets \"Matches game\" on unchecked objects whose every "
614 "checked placement matches. Objects you already judged are "
617 SameLineIfFits(ButtonWidth(broken_label.c_str()), right_edge);
618 ImGui::BeginDisabled(propose_broken == 0);
619 if (ImGui::Button(broken_label.c_str())) {
622 ImGui::EndDisabled();
623 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
625 "%s", tr(
"Sets \"Broken\" on unchecked objects with any differing "
626 "placement, with a note naming what differs."));
632 const float right_edge = RowRightEdge();
633 const float available = ImGui::GetContentRegionAvail().x;
634 const float spacing = ImGui::GetStyle().ItemSpacing.x;
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)
640 ImGui::SetNextItemWidth(search_width);
641 ImGui::InputTextWithHint(
"##ObjectCoverageFilter", tr(
"Search ID or name"),
643 if (fields_share_line) {
646 ImGui::SetNextItemWidth(fields_share_line ? 140.0f : available);
647 const char* preview =
651 if (ImGui::BeginCombo(
"##ObjectCoverageState", preview)) {
652 if (ImGui::Selectable(tr(
"Any status"),
state_filter_ < 0)) {
656 const int value =
static_cast<int>(state);
664 SameLineIfFits(CheckboxWidth(tr(
"Placed only")), right_edge);
666 SameLineIfFits(CheckboxWidth(tr(
"Release focus only")), right_edge);
667 ImGui::Checkbox(tr(
"Release focus only"), &
only_focus_);
669 SameLineIfFits(CheckboxWidth(tr(
"Differs from game")), right_edge);
675 std::vector<int> rows;
679 rows.push_back(object_id);
683 constexpr ImGuiTableFlags kFlags =
684 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
685 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable |
686 ImGuiTableFlags_SizingStretchProp;
689 const bool show_focus_column = ImGui::GetContentRegionAvail().x >= 420.0f;
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)))) {
697 ImGui::TableSetupScrollFreeze(0, 1);
701 ImGuiTableColumnFlags_WidthFixed, 48.0f);
703 ImGuiTableColumnFlags_WidthStretch, 3.0f);
704 if (show_focus_column) {
706 ImGuiTableColumnFlags_WidthStretch, 1.5f);
709 ImGuiTableColumnFlags_WidthFixed, 44.0f);
710 if (show_auto_column) {
712 ImGuiTableColumnFlags_WidthFixed, 56.0f);
715 ImGuiTableColumnFlags_WidthStretch, 1.5f);
716 ImGui::TableHeadersRow();
719 ImGuiListClipper clipper;
720 clipper.Begin(
static_cast<int>(rows.size()));
724 if (it != rows.end()) {
725 scroll_row =
static_cast<int>(it - rows.begin());
726 clipper.IncludeItemByIndex(scroll_row);
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);
736 ImGui::TableSetColumnIndex(0);
738 if (ImGui::Selectable(
FormatObjectId(object_id).c_str(), selected,
739 ImGuiSelectableFlags_SpanAllColumns)) {
742 if (row == scroll_row) {
743 ImGui::SetScrollHereY(0.3f);
745 ImGui::TableSetColumnIndex(1);
750 if (ImGui::IsItemHovered()) {
751 ImGui::SetTooltip(
"%s",
752 tr(
"Your ROM changes this object's draw code"));
756 if (show_focus_column) {
757 ImGui::TableSetColumnIndex(column++);
760 ImGui::TextUnformatted(groups[
static_cast<size_t>(group)].name);
763 ImGui::TableSetColumnIndex(column++);
765 if (room_count > 0) {
766 ImGui::Text(
"%d", room_count);
768 ImGui::TextDisabled(
"-");
770 if (show_auto_column) {
771 ImGui::TableSetColumnIndex(column++);
773 if (summary->placements_different == 0) {
775 summary->placements_checked);
778 summary->placements_different,
779 summary->placements_checked);
781 if (ImGui::IsItemHovered()) {
784 summary->placements_different == 0
785 ? tr(
"Every checked placement matches the game's tilemap.")
787 "%d of %d placements differ from the game: %s",
788 summary->placements_different,
789 summary->placements_checked,
791 summary->difference_bits))
796 ImGui::TableSetColumnIndex(column);
802 ImGui::TextColored(StateColor(state),
"%s",
813 if (!ImGui::BeginChild(
"##ObjectCoverageDetails", ImVec2(0, 0),
false)) {
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."));
828 const auto symbology =
831 const auto* routine = registry.GetRoutineInfo(
832 registry.GetRoutineIdForObject(
static_cast<int16_t
>(object_id)));
838 ImGui::TextWrapped(
"%s: %s | %s", tr(
"Routine"),
839 routine !=
nullptr ? routine->name.c_str() :
"-",
840 symbology.family.c_str());
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);
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."),
867 ImGui::TextWrapped(
"%s", focus.what_to_check);
871 std::optional<gui::StyleColorGuard> comparison_color;
872 if (summary->placements_different == 0) {
875 tr(
ICON_MD_CHECK " Matches the game in all %d checked placements"),
876 summary->placements_checked);
879 const std::string what =
882 tr(
ICON_MD_CLOSE " %d of %d checked placements differ from the "
884 summary->placements_different, summary->placements_checked,
898 const float verdict_right_edge = RowRightEdge();
899 bool first_verdict =
true;
901 const bool is_current = state == current;
902 if (!first_verdict) {
906 first_verdict =
false;
907 std::optional<gui::StyleColorGuard> verdict_color;
909 verdict_color.emplace(ImGuiCol_Button, StateColor(state));
920 evidence ? evidence->
note.c_str() :
"");
923 ImGui::SetNextItemWidth(-1.0f);
924 ImGui::InputTextWithHint(
"##ObjectCoverageNote",
925 tr(
"Note: what differs, which room, which size"),
927 if (ImGui::IsItemDeactivatedAfterEdit()) {
930 evidence = *existing;
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());
946 const auto* occurrences =
usage_.
Find(object_id);
947 if (occurrences ==
nullptr) {
949 "%s", tr(
"Not placed in any room of this ROM. Place it in a scratch "
950 "room to check it."));
953 ImGui::Text(tr(
"Placed %zu times in %d rooms:"), occurrences->size(),
955 for (
size_t i = 0; i < occurrences->size(); ++i) {
957 ImGui::PushID(
static_cast<int>(i));
958 if (ImGui::SmallButton(tr(
"Go"))) {
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));
969 if (result !=
nullptr && result->
object_id == object_id &&
const auto & vector() const
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
int rooms_compared() 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
void RecordRoom(bool exact)
const ObjectSummary * Summary(int object_id) const
std::optional< GameCaptureManifest > manifest_
void CheckRoomAgainstGame(int room_id)
void SetRooms(DungeonRoomStore *rooms)
std::map< int, int > last_room_for_object_
project::YazeProject * project_
std::string manifest_dir_
void FocusObject(int object_id, int room_id)
ObjectEvidenceStore evidence_
std::optional< int > note_buffer_object_
void StepAutomaticCheck()
std::optional< int > selected_object_
void DrawObjectTable(float height)
bool PassesFilters(int object_id) const
std::filesystem::path evidence_path_
void DrawObjectOccurrences(int object_id)
std::vector< int > review_order_
void SetVerdict(int object_id, ObjectEvidenceState state)
void DrawVerdictEditor(int object_id)
void Draw(bool *p_open) override
Draw the panel content.
NavigateCallback navigate_
ObjectAutoCheckResults auto_results_
std::map< int, ObjectCustomDrawCode > custom_code_
void EnsureEvidenceLoaded()
std::string EvidenceContextName() const
std::string status_message_
void StartAutomaticCheck()
std::string manifest_error_
std::string loaded_context_
std::vector< int > auto_queue_
void DrawAutomaticCheck()
bool only_auto_differences_
void ApplyAutomaticVerdicts(ObjectEvidenceState state)
void GoToOccurrence(int object_id, const ObjectOccurrence &occurrence)
void DrawAutomaticCheckResults()
void SetCaptureDir(const std::string &dir)
DungeonRoomStore * rooms_
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_capture_dir(std::string dir)
void Set(int object_id, ObjectEvidence evidence)
int RoomCountFor(int object_id) const
int rooms_scanned() 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.
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
static DrawRoutineRegistry & Get()
void RenderRoomGraphics()
const std::vector< RoomObject > & GetTileObjects() const
void EnsureObjectsLoaded()
void SetGameData(GameData *data)
#define ICON_MD_FOLDER_OPEN
#define ICON_MD_PLAY_ARROW
#define ICON_MD_SKIP_NEXT
std::string SanitizeForFilename(const std::string &value)
float ButtonWidth(const char *label)
ImVec4 StateColor(ObjectEvidenceState state)
const char * ObjectListLabel(int list_index)
void SameLineIfFits(float width, float right_edge)
constexpr char kEvidenceSubdir[]
float CheckboxWidth(const char *label)
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)
@ kIntentionalPreviewLimit
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 GetDisabledColor()
std::string ComputeSha1Hex(const uint8_t *data, size_t size)
Compute SHA-1 hash of data, return lowercase hex string (40 chars).
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)
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)
ObjectEvidenceState state
Modern project structure with comprehensive settings consolidation.
core::HackManifest hack_manifest