yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_validate_commands.cc
Go to the documentation of this file.
2
3#include <iostream>
4#include <vector>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
10#include "rom/rom.h"
12
13namespace yaze::cli {
14
15namespace {
16
17// =============================================================================
18// Map Validation Result
19// =============================================================================
20
22 int map_id = 0;
23 bool pointers_valid = false;
24 bool decompress_low_ok = false;
25 bool decompress_high_ok = false;
26 uint32_t snes_low = 0;
27 uint32_t snes_high = 0;
28 uint32_t pc_low = 0;
29 uint32_t pc_high = 0;
30 std::string error;
31 bool skipped = false;
32
33 std::string FormatJson() const {
34 return absl::StrFormat(
35 R"({"map_id":%d,"snes_low":"0x%06X","snes_high":"0x%06X",)"
36 R"("pc_low":"0x%06X","pc_high":"0x%06X","pointers_valid":%s,)"
37 R"("decomp_low":%s,"decomp_high":%s,"error":"%s","skipped":%s})",
38 map_id, snes_low, snes_high, pc_low, pc_high,
39 pointers_valid ? "true" : "false", decompress_low_ok ? "true" : "false",
40 decompress_high_ok ? "true" : "false", error,
41 skipped ? "true" : "false");
42 }
43
44 std::string FormatText() const {
45 if (skipped) {
46 return absl::StrFormat("map 0x%02X: skipped (%s)", map_id, error);
47 }
48 std::string status =
49 (pointers_valid && decompress_low_ok && decompress_high_ok) ? "OK"
50 : "FAIL";
51 return absl::StrFormat(
52 "map 0x%02X: %s snes_low=0x%06X snes_high=0x%06X ptr=%s dec_low=%s "
53 "dec_high=%s %s",
54 map_id, status, snes_low, snes_high, pointers_valid ? "OK" : "BAD",
55 decompress_low_ok ? "OK" : "BAD", decompress_high_ok ? "OK" : "BAD",
56 error);
57 }
58
59 bool IsValid() const {
60 return !skipped && pointers_valid && decompress_low_ok &&
61 decompress_high_ok;
62 }
63};
64
66 int total_maps = 0;
67 int valid_maps = 0;
68 int invalid_maps = 0;
69 int skipped_maps = 0;
70 int pointer_failures = 0;
71 int decompress_failures = 0;
72};
73
74// =============================================================================
75// Address Conversion
76// =============================================================================
77
78uint32_t SnesToPcLoRom(uint32_t snes_addr) {
79 return ((snes_addr & 0x7F0000) >> 1) | (snes_addr & 0x7FFF);
80}
81
82// =============================================================================
83// Validation Logic
84// =============================================================================
85
86absl::StatusOr<MapValidationResult> ValidateMapPointers(
87 const zelda3::Overworld& overworld, int map_id, bool include_tail) {
88 MapValidationResult result{};
89 result.map_id = map_id;
90
91 // Skip tail maps unless explicitly requested
92 if (!include_tail && map_id >= zelda3::kSpecialWorldMapIdStart + 0x20) {
93 result.skipped = true;
94 result.error = "tail disabled";
95 return result;
96 }
97
98 const auto ptr_low_base =
100 const auto ptr_high_base =
102
103 auto read_ptr = [&](uint32_t base) -> uint32_t {
104 uint8_t byte0 = overworld.rom()->data()[base + (3 * map_id)];
105 uint8_t byte1 = overworld.rom()->data()[base + (3 * map_id) + 1];
106 uint8_t byte2 = overworld.rom()->data()[base + (3 * map_id) + 2];
107 return (byte2 << 16) | (byte1 << 8) | byte0;
108 };
109
110 result.snes_low = read_ptr(ptr_low_base);
111 result.snes_high = read_ptr(ptr_high_base);
112 result.pc_low = SnesToPcLoRom(result.snes_low);
113 result.pc_high = SnesToPcLoRom(result.snes_high);
114
115 // Basic bounds check
116 result.pointers_valid = result.pc_low < overworld.rom()->size() &&
117 result.pc_high < overworld.rom()->size();
118
119 auto try_decompress = [&](uint32_t pc_addr) -> bool {
120 if (pc_addr >= overworld.rom()->size()) {
121 return false;
122 }
123 int size = 0;
124 auto buf =
125 gfx::HyruleMagicDecompress(overworld.rom()->data() + pc_addr, &size, 1);
126 return !buf.empty();
127 };
128
129 result.decompress_low_ok = try_decompress(result.pc_low);
130 result.decompress_high_ok = try_decompress(result.pc_high);
131
132 if (!result.pointers_valid) {
133 result.error = "pointer out of bounds";
134 } else if (!result.decompress_low_ok || !result.decompress_high_ok) {
135 result.error = "decompression failed";
136 }
137
138 return result;
139}
140
141// =============================================================================
142// Tile16 Validation
143// =============================================================================
144
146 bool uses_expanded = false;
147 int suspicious_count = 0;
148};
149
152
153 // Check if ROM uses expanded tile16
154 uint8_t expanded_flag = rom->data()[kMap16ExpandedFlagPos];
155 result.uses_expanded = (expanded_flag != 0x0F);
156
157 if (!result.uses_expanded) {
158 return result;
159 }
160
161 // Count suspicious tiles in expansion region
162 for (int tile = kNumTile16Vanilla; tile < kNumTile16Expanded; ++tile) {
163 uint32_t addr = kMap16TilesExpanded + (tile * 8);
164 if (addr + 8 > rom->size()) {
165 break;
166 }
167
168 bool all_same = true;
169 uint16_t first_val = rom->data()[addr] | (rom->data()[addr + 1] << 8);
170 for (int i = 1; i < 4; ++i) {
171 uint16_t val =
172 rom->data()[addr + i * 2] | (rom->data()[addr + i * 2 + 1] << 8);
173 if (val != first_val) {
174 all_same = false;
175 break;
176 }
177 }
178
179 if (all_same && first_val != 0x0000 && first_val != 0xFFFF) {
180 result.suspicious_count++;
181 }
182 }
183
184 return result;
185}
186
187} // namespace
188
190 Rom* rom, const resources::ArgumentParser& parser,
191 resources::OutputFormatter& formatter) {
192 bool include_tail = parser.HasFlag("include-tail");
193 bool check_tile16 = parser.HasFlag("check-tile16");
194 bool verbose = parser.HasFlag("verbose");
195 bool is_json = formatter.IsJson();
196
197 zelda3::Overworld overworld(rom);
198 RETURN_IF_ERROR(overworld.Load(rom));
199
200 // Validate maps
201 int max_map = include_tail ? 0xC0 : zelda3::kNumOverworldMaps;
202 std::vector<MapValidationResult> results;
203 results.reserve(max_map);
204
205 ValidationSummary summary;
206 summary.total_maps = max_map;
207
208 for (int i = 0; i < max_map; ++i) {
209 ASSIGN_OR_RETURN(auto result,
210 ValidateMapPointers(overworld, i, include_tail));
211 results.push_back(result);
212
213 if (result.skipped) {
214 summary.skipped_maps++;
215 } else if (result.IsValid()) {
216 summary.valid_maps++;
217 } else {
218 summary.invalid_maps++;
219 if (!result.pointers_valid) {
220 summary.pointer_failures++;
221 }
222 if (!result.decompress_low_ok || !result.decompress_high_ok) {
223 summary.decompress_failures++;
224 }
225 }
226 }
227
228 // Output maps array
229 formatter.BeginArray("maps");
230 for (const auto& result : results) {
231 if (is_json) {
232 formatter.AddArrayItem(result.FormatJson());
233 } else if (verbose || !result.IsValid()) {
234 // In text mode, only show failures unless verbose
235 formatter.AddArrayItem(result.FormatText());
236 }
237 }
238 formatter.EndArray();
239
240 // Output summary
241 formatter.AddField("total_maps", summary.total_maps);
242 formatter.AddField("valid_maps", summary.valid_maps);
243 formatter.AddField("invalid_maps", summary.invalid_maps);
244 formatter.AddField("skipped_maps", summary.skipped_maps);
245 formatter.AddField("pointer_failures", summary.pointer_failures);
246 formatter.AddField("decompress_failures", summary.decompress_failures);
247
248 // Tile16 validation
249 if (check_tile16) {
250 auto tile16_result = ValidateTile16Region(rom);
251 formatter.AddField("tile16_uses_expanded", tile16_result.uses_expanded);
252
253 if (tile16_result.uses_expanded) {
254 formatter.AddField("tile16_suspicious_count",
255 tile16_result.suspicious_count);
256 }
257 }
258
259 // Text mode summary
260 if (!is_json) {
261 std::cout << "\n=== Validation Summary ===\n";
262 std::cout << absl::StrFormat(" Total maps: %d\n", summary.total_maps);
263 std::cout << absl::StrFormat(" Valid: %d\n", summary.valid_maps);
264 std::cout << absl::StrFormat(" Invalid: %d\n", summary.invalid_maps);
265 std::cout << absl::StrFormat(" Skipped: %d\n", summary.skipped_maps);
266 if (summary.invalid_maps > 0) {
267 std::cout << absl::StrFormat(" Pointer failures: %d\n",
268 summary.pointer_failures);
269 std::cout << absl::StrFormat(" Decompress failures: %d\n",
270 summary.decompress_failures);
271 }
272 }
273
274 return absl::OkStatus();
275}
276
277} // 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:49
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
bool HasFlag(const std::string &name) const
Check if a flag is present.
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.
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:136
zelda3_version_pointers version_constants() const
Get version-specific ROM addresses.
Definition overworld.h:416
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
constexpr uint32_t kMap16ExpandedFlagPos
constexpr uint32_t kMap16TilesExpanded
constexpr int kNumTile16Vanilla
constexpr int kNumTile16Expanded
std::vector< uint8_t > HyruleMagicDecompress(uint8_t const *src, int *const size, int const p_big_endian, size_t max_src_size)
constexpr int kSpecialWorldMapIdStart
constexpr int kNumOverworldMaps
Definition common.h:85
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
uint32_t kCompressedAllMap32PointersHigh
Definition zelda.h:96
uint32_t kCompressedAllMap32PointersLow
Definition zelda.h:97