11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_format.h"
13#include "nlohmann/json.hpp"
30 static const std::vector<std::vector<IdRange>> kRanges = {
32 {{0x33, 0x34}, {0x70, 0x71}, {0x8D, 0x8E}, {0xB2, 0xB4}},
34 {{0x4C, 0x4C}, {0x8F, 0x8F}, {0xFD6, 0xFD9}},
36 {{0x3F, 0x48}, {0x79, 0x7A}, {0xC8, 0xC9}, {0xD9, 0xD9}, {0xE7, 0xE7}},
38 {{0xCA, 0xCA}, {0xD1, 0xD2}, {0xE3, 0xE6}},
40 {{0xD8, 0xD8}, {0xDA, 0xDA}},
56 if (key.size() < 3 || key[0] !=
'0' || (key[1] !=
'x' && key[1] !=
'X')) {
60 const long value = std::strtol(key.c_str() + 2, &end, 16);
61 if (end ==
nullptr || *end !=
'\0' || value < 0 || value > 0xFFF) {
64 return static_cast<int>(value);
76 return "fixed-awaiting-proof";
80 return "intentional-preview-limit";
92 return "Fixed, recheck";
94 return "Matches game";
96 return "Preview limit";
102 std::string_view key) {
113 return it ==
entries_.end() ? nullptr : &it->second;
123 evidence.
note.empty()) {
127 entries_[object_id] = std::move(evidence);
131 nlohmann::json objects = nlohmann::json::object();
132 for (
const auto& [object_id, evidence] :
entries_) {
133 nlohmann::json entry;
135 if (!evidence.note.empty()) {
136 entry[
"note"] = evidence.note;
138 if (evidence.room_id >= 0) {
139 entry[
"room"] = evidence.room_id;
141 if (!evidence.rom_sha1.empty()) {
142 entry[
"rom_sha1"] = evidence.rom_sha1;
144 if (!evidence.updated_utc.empty()) {
145 entry[
"updated_utc"] = evidence.updated_utc;
150 root[
"version"] = kEvidenceFileVersion;
151 root[
"objects"] = std::move(objects);
155 return root.dump(2) +
"\n";
159 std::string_view json) {
160 nlohmann::json root = nlohmann::json::parse(json,
nullptr,
162 if (root.is_discarded() || !root.is_object()) {
163 return absl::InvalidArgumentError(
"Object evidence file is not JSON");
165 if (!root.contains(
"version") || !root[
"version"].is_number_integer() ||
166 root[
"version"].get<
int>() != kEvidenceFileVersion) {
167 return absl::InvalidArgumentError(absl::StrCat(
168 "Object evidence file version must be ", kEvidenceFileVersion));
171 if (root.contains(
"capture_dir") && root[
"capture_dir"].is_string()) {
172 store.
capture_dir_ = root[
"capture_dir"].get<std::string>();
174 if (!root.contains(
"objects")) {
177 const auto& objects = root[
"objects"];
178 if (!objects.is_object()) {
179 return absl::InvalidArgumentError(
"\"objects\" must be a JSON object");
181 for (
const auto& [key, entry] : objects.items()) {
182 const std::optional<int> object_id = ParseObjectIdKey(key);
184 return absl::InvalidArgumentError(
185 absl::StrCat(
"Bad object ID key \"", key,
"\""));
187 if (!entry.is_object() || !entry.contains(
"state") ||
188 !entry[
"state"].is_string()) {
189 return absl::InvalidArgumentError(
190 absl::StrCat(
"Object ", key,
" has no state"));
192 const std::optional<ObjectEvidenceState> state =
195 return absl::InvalidArgumentError(
196 absl::StrCat(
"Object ", key,
" has unknown state \"",
197 entry[
"state"].get<std::string>(),
"\""));
200 evidence.
state = *state;
201 if (entry.contains(
"note") && entry[
"note"].is_string()) {
202 evidence.
note = entry[
"note"].get<std::string>();
204 if (entry.contains(
"room") && entry[
"room"].is_number_integer()) {
205 evidence.
room_id = entry[
"room"].get<
int>();
207 if (entry.contains(
"rom_sha1") && entry[
"rom_sha1"].is_string()) {
208 evidence.
rom_sha1 = entry[
"rom_sha1"].get<std::string>();
210 if (entry.contains(
"updated_utc") && entry[
"updated_utc"].is_string()) {
211 evidence.
updated_utc = entry[
"updated_utc"].get<std::string>();
213 store.
entries_[*object_id] = std::move(evidence);
219 const std::filesystem::path& path) {
221 if (!std::filesystem::exists(path, ec)) {
224 std::ifstream in(path, std::ios::binary);
226 return absl::UnavailableError(absl::StrCat(
"Cannot read ", path.string()));
228 std::ostringstream contents;
229 contents << in.rdbuf();
234 const std::filesystem::path& path)
const {
236 if (path.has_parent_path()) {
237 std::filesystem::create_directories(path.parent_path(), ec);
239 return absl::UnavailableError(absl::StrCat(
240 "Cannot create ", path.parent_path().string(),
": ", ec.message()));
243 std::filesystem::path temp_path = path;
246 std::ofstream out(temp_path, std::ios::binary | std::ios::trunc);
248 return absl::UnavailableError(
249 absl::StrCat(
"Cannot write ", temp_path.string()));
253 return absl::UnavailableError(
254 absl::StrCat(
"Failed writing ", temp_path.string()));
257 std::filesystem::rename(temp_path, path, ec);
259 std::filesystem::remove(temp_path, ec);
260 return absl::UnavailableError(
261 absl::StrCat(
"Cannot replace ", path.string()));
263 return absl::OkStatus();
267 const std::vector<zelda3::RoomObject>& objects) {
268 for (
size_t i = 0; i < objects.size(); ++i) {
269 const auto&
object = objects[i];
273 occurrence.
x =
object.x();
274 occurrence.
y =
object.y();
275 occurrence.
size =
object.size_;
276 occurrence.
layer =
object.GetLayerValue();
283 int object_id)
const {
285 return it ==
by_object_.end() ? nullptr : &it->second;
289 const auto* occurrences =
Find(object_id);
290 if (occurrences ==
nullptr) {
294 for (
const auto& occurrence : *occurrences) {
295 rooms.insert(occurrence.room_id);
297 return static_cast<int>(rooms.size());
301 static const std::vector<ReleaseFocusGroup> kGroups = {
303 "Anchor, repeat count and end-cap overlap. Try sizes 0, 1 and 15 and "
304 "placement at the room edge."},
306 "Horizontal, vertical and corner joins. Start with Oracle room 0x042."},
308 "Palette, edge versus interior tiles, and each layer on its own. "
309 "Oracle witnesses: 0x04A, then 0x033."},
310 {
"Ice and moving floors",
311 "Stamp size and palette in a real room. Oracle ice witnesses: "
312 "0x08C at (10,11) on BG1 and 0x0CE at (29,23) on BG2."},
314 "Check the editor indicator only. The in-game water is HDMA, so it is "
315 "not expected to match."},
317 "Stairs, the wall behind them and doors all visible on the lower "
318 "level. Room 0x077 has spiral stairs."},
319 {
"Corners",
"Corner and seam tiles, and anchors at the room boundary."},
325 const auto& ranges = FocusGroupRanges();
326 for (
size_t group = 0; group < ranges.size(); ++group) {
327 for (
const IdRange& range : ranges[group]) {
328 if (object_id >= range.first && object_id <= range.last) {
329 return static_cast<int>(group);
337 std::vector<int> order(object_ids.begin(), object_ids.end());
339 auto rank = [other_group](
int object_id) {
341 return group < 0 ? other_group : group;
343 std::stable_sort(order.begin(), order.end(), [&rank](
int a,
int b) {
344 const int rank_a = rank(a);
345 const int rank_b = rank(b);
346 return rank_a != rank_b ? rank_a < rank_b : a < b;
354 std::optional<int> after_object_id) {
355 if (review_order.empty()) {
359 if (after_object_id) {
361 std::find(review_order.begin(), review_order.end(), *after_object_id);
362 if (it != review_order.end()) {
363 start =
static_cast<size_t>(it - review_order.begin()) + 1;
366 for (
size_t step = 0; step < review_order.size(); ++step) {
367 const int object_id = review_order[(start + step) % review_order.size()];
369 usage.
Find(object_id) !=
nullptr) {
378 out.reserve(name.size());
379 for (
const char ch : name) {
380 if (ch >=
'A' && ch <=
'Z') {
381 out.push_back(
static_cast<char>(ch -
'A' +
'a'));
384 const bool keep = (ch >=
'a' && ch <=
'z') || (ch >=
'0' && ch <=
'9') ||
385 ch ==
'_' || ch ==
'-';
386 out.push_back(keep ? ch :
'_');
388 return out.empty() ? std::string(
"unnamed") : out;
392 for (
const auto& part : dir) {
401 const std::filesystem::path& dir) {
403 return absl::InvalidArgumentError(
404 absl::StrCat(dir.string(),
" must not contain \"..\""));
406 const std::filesystem::path path = dir /
"manifest.json";
407 std::ifstream in(path, std::ios::binary);
409 return absl::NotFoundError(
410 absl::StrCat(
"No manifest.json in ", dir.string()));
412 std::ostringstream contents;
413 contents << in.rdbuf();
414 nlohmann::json root = nlohmann::json::parse(contents.str(),
nullptr,
416 if (root.is_discarded() || !root.is_object() || !root.contains(
"rom_sha1") ||
417 !root[
"rom_sha1"].is_string() || !root.contains(
"rooms") ||
418 !root[
"rooms"].is_object()) {
419 return absl::InvalidArgumentError(
420 absl::StrCat(path.string(),
" is not a room tilemap capture manifest"));
423 manifest.
rom_sha1 = root[
"rom_sha1"].get<std::string>();
424 for (
const auto& [key, entry] : root[
"rooms"].items()) {
425 const std::optional<int> room_id = ParseObjectIdKey(key);
426 if (!room_id || !entry.is_object() || !entry.contains(
"status") ||
427 !entry[
"status"].is_string() ||
428 entry[
"status"].get<std::string>().rfind(
"ok", 0) != 0) {
439 return dir / absl::StrFormat(
"room_%03X.tilemap", room_id);
474 int room_id,
size_t object_index)
const {
475 auto it =
placements_.find({room_id, object_index});
476 return it ==
placements_.end() ? nullptr : &it->second;
480 int object_id)
const {
482 return it ==
summaries_.end() ? nullptr : &it->second;
487 const std::string& rom_sha1) {
488 std::map<int, ObjectEvidence> proposals;
489 const std::string rom_label = rom_sha1.substr(0, 8);
490 for (
const auto& [object_id, summary] : results.
summaries()) {
491 if (summary.placements_checked == 0 ||
492 evidence.
Find(object_id) !=
nullptr) {
497 if (summary.placements_different == 0) {
499 proposal.
room_id = summary.rooms_checked.front();
500 proposal.
note = absl::StrFormat(
501 "Automatic: tilemap matches the game in all %d placements across %d "
502 "rooms (Mesen capture, ROM %s). Colors and graphics not compared.",
503 summary.placements_checked,
504 static_cast<int>(summary.rooms_checked.size()), rom_label);
507 proposal.
room_id = summary.first_room_different;
508 proposal.
note = absl::StrFormat(
509 "Automatic: %d of %d placements differ from the game (%s); first in "
510 "room 0x%03X (Mesen capture, ROM %s).",
511 summary.placements_different, summary.placements_checked,
513 summary.first_room_different, rom_label);
515 proposals[object_id] = std::move(proposal);
521 const std::vector<zelda3::ObjectDrawCode>& draw_code,
522 const std::vector<core::ProtectedRegion>& hooks) {
523 std::map<int, ObjectCustomDrawCode> out;
524 for (
const auto& code : draw_code) {
527 if (code.jumps_to_expanded_code) {
529 custom.
address = code.jump_target;
532 for (
const auto& hook : hooks) {
533 if (hook.start < code.routine_end && code.routine_start < hook.end) {
535 custom.
address = std::max(hook.start, code.routine_start);
537 custom.module = hook.module;
543 out.emplace(code.object_id, std::move(custom));
550 return absl::StrFormat(
"0x%03X", object_id);
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
std::map< std::pair< int, size_t >, PlacementAutoResult > placements_
std::map< int, ObjectSummary > summaries_
const ObjectSummary * Summary(int object_id) const
const ObjectEvidence * Find(int object_id) const
absl::Status SaveToFile(const std::filesystem::path &path) const
static absl::StatusOr< ObjectEvidenceStore > FromJson(std::string_view json)
ObjectEvidenceState StateOf(int object_id) const
std::string ToJson() const
static absl::StatusOr< ObjectEvidenceStore > LoadFromFile(const std::filesystem::path &path)
std::map< int, ObjectEvidence > entries_
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)
std::map< int, std::vector< ObjectOccurrence > > by_object_
const std::vector< std::vector< IdRange > > & FocusGroupRanges()
std::optional< int > ParseObjectIdKey(const std::string &key)
constexpr int kEvidenceFileVersion
Editors are the view controllers for the application.
std::optional< ObjectEvidenceState > ParseObjectEvidenceState(std::string_view key)
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)
const char * ObjectEvidenceStateKey(ObjectEvidenceState state)
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 DescribeTileWordDifference(uint16_t xor_bits)
std::vector< int > captured_rooms
std::vector< int > rooms_checked
ObjectEvidenceState state