yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
custom_collision.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <limits>
5#include <optional>
6#include <string>
7#include <unordered_map>
8#include <utility>
9#include <vector>
10
11#if defined(YAZE_WITH_JSON)
12#include "nlohmann/json.hpp"
13#endif
14
15#include "absl/status/status.h"
16#include "absl/strings/str_format.h"
17#include "rom/snes.h"
19#include "zelda3/dungeon/room.h"
20
21namespace yaze {
22namespace zelda3 {
23
24namespace {
25constexpr int kCollisionMapWidth = 64;
26constexpr int kCollisionMapHeight = 64;
27constexpr uint16_t kCollisionSingleTileMarker = 0xF0F0;
28constexpr uint16_t kCollisionEndMarker = 0xFFFF;
30} // namespace
31
32absl::StatusOr<CustomCollisionMap> LoadCustomCollisionMap(Rom* rom,
33 int room_id) {
34 if (!rom || !rom->is_loaded()) {
35 return absl::InvalidArgumentError("ROM not loaded");
36 }
37 if (room_id < 0 || room_id >= kNumberOfRooms) {
38 return absl::OutOfRangeError("Room id out of range");
39 }
40
41 const auto& data = rom->vector();
42 const int pointer_offset = kCustomCollisionRoomPointers + (room_id * 3);
43 if (pointer_offset < 0 ||
44 pointer_offset + 2 >= static_cast<int>(data.size())) {
45 return absl::OutOfRangeError("Collision pointer table out of range");
46 }
47
48 // Oracle of Secrets: reserve a tail region for the WaterFill table.
49 // If the reserved region exists in this ROM, refuse to load collision blobs
50 // that overlap it (corrupted pointer table / missing terminator).
51 const bool has_water_fill_reserved_region =
52 (kWaterFillTableEnd <= static_cast<int>(data.size()));
53 const size_t collision_safe_end =
54 has_water_fill_reserved_region
55 ? std::min(static_cast<size_t>(data.size()),
56 static_cast<size_t>(kCustomCollisionDataSoftEnd))
57 : static_cast<size_t>(data.size());
58
59 uint32_t snes_ptr = data[pointer_offset] | (data[pointer_offset + 1] << 8) |
60 (data[pointer_offset + 2] << 16);
61
62 CustomCollisionMap result;
63 result.tiles.fill(0);
64
65 if (snes_ptr == 0) {
66 return result;
67 }
68 result.has_data = true;
69
70 int pc_ptr = SnesToPc(snes_ptr);
71 if (pc_ptr < 0 || pc_ptr >= static_cast<int>(data.size())) {
72 return absl::OutOfRangeError("Collision data pointer out of range");
73 }
74 if (static_cast<size_t>(pc_ptr) >= collision_safe_end) {
75 return absl::FailedPreconditionError(
76 absl::StrFormat("Collision data for room 0x%02X overlaps WaterFill "
77 "reserved region (pc=0x%06X)",
78 room_id, pc_ptr));
79 }
80
81 size_t cursor = static_cast<size_t>(pc_ptr);
82 bool single_tiles_mode = false;
83 bool found_end_marker = false;
84
85 while (cursor + 1 < collision_safe_end) {
86 uint16_t offset = data[cursor] | (data[cursor + 1] << 8);
87 cursor += 2;
88
89 if (offset == kCollisionEndMarker) {
90 found_end_marker = true;
91 break;
92 }
93
94 if (offset == kCollisionSingleTileMarker) {
95 single_tiles_mode = true;
96 continue;
97 }
98
99 if (!single_tiles_mode) {
100 if (cursor + 1 >= collision_safe_end) {
101 return absl::OutOfRangeError("Collision rectangle header out of range");
102 }
103 uint8_t width = data[cursor];
104 uint8_t height = data[cursor + 1];
105 cursor += 2;
106
107 if (width == 0 || height == 0) {
108 continue;
109 }
110
111 for (uint8_t row = 0; row < height; ++row) {
112 int row_offset = static_cast<int>(offset) + (row * kCollisionMapWidth);
113 for (uint8_t col = 0; col < width; ++col) {
114 if (cursor >= collision_safe_end) {
115 return absl::OutOfRangeError(
116 "Collision rectangle data out of range");
117 }
118 uint8_t tile = data[cursor++];
119 int idx = row_offset + col;
120 if (idx < kCollisionMapTiles) {
121 result.SetOverride(static_cast<size_t>(idx), tile);
122 }
123 }
124 }
125 } else {
126 if (cursor >= collision_safe_end) {
127 return absl::OutOfRangeError("Collision single tile out of range");
128 }
129 uint8_t tile = data[cursor++];
130 int idx = static_cast<int>(offset);
131 if (idx < kCollisionMapTiles) {
132 result.SetOverride(static_cast<size_t>(idx), tile);
133 }
134 }
135 }
136
137 if (has_water_fill_reserved_region && !found_end_marker) {
138 return absl::FailedPreconditionError(
139 absl::StrFormat("Collision data for room 0x%02X is unterminated before "
140 "WaterFill reserved region",
141 room_id));
142 }
143
144 return result;
145}
146
147absl::StatusOr<std::string> DumpCustomCollisionRoomsToJsonString(
148 const std::vector<CustomCollisionRoomEntry>& rooms) {
149#if !defined(YAZE_WITH_JSON)
150 return absl::UnimplementedError(
151 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
152#else
153 using json = nlohmann::json;
154
155 std::vector<CustomCollisionRoomEntry> sorted = rooms;
156 std::sort(
157 sorted.begin(), sorted.end(),
159 return a.room_id < b.room_id;
160 });
161
162 json root;
163 root["version"] = 1;
164 json arr = json::array();
165 for (const auto& r : sorted) {
166 if (r.room_id < 0 || r.room_id >= kNumberOfRooms) {
167 continue;
168 }
169
170 // Normalize offsets: keep the last value, including explicit floor 0,
171 // for each offset and write in ascending order.
172 std::unordered_map<int, int> tile_map;
173 tile_map.reserve(r.tiles.size());
174 for (const auto& t : r.tiles) {
175 if (t.offset >= kCollisionMapTiles) {
176 continue;
177 }
178 tile_map[static_cast<int>(t.offset)] = static_cast<int>(t.value);
179 }
180
181 std::vector<std::pair<int, int>> tiles;
182 tiles.reserve(tile_map.size());
183 for (const auto& [off, val] : tile_map) {
184 tiles.emplace_back(off, val);
185 }
186 if (tiles.empty()) {
187 continue;
188 }
189 std::sort(tiles.begin(), tiles.end(),
190 [](const auto& a, const auto& b) { return a.first < b.first; });
191
192 json item;
193 item["room_id"] = absl::StrFormat("0x%02X", r.room_id);
194 json tiles_arr = json::array();
195 for (const auto& [off, val] : tiles) {
196 tiles_arr.push_back(json::array({off, val}));
197 }
198 item["tiles"] = std::move(tiles_arr);
199 arr.push_back(std::move(item));
200 }
201 root["rooms"] = std::move(arr);
202 return root.dump(/*indent=*/2);
203#endif
204}
205
206absl::StatusOr<std::vector<CustomCollisionRoomEntry>>
207LoadCustomCollisionRoomsFromJsonString(const std::string& json_content) {
208#if !defined(YAZE_WITH_JSON)
209 return absl::UnimplementedError(
210 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
211#else
212 using json = nlohmann::json;
213
214 json root;
215 try {
216 root = json::parse(json_content);
217 } catch (const json::parse_error& e) {
218 return absl::InvalidArgumentError(std::string("JSON parse error: ") +
219 e.what());
220 }
221
222 const int version = root.value("version", 1);
223 if (version != 1) {
224 return absl::InvalidArgumentError(absl::StrFormat(
225 "Unsupported custom collision JSON version: %d", version));
226 }
227 if (!root.contains("rooms") || !root["rooms"].is_array()) {
228 return absl::InvalidArgumentError("Missing or invalid 'rooms' array");
229 }
230
231 auto parse_int = [](const json& v) -> std::optional<int> {
232 if (v.is_number_integer()) {
233 return v.get<int>();
234 }
235 if (v.is_number_unsigned()) {
236 const auto u = v.get<unsigned int>();
237 if (u > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
238 return std::nullopt;
239 }
240 return static_cast<int>(u);
241 }
242 if (v.is_string()) {
243 try {
244 size_t idx = 0;
245 const std::string s = v.get<std::string>();
246 const int parsed = std::stoi(s, &idx, 0);
247 if (idx == s.size()) {
248 return parsed;
249 }
250 } catch (...) {}
251 }
252 return std::nullopt;
253 };
254
255 std::vector<CustomCollisionRoomEntry> out;
256 out.reserve(root["rooms"].size());
257
258 std::unordered_map<int, bool> seen_rooms;
259 for (const auto& item : root["rooms"]) {
260 if (!item.is_object()) {
261 continue;
262 }
263
264 const json& room_v = item.contains("room_id") ? item["room_id"]
265 : item.contains("room") ? item["room"]
266 : json();
267 const auto room_id_opt = parse_int(room_v);
268 if (!room_id_opt.has_value() || *room_id_opt < 0 ||
269 *room_id_opt >= kNumberOfRooms) {
270 return absl::InvalidArgumentError(
271 "Invalid room_id in custom collision JSON");
272 }
273 const int room_id = *room_id_opt;
274 if (seen_rooms.contains(room_id)) {
275 return absl::InvalidArgumentError(absl::StrFormat(
276 "Duplicate room_id in custom collision JSON: 0x%02X", room_id));
277 }
278 seen_rooms[room_id] = true;
279
280 const json& tiles_v = item.contains("tiles") ? item["tiles"]
281 : item.contains("entries") ? item["entries"]
282 : json::array();
283 if (!tiles_v.is_array()) {
284 return absl::InvalidArgumentError(
285 absl::StrFormat("Invalid tiles array for room 0x%02X", room_id));
286 }
287
288 // Last-wins map for duplicate offsets.
289 std::unordered_map<int, int> tile_map;
290 tile_map.reserve(tiles_v.size());
291 for (const auto& entry : tiles_v) {
292 int off = -1;
293 int val = -1;
294 if (entry.is_array() && entry.size() >= 2) {
295 const auto off_opt = parse_int(entry[0]);
296 const auto val_opt = parse_int(entry[1]);
297 if (!off_opt.has_value() || !val_opt.has_value()) {
298 return absl::InvalidArgumentError(
299 absl::StrFormat("Invalid tile entry for room 0x%02X", room_id));
300 }
301 off = *off_opt;
302 val = *val_opt;
303 } else if (entry.is_object()) {
304 const json& off_v = entry.contains("offset") ? entry["offset"]
305 : entry.contains("off") ? entry["off"]
306 : json();
307 const json& val_v = entry.contains("value") ? entry["value"]
308 : entry.contains("val") ? entry["val"]
309 : json();
310 const auto off_opt = parse_int(off_v);
311 const auto val_opt = parse_int(val_v);
312 if (!off_opt.has_value() || !val_opt.has_value()) {
313 return absl::InvalidArgumentError(
314 absl::StrFormat("Invalid tile entry for room 0x%02X", room_id));
315 }
316 off = *off_opt;
317 val = *val_opt;
318 } else {
319 return absl::InvalidArgumentError(
320 absl::StrFormat("Invalid tile entry for room 0x%02X", room_id));
321 }
322
323 if (off < 0 || off >= kCollisionMapTiles) {
324 return absl::InvalidArgumentError(
325 absl::StrFormat("Invalid tile offset for room 0x%02X", room_id));
326 }
327 if (val < 0 || val > 0xFF) {
328 return absl::InvalidArgumentError(
329 absl::StrFormat("Invalid tile value for room 0x%02X", room_id));
330 }
331 tile_map[off] = val;
332 }
333
334 std::vector<std::pair<int, int>> tiles;
335 tiles.reserve(tile_map.size());
336 for (const auto& [off, val] : tile_map) {
337 tiles.emplace_back(off, val);
338 }
339 std::sort(tiles.begin(), tiles.end(),
340 [](const auto& a, const auto& b) { return a.first < b.first; });
341
343 r.room_id = room_id;
344 r.tiles.reserve(tiles.size());
345 for (const auto& [off, val] : tiles) {
347 t.offset = static_cast<uint16_t>(off);
348 t.value = static_cast<uint8_t>(val);
349 r.tiles.push_back(std::move(t));
350 }
351 out.push_back(std::move(r));
352 }
353
354 std::sort(
355 out.begin(), out.end(),
357 return a.room_id < b.room_id;
358 });
359 return out;
360#endif
361}
362
363absl::StatusOr<std::string> DumpCustomCollisionSourceFromRom(Rom* rom) {
364 std::vector<CustomCollisionRoomEntry> rooms;
365 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
366 auto map = LoadCustomCollisionMap(rom, room_id);
367 if (!map.ok()) {
368 return map.status();
369 }
370 if (!map->has_data) {
371 continue;
372 }
374 entry.room_id = room_id;
375 for (size_t offset = 0; offset < map->tiles.size(); ++offset) {
376 if (map->HasOverride(offset)) {
377 entry.tiles.push_back(CustomCollisionTileEntry{
378 static_cast<uint16_t>(offset), map->tiles[offset]});
379 }
380 }
381 if (!entry.tiles.empty()) {
382 rooms.push_back(std::move(entry));
383 }
384 }
386}
387
388} // namespace zelda3
389} // namespace yaze
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
const auto & vector() const
Definition rom.h:205
bool is_loaded() const
Definition rom.h:187
constexpr int kCustomCollisionDataSoftEnd
nlohmann::json json
absl::StatusOr< std::vector< CustomCollisionRoomEntry > > LoadCustomCollisionRoomsFromJsonString(const std::string &json_content)
absl::StatusOr< std::string > DumpCustomCollisionRoomsToJsonString(const std::vector< CustomCollisionRoomEntry > &rooms)
constexpr size_t kCollisionMapTiles
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
absl::StatusOr< std::string > DumpCustomCollisionSourceFromRom(Rom *rom)
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
std::array< uint8_t, 64 *64 > tiles
void SetOverride(size_t offset, uint8_t tile)
std::vector< CustomCollisionTileEntry > tiles