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"
9#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",
40 decompress_low_ok ? "true" : "false",
41 decompress_high_ok ? "true" : "false", error,
42 skipped ? "true" : "false");
43 }
44
45 std::string FormatText() const {
46 if (skipped) {
47 return absl::StrFormat("map 0x%02X: skipped (%s)", map_id, error);
48 }
49 std::string status = (pointers_valid && decompress_low_ok && decompress_high_ok)
50 ? "OK"
51 : "FAIL";
52 return absl::StrFormat(
53 "map 0x%02X: %s snes_low=0x%06X snes_high=0x%06X ptr=%s dec_low=%s "
54 "dec_high=%s %s",
55 map_id, status, snes_low, snes_high,
56 pointers_valid ? "OK" : "BAD",
57 decompress_low_ok ? "OK" : "BAD",
58 decompress_high_ok ? "OK" : "BAD", error);
59 }
60
61 bool IsValid() const {
62 return !skipped && pointers_valid && decompress_low_ok && decompress_high_ok;
63 }
64};
65
67 int total_maps = 0;
68 int valid_maps = 0;
69 int invalid_maps = 0;
70 int skipped_maps = 0;
71 int pointer_failures = 0;
72 int decompress_failures = 0;
73};
74
75// =============================================================================
76// Address Conversion
77// =============================================================================
78
79uint32_t SnesToPcLoRom(uint32_t snes_addr) {
80 return ((snes_addr & 0x7F0000) >> 1) | (snes_addr & 0x7FFF);
81}
82
83// =============================================================================
84// Validation Logic
85// =============================================================================
86
87absl::StatusOr<MapValidationResult> ValidateMapPointers(
88 const zelda3::Overworld& overworld, int map_id, bool include_tail) {
89 MapValidationResult result{};
90 result.map_id = map_id;
91
92 // Skip tail maps unless explicitly requested
93 if (!include_tail && map_id >= zelda3::kSpecialWorldMapIdStart + 0x20) {
94 result.skipped = true;
95 result.error = "tail disabled";
96 return result;
97 }
98
99 const auto ptr_low_base =
101 const auto ptr_high_base =
103
104 auto read_ptr = [&](uint32_t base) -> uint32_t {
105 uint8_t byte0 = overworld.rom()->data()[base + (3 * map_id)];
106 uint8_t byte1 = overworld.rom()->data()[base + (3 * map_id) + 1];
107 uint8_t byte2 = overworld.rom()->data()[base + (3 * map_id) + 2];
108 return (byte2 << 16) | (byte1 << 8) | byte0;
109 };
110
111 result.snes_low = read_ptr(ptr_low_base);
112 result.snes_high = read_ptr(ptr_high_base);
113 result.pc_low = SnesToPcLoRom(result.snes_low);
114 result.pc_high = SnesToPcLoRom(result.snes_high);
115
116 // Basic bounds check
117 result.pointers_valid = result.pc_low < overworld.rom()->size() &&
118 result.pc_high < overworld.rom()->size();
119
120 auto try_decompress = [&](uint32_t pc_addr) -> bool {
121 if (pc_addr >= overworld.rom()->size()) {
122 return false;
123 }
124 int size = 0;
125 auto buf =
126 gfx::HyruleMagicDecompress(overworld.rom()->data() + pc_addr, &size, 1);
127 return !buf.empty();
128 };
129
130 result.decompress_low_ok = try_decompress(result.pc_low);
131 result.decompress_high_ok = try_decompress(result.pc_high);
132
133 if (!result.pointers_valid) {
134 result.error = "pointer out of bounds";
135 } else if (!result.decompress_low_ok || !result.decompress_high_ok) {
136 result.error = "decompression failed";
137 }
138
139 return result;
140}
141
142// =============================================================================
143// Tile16 Validation
144// =============================================================================
145
147 bool uses_expanded = false;
148 int suspicious_count = 0;
149 std::vector<uint32_t> problem_addresses;
150};
151
154
155 // Check if ROM uses expanded tile16
156 uint8_t expanded_flag = rom->data()[kMap16ExpandedFlagPos];
157 result.uses_expanded = (expanded_flag != 0x0F);
158
159 if (!result.uses_expanded) {
160 return result;
161 }
162
163 // Check known problem addresses
164 for (uint32_t addr : kProblemAddresses) {
165 if (addr >= kMap16TilesExpanded && addr < kMap16TilesExpandedEnd) {
166 uint8_t sample[16] = {0};
167 for (int i = 0; i < 16 && addr + i < rom->size(); ++i) {
168 sample[i] = rom->data()[addr + i];
169 }
170
171 bool looks_valid = true;
172 for (int i = 0; i < 16; i += 2) {
173 uint16_t val = sample[i] | (sample[i + 1] << 8);
174 if ((val & 0x1F00) != 0 && (val & 0xE000) == 0) {
175 looks_valid = false;
176 }
177 }
178
179 if (!looks_valid) {
180 result.problem_addresses.push_back(addr);
181 }
182 }
183 }
184
185 // Count suspicious tiles in expansion region
186 for (int tile = kNumTile16Vanilla; tile < kNumTile16Expanded; ++tile) {
187 uint32_t addr = kMap16TilesExpanded + (tile * 8);
188 if (addr + 8 > rom->size()) {
189 break;
190 }
191
192 bool all_same = true;
193 uint16_t first_val = rom->data()[addr] | (rom->data()[addr + 1] << 8);
194 for (int i = 1; i < 4; ++i) {
195 uint16_t val =
196 rom->data()[addr + i * 2] | (rom->data()[addr + i * 2 + 1] << 8);
197 if (val != first_val) {
198 all_same = false;
199 break;
200 }
201 }
202
203 if (all_same && first_val != 0x0000 && first_val != 0xFFFF) {
204 result.suspicious_count++;
205 }
206 }
207
208 return result;
209}
210
211} // namespace
212
214 Rom* rom, const resources::ArgumentParser& parser,
215 resources::OutputFormatter& formatter) {
216 bool include_tail = parser.HasFlag("include-tail");
217 bool check_tile16 = parser.HasFlag("check-tile16");
218 bool verbose = parser.HasFlag("verbose");
219 bool is_json = formatter.IsJson();
220
221 zelda3::Overworld overworld(rom);
222 RETURN_IF_ERROR(overworld.Load(rom));
223
224 // Validate maps
225 int max_map = include_tail ? 0xC0 : zelda3::kNumOverworldMaps;
226 std::vector<MapValidationResult> results;
227 results.reserve(max_map);
228
229 ValidationSummary summary;
230 summary.total_maps = max_map;
231
232 for (int i = 0; i < max_map; ++i) {
233 ASSIGN_OR_RETURN(auto result,
234 ValidateMapPointers(overworld, i, include_tail));
235 results.push_back(result);
236
237 if (result.skipped) {
238 summary.skipped_maps++;
239 } else if (result.IsValid()) {
240 summary.valid_maps++;
241 } else {
242 summary.invalid_maps++;
243 if (!result.pointers_valid) {
244 summary.pointer_failures++;
245 }
246 if (!result.decompress_low_ok || !result.decompress_high_ok) {
247 summary.decompress_failures++;
248 }
249 }
250 }
251
252 // Output maps array
253 formatter.BeginArray("maps");
254 for (const auto& result : results) {
255 if (is_json) {
256 formatter.AddArrayItem(result.FormatJson());
257 } else if (verbose || !result.IsValid()) {
258 // In text mode, only show failures unless verbose
259 formatter.AddArrayItem(result.FormatText());
260 }
261 }
262 formatter.EndArray();
263
264 // Output summary
265 formatter.AddField("total_maps", summary.total_maps);
266 formatter.AddField("valid_maps", summary.valid_maps);
267 formatter.AddField("invalid_maps", summary.invalid_maps);
268 formatter.AddField("skipped_maps", summary.skipped_maps);
269 formatter.AddField("pointer_failures", summary.pointer_failures);
270 formatter.AddField("decompress_failures", summary.decompress_failures);
271
272 // Tile16 validation
273 if (check_tile16) {
274 auto tile16_result = ValidateTile16Region(rom);
275 formatter.AddField("tile16_uses_expanded", tile16_result.uses_expanded);
276
277 if (tile16_result.uses_expanded) {
278 formatter.AddField("tile16_problem_addresses",
279 static_cast<int>(tile16_result.problem_addresses.size()));
280 formatter.AddField("tile16_suspicious_count",
281 tile16_result.suspicious_count);
282
283 if (!is_json && !tile16_result.problem_addresses.empty()) {
284 std::cout << "\n=== Tile16 Problems ===\n";
285 for (uint32_t addr : tile16_result.problem_addresses) {
286 int tile_idx = (addr - kMap16TilesExpanded) / 8;
287 std::cout << absl::StrFormat(" 0x%06X (tile16 #%d): SUSPICIOUS\n",
288 addr, tile_idx);
289 }
290 }
291 }
292 }
293
294 // Text mode summary
295 if (!is_json) {
296 std::cout << "\n=== Validation Summary ===\n";
297 std::cout << absl::StrFormat(" Total maps: %d\n", summary.total_maps);
298 std::cout << absl::StrFormat(" Valid: %d\n", summary.valid_maps);
299 std::cout << absl::StrFormat(" Invalid: %d\n", summary.invalid_maps);
300 std::cout << absl::StrFormat(" Skipped: %d\n", summary.skipped_maps);
301 if (summary.invalid_maps > 0) {
302 std::cout << absl::StrFormat(" Pointer failures: %d\n",
303 summary.pointer_failures);
304 std::cout << absl::StrFormat(" Decompress failures: %d\n",
305 summary.decompress_failures);
306 }
307 }
308
309 return absl::OkStatus();
310}
311
312} // 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:24
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
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.
Represents the full Overworld data, light and dark world.
Definition overworld.h:217
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:36
zelda3_version_pointers version_constants() const
Get version-specific ROM addresses.
Definition overworld.h:225
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
Namespace for the command line interface.
constexpr uint32_t kMap16ExpandedFlagPos
const uint32_t kProblemAddresses[]
constexpr uint32_t kMap16TilesExpanded
constexpr uint32_t kMap16TilesExpandedEnd
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