yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_validate_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <fstream>
5#include <limits>
6#include <string>
7#include <utility>
8#include <vector>
9
10#include "absl/status/status.h"
11#include "absl/status/statusor.h"
12#include "absl/strings/str_format.h"
15#include "core/features.h"
16#include "rom/rom.h"
22#include "zelda3/dungeon/room.h"
24
25namespace yaze::cli {
26
27namespace {
28
29constexpr int kType1Start = 0x00;
30constexpr int kType1End = 0xF7;
31constexpr int kType2Start = 0x100;
32constexpr int kType2End = 0x13F;
33constexpr int kType3Start = 0xF80;
34constexpr int kType3End = 0xFFF;
35constexpr int kNumRooms = 296;
36
38 bool has_tiles = false;
39 int min_x = 0;
40 int min_y = 0;
41 int max_x = 0;
42 int max_y = 0;
43 int width = 0;
44 int height = 0;
45};
46
48 const std::vector<zelda3::ObjectDrawer::TileTrace>& trace) {
49 TraceBounds bounds;
50 if (trace.empty()) {
51 return bounds;
52 }
53
54 int min_x = std::numeric_limits<int>::max();
55 int min_y = std::numeric_limits<int>::max();
56 int max_x = std::numeric_limits<int>::min();
57 int max_y = std::numeric_limits<int>::min();
58
59 for (const auto& entry : trace) {
60 min_x = std::min(min_x, static_cast<int>(entry.x_tile));
61 min_y = std::min(min_y, static_cast<int>(entry.y_tile));
62 max_x = std::max(max_x, static_cast<int>(entry.x_tile));
63 max_y = std::max(max_y, static_cast<int>(entry.y_tile));
64 }
65
66 bounds.has_tiles = true;
67 bounds.min_x = min_x;
68 bounds.min_y = min_y;
69 bounds.max_x = max_x;
70 bounds.max_y = max_y;
71 bounds.width = max_x - min_x + 1;
72 bounds.height = max_y - min_y + 1;
73 return bounds;
74}
75
76// Choose an object origin (x,y) such that the expected selection bounds rectangle
77// (origin + expected.offset + expected.size) fits inside the 64x64 validation
78// canvas. This avoids false mismatches from traces being clipped at y<0/x<0.
80 const zelda3::DimensionService::DimensionResult& expected_bounds) {
81 constexpr int kRoomMaxX = zelda3::DrawContext::kMaxTilesX - 1;
82 constexpr int kRoomMaxY = zelda3::DrawContext::kMaxTilesY - 1;
83
84 const int min_x = expected_bounds.offset_x_tiles;
85 const int min_y = expected_bounds.offset_y_tiles;
86 const int max_x =
87 expected_bounds.offset_x_tiles + expected_bounds.width_tiles - 1;
88 const int max_y =
89 expected_bounds.offset_y_tiles + expected_bounds.height_tiles - 1;
90
91 const int low_x = std::max(0, -min_x);
92 const int low_y = std::max(0, -min_y);
93 const int high_x = std::min(kRoomMaxX, kRoomMaxX - max_x);
94 const int high_y = std::min(kRoomMaxY, kRoomMaxY - max_y);
95
96 auto choose_axis = [](int low, int high, int room_max) -> int {
97 if (low <= high) {
98 // Prefer origin at 0 when it fits; otherwise pick the nearest in-range.
99 if (0 < low)
100 return low;
101 if (0 > high)
102 return high;
103 return 0;
104 }
105 // No feasible origin; clamp as a best-effort so we still produce output.
106 return std::max(0, std::min(low, room_max));
107 };
108
109 return {choose_axis(low_x, high_x, kRoomMaxX),
110 choose_axis(low_y, high_y, kRoomMaxY)};
111}
112
113} // namespace
114
115namespace detail {
116
118 int object_id, int size,
119 const zelda3::DimensionService::DimensionResult& bounds, int object_x,
120 int object_y) {
121 constexpr int kRoomTilesX = zelda3::DrawContext::kMaxTilesX;
122 constexpr int kRoomTilesY = zelda3::DrawContext::kMaxTilesY;
123 const int room_max_x = kRoomTilesX - 1;
124 const int room_max_y = kRoomTilesY - 1;
125
126 const int min_x = object_x + bounds.offset_x_tiles;
127 const int min_y = object_y + bounds.offset_y_tiles;
128 int max_x = min_x + bounds.width_tiles - 1;
129 int max_y = min_y + bounds.height_tiles - 1;
130
131 if (size > 0) {
132 // Get base dimensions (size=0) through DimensionService.
133 zelda3::RoomObject base_obj(object_id, 0, 0, 0, 0);
134 auto base_dims = zelda3::DimensionService::Get().GetDimensions(base_obj);
135 const int base_w = base_dims.width_tiles;
136 const int base_h = base_dims.height_tiles;
137 const int sel_w = bounds.width_tiles;
138 const int sel_h = bounds.height_tiles;
139
140 const bool extends_h = (sel_w > base_w) && (sel_h == base_h);
141 const bool extends_v = (sel_h > base_h) && (sel_w == base_w);
142
143 if (extends_h && max_x > room_max_x && base_w > 0) {
144 const int delta = sel_w - base_w;
145 if (delta > 0 && (delta % size) == 0) {
146 const int spacing = delta / size;
147 if (spacing > base_w) {
148 // Keep only fully visible repeated segments; drop partial tail repeats.
149 const int extra_room_tiles = room_max_x - (min_x + (base_w - 1));
150 const int max_repeat =
151 std::clamp(std::min(size, extra_room_tiles / spacing), 0, size);
152 const int last_start = min_x + (max_repeat * spacing);
153 const int last_end = last_start + base_w - 1;
154 max_x = std::min(max_x, last_end);
155 }
156 }
157 }
158
159 if (extends_v && max_y > room_max_y && base_h > 0) {
160 const int delta = sel_h - base_h;
161 if (delta > 0 && (delta % size) == 0) {
162 const int spacing = delta / size;
163 if (spacing > base_h) {
164 // Keep only fully visible repeated segments; drop partial tail repeats.
165 const int extra_room_tiles = room_max_y - (min_y + (base_h - 1));
166 const int max_repeat =
167 std::clamp(std::min(size, extra_room_tiles / spacing), 0, size);
168 const int last_start = min_y + (max_repeat * spacing);
169 const int last_end = last_start + base_h - 1;
170 max_y = std::min(max_y, last_end);
171 }
172 }
173 }
174 }
175
176 const int clipped_min_x = std::clamp(min_x, 0, room_max_x);
177 const int clipped_min_y = std::clamp(min_y, 0, room_max_y);
178 const int clipped_max_x = std::clamp(max_x, 0, room_max_x);
179 const int clipped_max_y = std::clamp(max_y, 0, room_max_y);
180
182 clipped.offset_x_tiles = clipped_min_x - object_x;
183 clipped.offset_y_tiles = clipped_min_y - object_y;
184 clipped.width_tiles = std::max(0, clipped_max_x - clipped_min_x + 1);
185 clipped.height_tiles = std::max(0, clipped_max_y - clipped_min_y + 1);
186 return clipped;
187}
188
189} // namespace detail
190
191namespace {
192
193std::vector<int> BuildObjectIds(const absl::StatusOr<int>& object_arg) {
194 std::vector<int> object_ids;
195 if (object_arg.ok()) {
196 object_ids.push_back(object_arg.value());
197 return object_ids;
198 }
199
200 for (int id = kType1Start; id <= kType1End; ++id) {
201 object_ids.push_back(id);
202 }
203 for (int id = kType2Start; id <= kType2End; ++id) {
204 object_ids.push_back(id);
205 }
206 for (int id = kType3Start; id <= kType3End; ++id) {
207 object_ids.push_back(id);
208 }
209 return object_ids;
210}
211
212std::vector<int> BuildSizes(const absl::StatusOr<int>& size_arg) {
213 if (size_arg.ok()) {
214 return {size_arg.value()};
215 }
216 return {0, 1, 2, 7, 15};
217}
218
220 int object_id = 0;
221 int size = 0;
222 bool has_tiles = false;
223 int trace_width = 0;
224 int trace_height = 0;
225 int trace_min_x = 0;
226 int trace_min_y = 0;
227 int expected_width = 0;
228 int expected_height = 0;
229 int expected_offset_x = 0;
230 int expected_offset_y = 0;
231 int trace_offset_x = 0;
232 int trace_offset_y = 0;
233 bool has_room_context = false;
234 int room_id = -1;
235 int object_index = -1;
236 int object_x = 0;
237 int object_y = 0;
238 int object_layer = 0;
239 bool size_mismatch = false;
240 bool offset_mismatch = false;
241
242 std::string FormatText(bool include_room_fields) const {
243 if (!has_tiles) {
244 if (include_room_fields && has_room_context) {
245 return absl::StrFormat(
246 "room 0x%02X obj#%d (0x%03X size %d @%d,%d L%d): no tiles drawn",
247 room_id, object_index, object_id, size, object_x, object_y,
248 object_layer);
249 }
250 return absl::StrFormat("obj 0x%03X size %d: no tiles drawn", object_id,
251 size);
252 }
253 std::string status = (size_mismatch || offset_mismatch) ? "MISMATCH" : "OK";
254 if (include_room_fields && has_room_context) {
255 return absl::StrFormat(
256 "room 0x%02X obj#%d (0x%03X size %d @%d,%d L%d): %s trace=%dx%d "
257 "offset=(%d,%d) expected=%dx%d expected_offset=(%d,%d)",
258 room_id, object_index, object_id, size, object_x, object_y,
259 object_layer, status, trace_width, trace_height, trace_offset_x,
260 trace_offset_y, expected_width, expected_height, expected_offset_x,
261 expected_offset_y);
262 }
263 return absl::StrFormat(
264 "obj 0x%03X size %d: %s trace=%dx%d min=(%d,%d) offset=(%d,%d) "
265 "expected=%dx%d expected_offset=(%d,%d)",
266 object_id, size, status, trace_width, trace_height, trace_min_x,
267 trace_min_y, trace_offset_x, trace_offset_y, expected_width,
268 expected_height, expected_offset_x, expected_offset_y);
269 }
270
271 std::string FormatJson(bool include_room_fields) const {
272 if (include_room_fields && has_room_context) {
273 return absl::StrFormat(
274 R"({"object_id":"0x%03X","size":%d,"has_tiles":%s,)"
275 R"("trace_width":%d,"trace_height":%d,"trace_min_x":%d,"trace_min_y":%d,)"
276 R"("trace_offset_x":%d,"trace_offset_y":%d,)"
277 R"("expected_width":%d,"expected_height":%d,"expected_offset_x":%d,"expected_offset_y":%d,)"
278 R"("room_id":%d,"object_index":%d,"object_x":%d,"object_y":%d,"object_layer":%d,)"
279 R"("size_mismatch":%s,"offset_mismatch":%s})",
280 object_id, size, has_tiles ? "true" : "false", trace_width,
281 trace_height, trace_min_x, trace_min_y, trace_offset_x,
282 trace_offset_y, expected_width, expected_height, expected_offset_x,
283 expected_offset_y, room_id, object_index, object_x, object_y,
284 object_layer, size_mismatch ? "true" : "false",
285 offset_mismatch ? "true" : "false");
286 }
287 return absl::StrFormat(
288 R"({"object_id":"0x%03X","size":%d,"has_tiles":%s,)"
289 R"("trace_width":%d,"trace_height":%d,"trace_min_x":%d,"trace_min_y":%d,)"
290 R"("trace_offset_x":%d,"trace_offset_y":%d,)"
291 R"("expected_width":%d,"expected_height":%d,"expected_offset_x":%d,"expected_offset_y":%d,)"
292 R"("size_mismatch":%s,"offset_mismatch":%s})",
293 object_id, size, has_tiles ? "true" : "false", trace_width,
294 trace_height, trace_min_x, trace_min_y, trace_offset_x, trace_offset_y,
295 expected_width, expected_height, expected_offset_x, expected_offset_y,
296 size_mismatch ? "true" : "false", offset_mismatch ? "true" : "false");
297 }
298};
299
301 std::string json_path;
302 std::string csv_path;
303};
304
306 int object_id = 0;
307 int size = 0;
308 bool has_room_context = false;
309 int room_id = -1;
310 int object_index = -1;
311 int object_x = 0;
312 int object_y = 0;
313 int object_layer = 0;
314 std::vector<zelda3::ObjectDrawer::TileTrace> tiles;
315};
316
317bool EndsWith(const std::string& value, const std::string& suffix) {
318 if (value.size() < suffix.size()) {
319 return false;
320 }
321 return value.compare(value.size() - suffix.size(), suffix.size(), suffix) ==
322 0;
323}
324
325ReportPaths ResolveReportPaths(const std::string& report_base) {
326 std::string base =
327 report_base.empty() ? "dungeon_object_validation_report" : report_base;
328 ReportPaths paths{};
329 if (EndsWith(base, ".json")) {
330 paths.json_path = base;
331 paths.csv_path = base.substr(0, base.size() - 5) + ".csv";
332 } else if (EndsWith(base, ".csv")) {
333 paths.csv_path = base;
334 paths.json_path = base.substr(0, base.size() - 4) + ".json";
335 } else {
336 paths.json_path = base + ".json";
337 paths.csv_path = base + ".csv";
338 }
339 return paths;
340}
341
342absl::Status WriteJsonReport(const ReportPaths& paths, bool include_room_fields,
343 int object_count, int size_cases, int test_cases,
344 int mismatch_count, int empty_traces,
345 int negative_offsets, int skipped_nothing,
346 const std::vector<ValidationResult>& mismatches) {
347 std::ofstream out(paths.json_path);
348 if (!out.is_open()) {
349 return absl::InternalError(
350 absl::StrFormat("Failed to open report file: %s", paths.json_path));
351 }
352
353 out << "{\n";
354 out << " \"summary\": {\n";
355 out << absl::StrFormat(" \"object_count\": %d,\n", object_count);
356 out << absl::StrFormat(" \"size_cases\": %d,\n", size_cases);
357 out << absl::StrFormat(" \"test_cases\": %d,\n", test_cases);
358 out << absl::StrFormat(" \"mismatch_count\": %d,\n", mismatch_count);
359 out << absl::StrFormat(" \"empty_traces\": %d,\n", empty_traces);
360 out << absl::StrFormat(" \"negative_offsets\": %d,\n", negative_offsets);
361 out << absl::StrFormat(" \"skipped_nothing\": %d\n", skipped_nothing);
362 out << " },\n";
363
364 out << " \"mismatches\": [\n";
365 for (size_t i = 0; i < mismatches.size(); ++i) {
366 out << " " << mismatches[i].FormatJson(include_room_fields);
367 if (i + 1 < mismatches.size()) {
368 out << ",";
369 }
370 out << "\n";
371 }
372 out << " ]\n";
373 out << "}\n";
374 return absl::OkStatus();
375}
376
377absl::Status WriteCsvReport(const ReportPaths& paths, bool include_room_fields,
378 const std::vector<ValidationResult>& mismatches) {
379 std::ofstream out(paths.csv_path);
380 if (!out.is_open()) {
381 return absl::InternalError(
382 absl::StrFormat("Failed to open report file: %s", paths.csv_path));
383 }
384
385 out << "category,object_id,size,has_tiles,trace_width,trace_height,trace_min_"
386 "x,trace_min_y,";
387 if (include_room_fields) {
388 out << "trace_offset_x,trace_offset_y,room_id,object_index,object_x,object_"
389 "y,object_layer,";
390 }
391 out << "expected_width,expected_height,expected_offset_x,expected_offset_y,"
392 "size_mismatch,offset_mismatch\n";
393 auto write_row = [&](const ValidationResult& result) {
394 const std::string category = result.has_tiles ? "mismatch" : "empty";
395 out << category << "," << absl::StrFormat("0x%03X", result.object_id) << ","
396 << result.size << "," << (result.has_tiles ? "true" : "false") << ","
397 << result.trace_width << "," << result.trace_height << ","
398 << result.trace_min_x << "," << result.trace_min_y << ",";
399 if (include_room_fields) {
400 out << result.trace_offset_x << "," << result.trace_offset_y << ","
401 << result.room_id << "," << result.object_index << ","
402 << result.object_x << "," << result.object_y << ","
403 << result.object_layer << ",";
404 }
405 out << "" << result.expected_width << "," << result.expected_height << ","
406 << result.expected_offset_x << "," << result.expected_offset_y << ","
407 << (result.size_mismatch ? "true" : "false") << ","
408 << (result.offset_mismatch ? "true" : "false") << "\n";
409 };
410
411 for (const auto& result : mismatches) {
412 write_row(result);
413 }
414
415 return absl::OkStatus();
416}
417
418std::vector<zelda3::ObjectDrawer::TileTrace> NormalizeTrace(
419 const std::vector<zelda3::ObjectDrawer::TileTrace>& trace) {
420 std::vector<zelda3::ObjectDrawer::TileTrace> normalized = trace;
421 std::sort(normalized.begin(), normalized.end(),
422 [](const zelda3::ObjectDrawer::TileTrace& left,
423 const zelda3::ObjectDrawer::TileTrace& right) {
424 if (left.y_tile != right.y_tile) {
425 return left.y_tile < right.y_tile;
426 }
427 if (left.x_tile != right.x_tile) {
428 return left.x_tile < right.x_tile;
429 }
430 if (left.tile_id != right.tile_id) {
431 return left.tile_id < right.tile_id;
432 }
433 if (left.layer != right.layer) {
434 return left.layer < right.layer;
435 }
436 return left.flags < right.flags;
437 });
438 return normalized;
439}
440
441absl::Status WriteTraceDump(const std::string& path, bool include_room_fields,
442 int empty_case_count,
443 const std::vector<TraceDumpCase>& cases) {
444 std::ofstream out(path);
445 if (!out.is_open()) {
446 return absl::InternalError(
447 absl::StrFormat("Failed to open trace dump file: %s", path));
448 }
449
450 out << "{\n";
451 out << absl::StrFormat(" \"case_count\": %d,\n",
452 static_cast<int>(cases.size()));
453 out << absl::StrFormat(" \"empty_case_count\": %d,\n", empty_case_count);
454 out << " \"cases\": [\n";
455
456 for (size_t i = 0; i < cases.size(); ++i) {
457 const auto& entry = cases[i];
458 out << " {\n";
459 out << absl::StrFormat(" \"object_id\": \"0x%03X\",\n",
460 entry.object_id);
461 out << absl::StrFormat(" \"object_id_dec\": %d,\n", entry.object_id);
462 out << absl::StrFormat(" \"size\": %d,\n", entry.size);
463 if (include_room_fields && entry.has_room_context) {
464 out << absl::StrFormat(" \"room_id\": %d,\n", entry.room_id);
465 out << absl::StrFormat(" \"object_index\": %d,\n",
466 entry.object_index);
467 out << absl::StrFormat(" \"object_x\": %d,\n", entry.object_x);
468 out << absl::StrFormat(" \"object_y\": %d,\n", entry.object_y);
469 out << absl::StrFormat(" \"object_layer\": %d,\n",
470 entry.object_layer);
471 }
472 out << " \"tiles\": [\n";
473
474 for (size_t t = 0; t < entry.tiles.size(); ++t) {
475 const auto& tile = entry.tiles[t];
476 out << absl::StrFormat(
477 " {\"x_tile\":%d,\"y_tile\":%d,"
478 "\"tile_id\":\"0x%04X\",\"tile_id_dec\":%d,"
479 "\"layer\":%d,\"flags\":%d}",
480 tile.x_tile, tile.y_tile, tile.tile_id, tile.tile_id, tile.layer,
481 tile.flags);
482 if (t + 1 < entry.tiles.size()) {
483 out << ",";
484 }
485 out << "\n";
486 }
487 out << " ]\n";
488 out << " }";
489 if (i + 1 < cases.size()) {
490 out << ",";
491 }
492 out << "\n";
493 }
494
495 out << " ]\n";
496 out << "}\n";
497 return absl::OkStatus();
498}
499
500} // namespace
501
502absl::Status DungeonObjectValidateCommandHandler::Execute(
503 Rom* rom, const resources::ArgumentParser& parser,
504 resources::OutputFormatter& formatter) {
505 auto object_arg = parser.GetInt("object");
506 auto size_arg = parser.GetInt("size");
507 auto room_arg = parser.GetInt("room");
508 auto report_arg = parser.GetString("report");
509 auto trace_out_arg = parser.GetString("trace-out");
510 bool verbose = parser.HasFlag("verbose");
511 const bool room_mode = room_arg.ok();
512 const int room_id = room_mode ? room_arg.value() : -1;
513 if (room_mode && (room_id < 0 || room_id >= kNumRooms)) {
514 return absl::InvalidArgumentError(
515 absl::StrFormat("Room ID must be between 0 and %d", kNumRooms - 1));
516 }
517
518 // Initialize ObjectDimensionTable so DimensionService's fallback path works.
519 auto& dimension_table = zelda3::ObjectDimensionTable::Get();
520 auto load_status = dimension_table.LoadFromRom(rom);
521 if (!load_status.ok()) {
522 return load_status;
523 }
524 auto& dim_service = zelda3::DimensionService::Get();
525
526 std::vector<int> object_ids = BuildObjectIds(object_arg);
527 std::vector<int> sizes = BuildSizes(size_arg);
528
529 zelda3::ObjectDrawer drawer(rom, 0, nullptr);
530 std::vector<zelda3::ObjectDrawer::TileTrace> trace;
531 drawer.SetTraceCollector(&trace, true);
532
533 gfx::BackgroundBuffer bg1(512, 512);
534 gfx::BackgroundBuffer bg2(512, 512);
535 gfx::PaletteGroup palette_group;
536
537 int total_tests = 0;
538 int mismatch_count = 0;
539 int empty_trace_count = 0;
540 int negative_offset_count = 0;
541 int skipped_nothing = 0;
542 int room_object_count = 0;
543 std::vector<ValidationResult> mismatches;
544 std::vector<ValidationResult> empty_traces;
545 std::vector<TraceDumpCase> trace_cases;
546
547 ReportPaths report_paths = ResolveReportPaths(
548 report_arg.has_value() ? report_arg.value() : std::string());
549 const bool write_trace_dump = trace_out_arg.has_value();
550 if (write_trace_dump) {
551 trace_cases.reserve(object_ids.size() * sizes.size());
552 }
553
554 bool prev_custom_objects = core::FeatureFlags::get().kEnableCustomObjects;
555 core::FeatureFlags::get().kEnableCustomObjects = false;
556
557 if (room_mode) {
558 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
559 room.LoadObjects();
560 const auto& room_objects = room.GetTileObjects();
561 room_object_count = static_cast<int>(room_objects.size());
562 if (write_trace_dump) {
563 trace_cases.reserve(room_objects.size());
564 }
565
566 for (size_t idx = 0; idx < room_objects.size(); ++idx) {
567 const auto& room_obj = room_objects[idx];
568 int object_id = room_obj.id_;
569 int routine_id = drawer.GetDrawRoutineId(object_id);
570 if (routine_id == zelda3::DrawRoutineIds::kNothing) {
571 skipped_nothing++;
572 continue;
573 }
574 total_tests++;
575 trace.clear();
576
577 zelda3::RoomObject obj = room_obj;
578 obj.SetRom(rom);
579 auto draw_status = drawer.DrawObject(obj, bg1, bg2, palette_group);
580 if (!draw_status.ok()) {
581 return draw_status;
582 }
583
584 auto expected_bounds = dim_service.GetDimensions(obj);
585 expected_bounds = detail::ClipSelectionBoundsToRoom(
586 object_id, obj.size_, expected_bounds, obj.x_, obj.y_);
587
588 TraceBounds bounds = ComputeBounds(trace);
589 if (write_trace_dump) {
590 TraceDumpCase trace_case{};
591 trace_case.object_id = object_id;
592 trace_case.size = obj.size_;
593 trace_case.has_room_context = true;
594 trace_case.room_id = room_id;
595 trace_case.object_index = static_cast<int>(idx);
596 trace_case.object_x = obj.x_;
597 trace_case.object_y = obj.y_;
598 trace_case.object_layer = obj.GetLayerValue();
599 trace_case.tiles = NormalizeTrace(trace);
600 trace_cases.push_back(std::move(trace_case));
601 }
602
603 ValidationResult result{};
604 result.object_id = object_id;
605 result.size = obj.size_;
606 result.has_room_context = true;
607 result.room_id = room_id;
608 result.object_index = static_cast<int>(idx);
609 result.object_x = obj.x_;
610 result.object_y = obj.y_;
611 result.object_layer = obj.GetLayerValue();
612 result.expected_width = expected_bounds.width_tiles;
613 result.expected_height = expected_bounds.height_tiles;
614 result.expected_offset_x = expected_bounds.offset_x_tiles;
615 result.expected_offset_y = expected_bounds.offset_y_tiles;
616
617 if (!bounds.has_tiles) {
618 empty_trace_count++;
619 result.has_tiles = false;
620 result.trace_width = 0;
621 result.trace_height = 0;
622 result.trace_min_x = 0;
623 result.trace_min_y = 0;
624 result.trace_offset_x = 0;
625 result.trace_offset_y = 0;
626 result.size_mismatch = true;
627 result.offset_mismatch = false;
628 mismatch_count++;
629 mismatches.push_back(result);
630 if (verbose) {
631 empty_traces.push_back(result);
632 }
633 continue;
634 }
635
636 result.has_tiles = true;
637 result.trace_width = bounds.width;
638 result.trace_height = bounds.height;
639 result.trace_min_x = bounds.min_x;
640 result.trace_min_y = bounds.min_y;
641 result.trace_offset_x = bounds.min_x - obj.x_;
642 result.trace_offset_y = bounds.min_y - obj.y_;
643 result.size_mismatch = (bounds.width != expected_bounds.width_tiles ||
644 bounds.height != expected_bounds.height_tiles);
645 result.offset_mismatch =
646 (result.trace_offset_x != expected_bounds.offset_x_tiles ||
647 result.trace_offset_y != expected_bounds.offset_y_tiles);
648
649 if (expected_bounds.offset_x_tiles < 0 ||
650 expected_bounds.offset_y_tiles < 0) {
651 negative_offset_count++;
652 }
653
654 if (result.size_mismatch || result.offset_mismatch) {
655 mismatch_count++;
656 mismatches.push_back(result);
657 }
658 }
659 } else {
660 for (int object_id : object_ids) {
661 int routine_id = drawer.GetDrawRoutineId(object_id);
662 if (routine_id == zelda3::DrawRoutineIds::kNothing) {
663 skipped_nothing++;
664 continue;
665 }
666 for (int size : sizes) {
667 total_tests++;
668 trace.clear();
669
670 // Use a temporary object at origin (0,0) to get initial dimensions,
671 // then choose a safe origin that keeps bounds within the room canvas.
672 zelda3::RoomObject temp_obj(object_id, 0, 0, static_cast<uint8_t>(size),
673 0);
674 auto expected_bounds = dim_service.GetDimensions(temp_obj);
675 const auto [origin_x, origin_y] =
676 ChooseOriginForExpectedBounds(expected_bounds);
677
678 zelda3::RoomObject obj(object_id, origin_x, origin_y,
679 static_cast<uint8_t>(size), 0);
680 obj.layer_ = zelda3::RoomObject::LayerType::BG1;
681
682 // Re-query with the positioned object so DimensionService sees the
683 // final coordinates, then apply room-boundary clipping.
684 expected_bounds = dim_service.GetDimensions(obj);
685 expected_bounds = detail::ClipSelectionBoundsToRoom(
686 object_id, size, expected_bounds, obj.x_, obj.y_);
687
688 if (expected_bounds.offset_x_tiles < 0 ||
689 expected_bounds.offset_y_tiles < 0) {
690 negative_offset_count++;
691 }
692
693 auto draw_status = drawer.DrawObject(obj, bg1, bg2, palette_group);
694 if (!draw_status.ok()) {
695 return draw_status;
696 }
697
698 TraceBounds bounds = ComputeBounds(trace);
699 if (write_trace_dump) {
700 TraceDumpCase trace_case{};
701 trace_case.object_id = object_id;
702 trace_case.size = size;
703 trace_case.tiles = NormalizeTrace(trace);
704 trace_cases.push_back(std::move(trace_case));
705 }
706 if (!bounds.has_tiles) {
707 empty_trace_count++;
708 ValidationResult result{};
709 result.object_id = object_id;
710 result.size = size;
711 result.has_tiles = false;
712 result.trace_width = 0;
713 result.trace_height = 0;
714 result.trace_min_x = 0;
715 result.trace_min_y = 0;
716 result.expected_width = expected_bounds.width_tiles;
717 result.expected_height = expected_bounds.height_tiles;
718 result.expected_offset_x = expected_bounds.offset_x_tiles;
719 result.expected_offset_y = expected_bounds.offset_y_tiles;
720 result.size_mismatch = true;
721 result.offset_mismatch = false;
722 mismatch_count++;
723 mismatches.push_back(result);
724 if (verbose) {
725 empty_traces.push_back(result);
726 }
727 continue;
728 }
729
730 ValidationResult result{};
731 result.object_id = object_id;
732 result.size = size;
733 result.has_tiles = true;
734 result.trace_width = bounds.width;
735 result.trace_height = bounds.height;
736 result.trace_min_x = bounds.min_x;
737 result.trace_min_y = bounds.min_y;
738 result.trace_offset_x = bounds.min_x - obj.x_;
739 result.trace_offset_y = bounds.min_y - obj.y_;
740 result.expected_width = expected_bounds.width_tiles;
741 result.expected_height = expected_bounds.height_tiles;
742 result.expected_offset_x = expected_bounds.offset_x_tiles;
743 result.expected_offset_y = expected_bounds.offset_y_tiles;
744 result.size_mismatch = (bounds.width != expected_bounds.width_tiles ||
745 bounds.height != expected_bounds.height_tiles);
746 result.offset_mismatch =
747 (result.trace_offset_x != expected_bounds.offset_x_tiles ||
748 result.trace_offset_y != expected_bounds.offset_y_tiles);
749
750 if (result.size_mismatch || result.offset_mismatch) {
751 mismatch_count++;
752 mismatches.push_back(result);
753 }
754 }
755 }
756 }
757
758 core::FeatureFlags::get().kEnableCustomObjects = prev_custom_objects;
759
760 const int object_count =
761 room_mode ? room_object_count : static_cast<int>(object_ids.size());
762 auto json_status =
763 WriteJsonReport(report_paths, room_mode, object_count,
764 room_mode ? 1 : static_cast<int>(sizes.size()),
765 total_tests, mismatch_count, empty_trace_count,
766 negative_offset_count, skipped_nothing, mismatches);
767 if (!json_status.ok()) {
768 return json_status;
769 }
770
771 auto csv_status = WriteCsvReport(report_paths, room_mode, mismatches);
772 if (!csv_status.ok()) {
773 return csv_status;
774 }
775
776 if (write_trace_dump) {
777 auto trace_status = WriteTraceDump(trace_out_arg.value(), room_mode,
778 empty_trace_count, trace_cases);
779 if (!trace_status.ok()) {
780 return trace_status;
781 }
782 formatter.AddField("trace_dump", trace_out_arg.value());
783 }
784
785 formatter.AddField("object_count", object_count);
786 formatter.AddField("size_cases",
787 room_mode ? 1 : static_cast<int>(sizes.size()));
788 formatter.AddField("test_cases", total_tests);
789 formatter.AddField("mismatch_count", mismatch_count);
790 formatter.AddField("empty_traces", empty_trace_count);
791 formatter.AddField("negative_offsets", negative_offset_count);
792 formatter.AddField("skipped_nothing", skipped_nothing);
793 if (room_mode) {
794 formatter.AddField("room_id", room_id);
795 }
796 formatter.AddField("report_json", report_paths.json_path);
797 formatter.AddField("report_csv", report_paths.csv_path);
798
799 formatter.BeginArray("mismatches");
800 for (const auto& result : mismatches) {
801 formatter.AddArrayItem(formatter.IsJson() ? result.FormatJson(room_mode)
802 : result.FormatText(room_mode));
803 }
804 formatter.EndArray();
805
806 if (verbose) {
807 formatter.BeginArray("empty_trace_objects");
808 for (const auto& result : empty_traces) {
809 formatter.AddArrayItem(formatter.IsJson() ? result.FormatJson(room_mode)
810 : result.FormatText(room_mode));
811 }
812 formatter.EndArray();
813 }
814
815 return absl::OkStatus();
816}
817
818} // namespace yaze::cli
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
static DimensionService & Get()
DimensionResult GetDimensions(const RoomObject &obj) const
Draws dungeon objects to background buffers using game patterns.
int GetDrawRoutineId(int16_t object_id) const
Get draw routine ID for an object.
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr)
Draw a room object to background buffers.
void SetTraceCollector(std::vector< TileTrace > *collector, bool trace_only=false)
void SetRom(Rom *rom)
Definition room_object.h:70
uint8_t GetLayerValue() const
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:314
void LoadObjects()
Definition room.cc:1292
absl::Status WriteTraceDump(const std::string &path, bool include_room_fields, int empty_case_count, const std::vector< TraceDumpCase > &cases)
absl::Status WriteJsonReport(const ReportPaths &paths, bool include_room_fields, int object_count, int size_cases, int test_cases, int mismatch_count, int empty_traces, int negative_offsets, int skipped_nothing, const std::vector< ValidationResult > &mismatches)
std::vector< int > BuildObjectIds(const absl::StatusOr< int > &object_arg)
std::vector< int > BuildSizes(const absl::StatusOr< int > &size_arg)
std::pair< int, int > ChooseOriginForExpectedBounds(const zelda3::DimensionService::DimensionResult &expected_bounds)
absl::Status WriteCsvReport(const ReportPaths &paths, bool include_room_fields, const std::vector< ValidationResult > &mismatches)
std::vector< zelda3::ObjectDrawer::TileTrace > NormalizeTrace(const std::vector< zelda3::ObjectDrawer::TileTrace > &trace)
TraceBounds ComputeBounds(const std::vector< zelda3::ObjectDrawer::TileTrace > &trace)
zelda3::DimensionService::DimensionResult ClipSelectionBoundsToRoom(int object_id, int size, const zelda3::DimensionService::DimensionResult &bounds, int object_x, int object_y)
Namespace for the command line interface.
Represents a group of palettes.
static constexpr int kMaxTilesY
static constexpr int kMaxTilesX