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"
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))
64 return out.empty() ? std::string(
"unnamed") : out;
68 if (needle ==
nullptr || needle[0] ==
'\0') {
73 return lower_haystack.find(lower_needle) != std::string::npos;
93 const float line_end =
94 ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + width;
95 if (line_end <= right_edge) {
101 return ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
105 return ImGui::CalcTextSize(label,
nullptr,
true).x +
106 ImGui::GetStyle().FramePadding.x * 2.0f;
110 return ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x +
111 ImGui::CalcTextSize(label,
nullptr,
true).x;
115 return absl::FormatTime(
"%Y-%m-%dT%H:%M:%SZ", absl::Now(),
116 absl::UTCTimeZone());
133 return "project_" + SanitizeForFilename(
project_->
name);
138 return std::string();
146 registry.Initialize();
156 const std::vector<core::ProtectedRegion> no_hooks;
165 for (
int room_id = 0; room_id < static_cast<int>(
rooms_->
size()); ++room_id) {
167 room->EnsureObjectsLoaded();
190 if (!app_data.ok()) {
204 ". Verdicts will not be saved until it "
205 "is fixed or moved.");
237 if (!manifest.ok()) {
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));
268 const auto start = std::chrono::steady_clock::now();
269 constexpr auto kFrameBudget = std::chrono::milliseconds(12);
272 if (std::chrono::steady_clock::now() - start > kFrameBudget) {
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>());
289 "Room 0x%03X: %s", room_id, std::string(game.status().message())));
298 std::vector<bool> hidden(objects.size());
299 for (
size_t i = 0; i < objects.size(); ++i) {
301 static_cast<int>(room.
tag2()),
305 objects,
yaze, hidden);
309 for (
const auto& placement : check.placements) {
321 for (
auto& [object_id, proposal] :
323 if (proposal.state != state) {
326 proposal.updated_utc = NowUtc();
332 status_message_ = absl::StrFormat(
"Marked %d objects as \"%s\".", applied,
354 evidence = *existing;
356 evidence.
state = state;
359 evidence.
room_id = room_it->second;
389 const std::optional<int> next =
398 if (
const auto* occurrences =
usage_.
Find(*next);
399 occurrences !=
nullptr && !occurrences->empty()) {
413 if (summary ==
nullptr || summary->placements_different == 0) {
422 const std::string haystack = absl::StrCat(
435 ImGui::TextDisabled(
"%s", tr(
"Load a ROM to check dungeon objects."));
449 const float details_height = ImGui::GetContentRegionAvail().y * 0.45f;
468 ImGui::TextWrapped(tr(
"Checked %d of %d objects placed in this ROM (%d "
471 ImGui::ProgressBar(placed > 0 ?
static_cast<float>(checked) / placed : 0.0f,
472 ImVec2(-1.0f, 0.0f));
474 const float right_edge = RowRightEdge();
480 const std::string label =
482 counts[
static_cast<int>(state)]);
484 SameLineIfFits(ImGui::CalcTextSize(label.c_str()).x, right_edge);
487 ImGui::TextColored(StateColor(state),
"%s", label.c_str());
490 const std::string next_label =
492 const std::string rescan_label =
494 if (ImGui::Button(next_label.c_str())) {
497 if (ImGui::IsItemHovered()) {
499 "%s", tr(
"Opens the next unchecked object in release-focus order, in "
500 "a room that uses it, with the object selected."));
502 SameLineIfFits(ButtonWidth(rescan_label.c_str()), right_edge);
503 if (ImGui::Button(rescan_label.c_str())) {
521 if (!ImGui::CollapsingHeader(tr(
"Automatic check against the game"))) {
524 ImGui::PushID(
"ObjectCoverageAuto");
526 ImGui::TextWrapped(
"%s %s", tr(
"Game captures:"),
527 dir.empty() ? tr(
"none chosen") : dir.c_str());
532 if (!chosen.empty()) {
536 if (ImGui::IsItemHovered()) {
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."));
548 ImGui::TextWrapped(tr(
"%zu rooms captured from the game for this ROM."),
551 const float fraction =
556 fraction, ImVec2(-1.0f, 0.0f),
560 tr(
"Run automatic check"))
574 int objects_matching = 0;
575 int objects_differing = 0;
577 (summary.placements_different == 0 ? objects_matching
578 : objects_differing)++;
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);
586 "%s", tr(
"Compares tiles, flips, palette rows and priority. Colors and "
587 "graphics sheets are not compared."));
592 int propose_verified = 0;
593 int propose_broken = 0;
594 for (
const auto& [object_id, proposal] :
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 =
604 tr(
"Mark differences as Broken"), propose_broken);
605 ImGui::BeginDisabled(propose_verified == 0);
606 if (ImGui::Button(verified_label.c_str())) {
609 ImGui::EndDisabled();
610 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
612 "%s", tr(
"Sets \"Matches game\" on unchecked objects whose every "
613 "checked placement matches. Objects you already judged are "
616 SameLineIfFits(ButtonWidth(broken_label.c_str()), right_edge);
617 ImGui::BeginDisabled(propose_broken == 0);
618 if (ImGui::Button(broken_label.c_str())) {
621 ImGui::EndDisabled();
622 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
624 "%s", tr(
"Sets \"Broken\" on unchecked objects with any differing "
625 "placement, with a note naming what differs."));
631 const float right_edge = RowRightEdge();
632 const float available = ImGui::GetContentRegionAvail().x;
633 const float spacing = ImGui::GetStyle().ItemSpacing.x;
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)
639 ImGui::SetNextItemWidth(search_width);
640 ImGui::InputTextWithHint(
"##ObjectCoverageFilter", tr(
"Search ID or name"),
642 if (fields_share_line) {
645 ImGui::SetNextItemWidth(fields_share_line ? 140.0f : available);
646 const char* preview =
650 if (ImGui::BeginCombo(
"##ObjectCoverageState", preview)) {
651 if (ImGui::Selectable(tr(
"Any status"),
state_filter_ < 0)) {
655 const int value =
static_cast<int>(state);
663 SameLineIfFits(CheckboxWidth(tr(
"Placed only")), right_edge);
665 SameLineIfFits(CheckboxWidth(tr(
"Release focus only")), right_edge);
666 ImGui::Checkbox(tr(
"Release focus only"), &
only_focus_);
668 SameLineIfFits(CheckboxWidth(tr(
"Differs from game")), right_edge);
674 std::vector<int> rows;
678 rows.push_back(object_id);
682 constexpr ImGuiTableFlags kFlags =
683 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
684 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable |
685 ImGuiTableFlags_SizingStretchProp;
688 const bool show_focus_column = ImGui::GetContentRegionAvail().x >= 420.0f;
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)))) {
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,
703 ImGui::TableSetupColumn(tr(
"Rooms"), ImGuiTableColumnFlags_WidthFixed, 44.0f);
704 if (show_auto_column) {
705 ImGui::TableSetupColumn(tr(
"Game"), ImGuiTableColumnFlags_WidthFixed,
708 ImGui::TableSetupColumn(tr(
"Status"), ImGuiTableColumnFlags_WidthStretch,
710 ImGui::TableHeadersRow();
713 ImGuiListClipper clipper;
714 clipper.Begin(
static_cast<int>(rows.size()));
718 if (it != rows.end()) {
719 scroll_row =
static_cast<int>(it - rows.begin());
720 clipper.IncludeItemByIndex(scroll_row);
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);
730 ImGui::TableSetColumnIndex(0);
732 if (ImGui::Selectable(
FormatObjectId(object_id).c_str(), selected,
733 ImGuiSelectableFlags_SpanAllColumns)) {
736 if (row == scroll_row) {
737 ImGui::SetScrollHereY(0.3f);
739 ImGui::TableSetColumnIndex(1);
744 if (ImGui::IsItemHovered()) {
745 ImGui::SetTooltip(
"%s",
746 tr(
"Your ROM changes this object's draw code"));
750 if (show_focus_column) {
751 ImGui::TableSetColumnIndex(column++);
754 ImGui::TextUnformatted(groups[
static_cast<size_t>(group)].name);
757 ImGui::TableSetColumnIndex(column++);
759 if (room_count > 0) {
760 ImGui::Text(
"%d", room_count);
762 ImGui::TextDisabled(
"-");
764 if (show_auto_column) {
765 ImGui::TableSetColumnIndex(column++);
767 if (summary->placements_different == 0) {
769 summary->placements_checked);
772 summary->placements_different,
773 summary->placements_checked);
775 if (ImGui::IsItemHovered()) {
778 summary->placements_different == 0
779 ? tr(
"Every checked placement matches the game's tilemap.")
781 "%d of %d placements differ from the game: %s",
782 summary->placements_different,
783 summary->placements_checked,
785 summary->difference_bits))
790 ImGui::TableSetColumnIndex(column);
796 ImGui::TextColored(StateColor(state),
"%s",
807 if (!ImGui::BeginChild(
"##ObjectCoverageDetails", ImVec2(0, 0),
false)) {
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."));
822 const auto symbology =
825 const auto* routine = registry.GetRoutineInfo(
826 registry.GetRoutineIdForObject(
static_cast<int16_t
>(object_id)));
832 ImGui::TextWrapped(
"%s: %s | %s", tr(
"Routine"),
833 routine !=
nullptr ? routine->name.c_str() :
"-",
834 symbology.family.c_str());
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);
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."),
861 ImGui::TextWrapped(
"%s", focus.what_to_check);
865 std::optional<gui::StyleColorGuard> comparison_color;
866 if (summary->placements_different == 0) {
869 tr(
ICON_MD_CHECK " Matches the game in all %d checked placements"),
870 summary->placements_checked);
873 const std::string what =
876 tr(
ICON_MD_CLOSE " %d of %d checked placements differ from the "
878 summary->placements_different, summary->placements_checked,
892 const float verdict_right_edge = RowRightEdge();
893 bool first_verdict =
true;
895 const bool is_current = state == current;
896 if (!first_verdict) {
900 first_verdict =
false;
901 std::optional<gui::StyleColorGuard> verdict_color;
903 verdict_color.emplace(ImGuiCol_Button, StateColor(state));
914 evidence ? evidence->
note.c_str() :
"");
917 ImGui::SetNextItemWidth(-1.0f);
918 ImGui::InputTextWithHint(
"##ObjectCoverageNote",
919 tr(
"Note: what differs, which room, which size"),
921 if (ImGui::IsItemDeactivatedAfterEdit()) {
924 evidence = *existing;
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());
940 const auto* occurrences =
usage_.
Find(object_id);
941 if (occurrences ==
nullptr) {
943 "%s", tr(
"Not placed in any room of this ROM. Place it in a scratch "
944 "room to check it."));
947 ImGui::Text(tr(
"Placed %zu times in %d rooms:"), occurrences->size(),
949 for (
size_t i = 0; i < occurrences->size(); ++i) {
951 ImGui::PushID(
static_cast<int>(i));
952 if (ImGui::SmallButton(tr(
"Go"))) {
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));
963 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[]
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