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 std::vector<uint32_t> problem_addresses;
149};
150
153
154 // Check if ROM uses expanded tile16
155 uint8_t expanded_flag = rom->data()[kMap16ExpandedFlagPos];
156 result.uses_expanded = (expanded_flag != 0x0F);
157
158 if (!result.uses_expanded) {
159 return result;
160 }
161
162 // Check known problem addresses
163 for (uint32_t addr : kProblemAddresses) {
164 if (addr >= kMap16TilesExpanded && addr < kMap16TilesExpandedEnd) {
165 uint8_t sample[16] = {0};
166 for (int i = 0; i < 16 && addr + i < rom->size(); ++i) {
167 sample[i] = rom->data()[addr + i];
168 }
169
170 bool looks_valid = true;
171 for (int i = 0; i < 16; i += 2) {
172 uint16_t val = sample[i] | (sample[i + 1] << 8);
173 if ((val & 0x1F00) != 0 && (val & 0xE000) == 0) {
174 looks_valid = false;
175 }
176 }
177
178 if (!looks_valid) {
179 result.problem_addresses.push_back(addr);
180 }
181 }
182 }
183
184 // Count suspicious tiles in expansion region
185 for (int tile = kNumTile16Vanilla; tile < kNumTile16Expanded; ++tile) {
186 uint32_t addr = kMap16TilesExpanded + (tile * 8);
187 if (addr + 8 > rom->size()) {
188 break;
189 }
190
191 bool all_same = true;
192 uint16_t first_val = rom->data()[addr] | (rom->data()[addr + 1] << 8);
193 for (int i = 1; i < 4; ++i) {
194 uint16_t val =
195 rom->data()[addr + i * 2] | (rom->data()[addr + i * 2 + 1] << 8);
196 if (val != first_val) {
197 all_same = false;
198 break;
199 }
200 }
201
202 if (all_same && first_val != 0x0000 && first_val != 0xFFFF) {
203 result.suspicious_count++;
204 }
205 }
206
207 return result;
208}
209
210} // namespace
211
213 Rom* rom, const resources::ArgumentParser& parser,
214 resources::OutputFormatter& formatter) {
215 bool include_tail = parser.HasFlag("include-tail");
216 bool check_tile16 = parser.HasFlag("check-tile16");
217 bool verbose = parser.HasFlag("verbose");
218 bool is_json = formatter.IsJson();
219
220 zelda3::Overworld overworld(rom);
221 RETURN_IF_ERROR(overworld.Load(rom));
222
223 // Validate maps
224 int max_map = include_tail ? 0xC0 : zelda3::kNumOverworldMaps;
225 std::vector<MapValidationResult> results;
226 results.reserve(max_map);
227
228 ValidationSummary summary;
229 summary.total_maps = max_map;
230
231 for (int i = 0; i < max_map; ++i) {
232 ASSIGN_OR_RETURN(auto result,
233 ValidateMapPointers(overworld, i, include_tail));
234 results.push_back(result);
235
236 if (result.skipped) {
237 summary.skipped_maps++;
238 } else if (result.IsValid()) {
239 summary.valid_maps++;
240 } else {
241 summary.invalid_maps++;
242 if (!result.pointers_valid) {
243 summary.pointer_failures++;
244 }
245 if (!result.decompress_low_ok || !result.decompress_high_ok) {
246 summary.decompress_failures++;
247 }
248 }
249 }
250
251 // Output maps array
252 formatter.BeginArray("maps");
253 for (const auto& result : results) {
254 if (is_json) {
255 formatter.AddArrayItem(result.FormatJson());
256 } else if (verbose || !result.IsValid()) {
257 // In text mode, only show failures unless verbose
258 formatter.AddArrayItem(result.FormatText());
259 }
260 }
261 formatter.EndArray();
262
263 // Output summary
264 formatter.AddField("total_maps", summary.total_maps);
265 formatter.AddField("valid_maps", summary.valid_maps);
266 formatter.AddField("invalid_maps", summary.invalid_maps);
267 formatter.AddField("skipped_maps", summary.skipped_maps);
268 formatter.AddField("pointer_failures", summary.pointer_failures);
269 formatter.AddField("decompress_failures", summary.decompress_failures);
270
271 // Tile16 validation
272 if (check_tile16) {
273 auto tile16_result = ValidateTile16Region(rom);
274 formatter.AddField("tile16_uses_expanded", tile16_result.uses_expanded);
275
276 if (tile16_result.uses_expanded) {
277 formatter.AddField(
278 "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