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(absl::StrFormat(
76 "Collision data for room 0x%02X overlaps WaterFill reserved region (pc=0x%06X)",
77 room_id, pc_ptr));
78 }
79
80 size_t cursor = static_cast<size_t>(pc_ptr);
81 bool single_tiles_mode = false;
82 bool found_end_marker = false;
83
84 while (cursor + 1 < collision_safe_end) {
85 uint16_t offset = data[cursor] | (data[cursor + 1] << 8);
86 cursor += 2;
87
88 if (offset == kCollisionEndMarker) {
89 found_end_marker = true;
90 break;
91 }
92
93 if (offset == kCollisionSingleTileMarker) {
94 single_tiles_mode = true;
95 continue;
96 }
97
98 if (!single_tiles_mode) {
99 if (cursor + 1 >= collision_safe_end) {
100 return absl::OutOfRangeError("Collision rectangle header out of range");
101 }
102 uint8_t width = data[cursor];
103 uint8_t height = data[cursor + 1];
104 cursor += 2;
105
106 if (width == 0 || height == 0) {
107 continue;
108 }
109
110 for (uint8_t row = 0; row < height; ++row) {
111 int row_offset = static_cast<int>(offset) + (row * kCollisionMapWidth);
112 for (uint8_t col = 0; col < width; ++col) {
113 if (cursor >= collision_safe_end) {
114 return absl::OutOfRangeError(
115 "Collision rectangle data out of range");
116 }
117 uint8_t tile = data[cursor++];
118 int idx = row_offset + col;
119 if (idx >= 0 && idx < kCollisionMapWidth * kCollisionMapHeight) {
120 result.tiles[static_cast<size_t>(idx)] = tile;
121 }
122 }
123 }
124 } else {
125 if (cursor >= collision_safe_end) {
126 return absl::OutOfRangeError("Collision single tile out of range");
127 }
128 uint8_t tile = data[cursor++];
129 int idx = static_cast<int>(offset);
130 if (idx >= 0 && idx < kCollisionMapWidth * kCollisionMapHeight) {
131 result.tiles[static_cast<size_t>(idx)] = tile;
132 }
133 }
134 }
135
136 if (has_water_fill_reserved_region && !found_end_marker) {
137 return absl::FailedPreconditionError(absl::StrFormat(
138 "Collision data for room 0x%02X is unterminated before WaterFill reserved region",
139 room_id));
140 }
141
142 return result;
143}
144
145absl::StatusOr<std::string> DumpCustomCollisionRoomsToJsonString(
146 const std::vector<CustomCollisionRoomEntry>& rooms) {
147#if !defined(YAZE_WITH_JSON)
148 return absl::UnimplementedError(
149 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
150#else
151 using json = nlohmann::json;
152
153 std::vector<CustomCollisionRoomEntry> sorted = rooms;
154 std::sort(sorted.begin(), sorted.end(),
155 [](const CustomCollisionRoomEntry& a,
156 const CustomCollisionRoomEntry& b) {
157 return a.room_id < b.room_id;
158 });
159
160 json root;
161 root["version"] = 1;
162 json arr = json::array();
163 for (const auto& r : sorted) {
164 if (r.room_id < 0 || r.room_id >= kNumberOfRooms) {
165 continue;
166 }
167
168 // Normalize offsets: keep last value for each offset and write in ascending
169 // order. Drop value=0 for compactness (0 is treated as "unset" by the editor).
170 std::unordered_map<int, int> tile_map;
171 tile_map.reserve(r.tiles.size());
172 for (const auto& t : r.tiles) {
173 if (t.offset >= kCollisionMapTiles) {
174 continue;
175 }
176 tile_map[static_cast<int>(t.offset)] = static_cast<int>(t.value);
177 }
178
179 std::vector<std::pair<int, int>> tiles;
180 tiles.reserve(tile_map.size());
181 for (const auto& [off, val] : tile_map) {
182 if (val == 0) {
183 continue;
184 }
185 tiles.emplace_back(off, val);
186 }
187 if (tiles.empty()) {
188 continue;
189 }
190 std::sort(tiles.begin(), tiles.end(),
191 [](const auto& a, const auto& b) { return a.first < b.first; });
192
193 json item;
194 item["room_id"] = absl::StrFormat("0x%02X", r.room_id);
195 json tiles_arr = json::array();
196 for (const auto& [off, val] : tiles) {
197 tiles_arr.push_back(json::array({off, val}));
198 }
199 item["tiles"] = std::move(tiles_arr);
200 arr.push_back(std::move(item));
201 }
202 root["rooms"] = std::move(arr);
203 return root.dump(/*indent=*/2);
204#endif
205}
206
207absl::StatusOr<std::vector<CustomCollisionRoomEntry>>
208LoadCustomCollisionRoomsFromJsonString(const std::string& json_content) {
209#if !defined(YAZE_WITH_JSON)
210 return absl::UnimplementedError(
211 "JSON support not enabled. Build with -DYAZE_WITH_JSON=ON");
212#else
213 using json = nlohmann::json;
214
215 json root;
216 try {
217 root = json::parse(json_content);
218 } catch (const json::parse_error& e) {
219 return absl::InvalidArgumentError(
220 std::string("JSON parse error: ") + e.what());
221 }
222
223 const int version = root.value("version", 1);
224 if (version != 1) {
225 return absl::InvalidArgumentError(
226 absl::StrFormat("Unsupported custom collision JSON version: %d",
227 version));
228 }
229 if (!root.contains("rooms") || !root["rooms"].is_array()) {
230 return absl::InvalidArgumentError("Missing or invalid 'rooms' array");
231 }
232
233 auto parse_int = [](const json& v) -> std::optional<int> {
234 if (v.is_number_integer()) {
235 return v.get<int>();
236 }
237 if (v.is_number_unsigned()) {
238 const auto u = v.get<unsigned int>();
239 if (u > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
240 return std::nullopt;
241 }
242 return static_cast<int>(u);
243 }
244 if (v.is_string()) {
245 try {
246 size_t idx = 0;
247 const std::string s = v.get<std::string>();
248 const int parsed = std::stoi(s, &idx, 0);
249 if (idx == s.size()) {
250 return parsed;
251 }
252 } catch (...) {
253 }
254 }
255 return std::nullopt;
256 };
257
258 std::vector<CustomCollisionRoomEntry> out;
259 out.reserve(root["rooms"].size());
260
261 std::unordered_map<int, bool> seen_rooms;
262 for (const auto& item : root["rooms"]) {
263 if (!item.is_object()) {
264 continue;
265 }
266
267 const json& room_v =
268 item.contains("room_id") ? item["room_id"]
269 : item.contains("room") ? item["room"]
270 : json();
271 const auto room_id_opt = parse_int(room_v);
272 if (!room_id_opt.has_value() || *room_id_opt < 0 ||
273 *room_id_opt >= kNumberOfRooms) {
274 return absl::InvalidArgumentError(
275 "Invalid room_id in custom collision JSON");
276 }
277 const int room_id = *room_id_opt;
278 if (seen_rooms.contains(room_id)) {
279 return absl::InvalidArgumentError(
280 absl::StrFormat("Duplicate room_id in custom collision JSON: 0x%02X",
281 room_id));
282 }
283 seen_rooms[room_id] = true;
284
285 const json& tiles_v =
286 item.contains("tiles") ? item["tiles"]
287 : item.contains("entries") ? item["entries"]
288 : json::array();
289 if (!tiles_v.is_array()) {
290 return absl::InvalidArgumentError(
291 absl::StrFormat("Invalid tiles array for room 0x%02X", room_id));
292 }
293
294 // Last-wins map for duplicate offsets.
295 std::unordered_map<int, int> tile_map;
296 tile_map.reserve(tiles_v.size());
297 for (const auto& entry : tiles_v) {
298 int off = -1;
299 int val = -1;
300 if (entry.is_array() && entry.size() >= 2) {
301 const auto off_opt = parse_int(entry[0]);
302 const auto val_opt = parse_int(entry[1]);
303 if (!off_opt.has_value() || !val_opt.has_value()) {
304 return absl::InvalidArgumentError(
305 absl::StrFormat("Invalid tile entry for room 0x%02X", room_id));
306 }
307 off = *off_opt;
308 val = *val_opt;
309 } else if (entry.is_object()) {
310 const json& off_v =
311 entry.contains("offset") ? entry["offset"]
312 : entry.contains("off") ? entry["off"]
313 : json();
314 const json& val_v =
315 entry.contains("value") ? entry["value"]
316 : entry.contains("val") ? entry["val"]
317 : json();
318 const auto off_opt = parse_int(off_v);
319 const auto val_opt = parse_int(val_v);
320 if (!off_opt.has_value() || !val_opt.has_value()) {
321 return absl::InvalidArgumentError(
322 absl::StrFormat("Invalid tile entry for room 0x%02X", room_id));
323 }
324 off = *off_opt;
325 val = *val_opt;
326 } else {
327 return absl::InvalidArgumentError(
328 absl::StrFormat("Invalid tile entry for room 0x%02X", room_id));
329 }
330
331 if (off < 0 || off >= kCollisionMapTiles) {
332 return absl::InvalidArgumentError(
333 absl::StrFormat("Invalid tile offset for room 0x%02X", room_id));
334 }
335 if (val < 0 || val > 0xFF) {
336 return absl::InvalidArgumentError(
337 absl::StrFormat("Invalid tile value for room 0x%02X", room_id));
338 }
339 tile_map[off] = val;
340 }
341
342 std::vector<std::pair<int, int>> tiles;
343 tiles.reserve(tile_map.size());
344 for (const auto& [off, val] : tile_map) {
345 tiles.emplace_back(off, val);
346 }
347 std::sort(tiles.begin(), tiles.end(),
348 [](const auto& a, const auto& b) { return a.first < b.first; });
349
351 r.room_id = room_id;
352 r.tiles.reserve(tiles.size());
353 for (const auto& [off, val] : tiles) {
355 t.offset = static_cast<uint16_t>(off);
356 t.value = static_cast<uint8_t>(val);
357 r.tiles.push_back(std::move(t));
358 }
359 out.push_back(std::move(r));
360 }
361
362 std::sort(out.begin(), out.end(),
363 [](const CustomCollisionRoomEntry& a,
364 const CustomCollisionRoomEntry& b) {
365 return a.room_id < b.room_id;
366 });
367 return out;
368#endif
369}
370
371} // namespace zelda3
372} // 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:28
const auto & vector() const
Definition rom.h:143
bool is_loaded() const
Definition rom.h:132
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)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
std::array< uint8_t, 64 *64 > tiles
std::vector< CustomCollisionTileEntry > tiles