83 std::string_view rom_sha1) {
84 std::vector<uint8_t> manifest_bytes;
85 if (!ReadFileBytes(dir /
"manifest.json", manifest_bytes)) {
86 return absl::NotFoundError(
87 absl::StrCat(
"No manifest.json in ", dir.string()));
90 json::parse(manifest_bytes.begin(), manifest_bytes.end(),
nullptr,
92 if (manifest.is_discarded() || !manifest.is_object() ||
93 !manifest.contains(
"rooms") || !manifest[
"rooms"].is_object()) {
94 return absl::InvalidArgumentError(
95 absl::StrCat(dir.string(),
"/manifest.json is malformed"));
98 std::vector<std::string> problems;
99 const std::string manifest_rom = ManifestString(manifest,
"rom_sha1");
100 if (manifest_rom != expected.
rom_sha1) {
101 problems.push_back(absl::StrCat(
"capture is for ROM ", manifest_rom,
102 ", baseline expects ", expected.
rom_sha1));
104 if (rom_sha1 != expected.
rom_sha1) {
107 problems.push_back(absl::StrCat(
"ROM under test is ", std::string(rom_sha1),
108 ", baseline expects ", expected.
rom_sha1));
110 const std::string entrance = ManifestString(manifest,
"entrance");
111 if (entrance != expected.
entrance) {
112 problems.push_back(absl::StrCat(
"capture entrance is \"", entrance,
113 "\", baseline expects \"",
116 const std::string room_flags = ManifestString(manifest,
"room_flags");
118 problems.push_back(absl::StrCat(
"capture room_flags is \"", room_flags,
119 "\", baseline expects \"",
123 problems.push_back(
"baseline requires no rooms");
128 const json& rooms = manifest[
"rooms"];
130 const std::string key = RoomKey(room_id);
131 if (!rooms.contains(key) || !rooms[key].is_object()) {
132 problems.push_back(absl::StrCat(
"room ", key,
": not in the manifest"));
135 const json& entry = rooms[key];
136 const std::string status = ManifestString(entry,
"status");
137 if (status !=
"ok") {
139 absl::StrCat(
"room ", key,
": status \"", status,
"\", not ok"));
142 const std::string file = ManifestString(entry,
"file");
143 const std::string sha1 = ManifestString(entry,
"sha1");
144 if (file.empty() || sha1.empty() ||
145 std::filesystem::path(file).filename().string() != file) {
147 absl::StrCat(
"room ", key,
": missing or unsafe file/sha1 entry"));
150 std::vector<uint8_t> bytes;
151 if (!ReadFileBytes(dir / file, bytes)) {
152 problems.push_back(absl::StrCat(
"room ", key,
": cannot read ", file));
156 problems.push_back(absl::StrCat(
"room ", key,
": ", file,
" is ",
157 bytes.size(),
" bytes, expected ",
162 problems.push_back(absl::StrCat(
"room ", key,
": ", file,
163 " does not match its manifest SHA-1"));
169 if (!problems.empty()) {
170 return absl::FailedPreconditionError(
171 absl::StrCat(
"Capture ", dir.string(),
" cannot be used:\n ",
172 absl::StrJoin(problems,
"\n ")));
186 const json root = json::parse(text.begin(), text.end(),
nullptr,
188 if (root.is_discarded() || !root.is_object()) {
189 return absl::InvalidArgumentError(
"Baseline is not a JSON object");
192 if (!root.contains(
"version") || !root[
"version"].is_number_integer() ||
193 root[
"version"].get<
int>() != kBaselineVersion) {
194 return absl::InvalidArgumentError(
195 absl::StrCat(
"Baseline version must be ", kBaselineVersion));
197 baseline.
version = kBaselineVersion;
198 baseline.
state = ManifestString(root,
"state");
199 if (baseline.
state.empty()) {
200 return absl::InvalidArgumentError(
"Baseline needs a \"state\"");
202 if (!root.contains(
"capture") || !root[
"capture"].is_object()) {
203 return absl::InvalidArgumentError(
"Baseline needs a \"capture\" object");
205 const json& capture = root[
"capture"];
211 return absl::InvalidArgumentError(
212 "Baseline capture needs a 40-digit rom_sha1 and an entrance");
214 if (!capture.contains(
"required_rooms") ||
215 !capture[
"required_rooms"].is_array()) {
216 return absl::InvalidArgumentError(
217 "Baseline capture needs a required_rooms array");
219 for (
const auto& room : capture[
"required_rooms"]) {
221 if (!room.is_string() || !ParseRoomId(room.get<std::string>(), room_id)) {
222 return absl::InvalidArgumentError(
223 "required_rooms entries must look like \"0x0A4\"");
228 if (!root.contains(
"groups") || !root[
"groups"].is_array()) {
229 return absl::InvalidArgumentError(
"Baseline needs a \"groups\" array");
231 for (
const auto& item : root[
"groups"]) {
233 if (!item.is_object() || !item.contains(
"room") ||
234 !item[
"room"].is_string() ||
235 !ParseRoomId(item[
"room"].get<std::string>(), group.
room_id)) {
236 return absl::InvalidArgumentError(
237 "Each group needs a \"room\" like \"0x0A4\"");
239 group.
reason = ManifestString(item,
"reason");
240 group.
evidence = ManifestString(item,
"evidence");
241 group.
reviewed = item.contains(
"reviewed") &&
242 item[
"reviewed"].is_boolean() &&
243 item[
"reviewed"].get<
bool>();
244 if (!item.contains(
"tiles") || !item[
"tiles"].is_array() ||
245 item[
"tiles"].empty()) {
246 return absl::InvalidArgumentError(
247 absl::StrCat(
"Group for room ", RoomKey(group.
room_id),
248 " needs a non-empty \"tiles\" array"));
250 for (
const auto& tile_json : item[
"tiles"]) {
252 if (!tile_json.is_object() || !tile_json.contains(
"layer") ||
253 !tile_json.contains(
"x") || !tile_json.contains(
"y") ||
254 !tile_json[
"layer"].is_number_integer() ||
255 !tile_json[
"x"].is_number_integer() ||
256 !tile_json[
"y"].is_number_integer()) {
257 return absl::InvalidArgumentError(
258 absl::StrCat(
"Bad tile in room ", RoomKey(group.
room_id)));
260 tile.
layer = tile_json[
"layer"].get<
int>();
261 tile.
x = tile_json[
"x"].get<
int>();
262 tile.
y = tile_json[
"y"].get<
int>();
263 tile.
digest = ManifestString(tile_json,
"digest");
264 if ((tile.
layer != 1 && tile.
layer != 2) || tile.
x < 0 || tile.
x > 63 ||
265 tile.
y < 0 || tile.
y > 63 || tile.
digest.size() != 12) {
266 return absl::InvalidArgumentError(
267 absl::StrCat(
"Bad tile in room ", RoomKey(group.
room_id)));
269 group.
tiles.push_back(tile);
275 return absl::InvalidArgumentError(
276 absl::StrCat(
"Group for room ", RoomKey(group.
room_id),
277 " is not in required_rooms"));
279 baseline.
groups.push_back(std::move(group));
345 const std::vector<ObservedDifference>& observed,
346 const std::vector<int>& rooms_checked) {
348 const std::set<int> checked(rooms_checked.begin(), rooms_checked.end());
352 bool approved =
false;
355 std::map<TileKey, Expected> expected;
356 for (
const auto& group : baseline.
groups) {
357 if (!checked.contains(group.room_id)) {
360 const bool approved =
361 group.reviewed && !group.evidence.empty() && !group.reason.empty();
362 for (
const auto& tile : group.tiles) {
363 const TileKey key{group.room_id, tile.layer, tile.x, tile.y};
364 if (expected.contains(key)) {
366 tile.layer, tile.x, tile.y, group.reason});
369 expected.emplace(key, Expected{tile.digest, approved, group.reason});
372 group.room_id, tile.layer, tile.x, tile.y,
373 group.evidence.empty()
374 ?
"no evidence recorded"
375 :
"reviewed is false"});
380 std::set<TileKey> seen;
381 for (
const auto& difference : observed) {
382 if (!checked.contains(difference.room_id)) {
385 const TileKey key{difference.room_id, difference.layer, difference.x,
388 const std::string digest =
390 auto it = expected.find(key);
391 if (it == expected.end()) {
394 difference.x, difference.y,
395 absl::StrFormat(
"game %04X, yaze %04X", difference.game,
399 if (it->second.digest != digest) {
402 difference.x, difference.y,
403 absl::StrFormat(
"baseline %s (%s), now game %04X, yaze %04X",
404 it->second.digest, it->second.reason,
405 difference.game, difference.yaze)});
408 if (it->second.approved) {
413 for (
const auto& [key, entry] : expected) {
414 if (!seen.contains(key)) {
415 const auto& [room_id, layer, x, y] = key;
418 absl::StrCat(entry.reason,
419 ": yaze now matches the game; remove the entry")});
425 return std::tie(a.room_id, a.layer, a.y, a.x, a.kind) <
426 std::tie(b.room_id, b.layer, b.y, b.x, b.kind);