yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room_object.cc
Go to the documentation of this file.
1#include "room_object.h"
2
3#include <algorithm>
4
5#include "absl/status/status.h"
6#include "absl/strings/str_format.h"
7#include "util/log.h"
9
10namespace yaze {
11namespace zelda3 {
12
13namespace {
15 int base_ptr; // base address of subtype table in ROM (PC)
16 int index_mask; // mask to apply to object id for index
17 int id_offset; // offset to subtract from object_id before masking
18
19 SubtypeTableInfo(int base, int mask, int offset = 0)
20 : base_ptr(base), index_mask(mask), id_offset(offset) {}
21};
22
24 // Heuristic: 0x00-0xFF => subtype1, 0x100-0x1FF => subtype2, >=0xF80 =>
25 // subtype3. Type 3 IDs from decoding are 0xF80-0xFFF (b3 0xF8-0xFF shifted).
26 if (object_id >= 0xF80) {
27 // Type 3: IDs 0xF80-0xFFF map to table indices 0-127
28 // Subtract 0xF80 first, then mask with 0x7F
29 return SubtypeTableInfo(kRoomObjectSubtype3, 0x7F, 0xF80);
30 } else if (object_id >= 0x100) {
31 // Type 2: IDs 0x100-0x1FF map to table indices 0-255
32 return SubtypeTableInfo(kRoomObjectSubtype2, 0xFF, 0x100);
33 } else {
34 // Type 1: IDs 0x00-0xFF map directly to table indices
36 }
37}
38
39bool IsAllBgsObjectId(int object_id) {
40 // Objects that should be treated as drawing to both BG1 and BG2.
41 //
42 // NOTE: This is editor/runtime metadata for our renderer, not a ROM field.
43 // Keep this list in sync with DecodeObjectFromBytes behavior and any
44 // special-cased BothBG handling in ObjectDrawer.
45 const int id = object_id;
46 // USDASM: Rightwards2x4spaced4_1to16 writes to both tilemaps.
47 if ((id >= 0x03 && id <= 0x04) ||
48 (id >= 0x63 && id <= 0x64) || // Routine 9 objects
49 // Routine 17 (Acute Diagonals)
50 id == 0x0C || id == 0x0D || id == 0x10 || id == 0x11 || id == 0x14 ||
51 id == 0x15 || id == 0x18 || id == 0x19 || id == 0x1C || id == 0x1D ||
52 id == 0x20 ||
53 // Routine 18 (Grave Diagonals)
54 id == 0x0E || id == 0x0F || id == 0x12 || id == 0x13 || id == 0x16 ||
55 id == 0x17 || id == 0x1A || id == 0x1B || id == 0x1E || id == 0x1F) {
56 return true;
57 }
58 return false;
59}
60} // namespace
61
63 return static_cast<ObjectOption>(static_cast<int>(lhs) |
64 static_cast<int>(rhs));
65}
66
68 return static_cast<ObjectOption>(static_cast<int>(lhs) &
69 static_cast<int>(rhs));
70}
71
73 return static_cast<ObjectOption>(static_cast<int>(lhs) ^
74 static_cast<int>(rhs));
75}
76
78 return static_cast<ObjectOption>(~static_cast<int>(option));
79}
80
81// NOTE: DrawTile was legacy ZScream code that is no longer used.
82// Modern rendering uses ObjectDrawer which draws directly to BackgroundBuffer
83// bitmaps.
84
86 if (tiles_loaded_) {
87 return;
88 }
89
90 if (rom_ == nullptr) {
91 // DEBUG: Log wall/corner objects
92 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
93 (id_ >= 0x100 && id_ <= 0x103)) {
94 LOG_DEBUG("RoomObject", "EnsureTilesLoaded: obj=0x%03X ROM is NULL!",
95 id_);
96 }
97 return;
98 }
99
100 // Try the new parser first - this is more efficient and accurate
101 auto parser_status = LoadTilesWithParser();
102 if (parser_status.ok()) {
103 tiles_loaded_ = true;
104 // DEBUG: Log wall/corner objects
105 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
106 (id_ >= 0x100 && id_ <= 0x103)) {
107 LOG_DEBUG("RoomObject",
108 "EnsureTilesLoaded: obj=0x%03X loaded %zu tiles via parser",
109 id_, tiles_.size());
110 }
111 return;
112 }
113
114 // DEBUG: Log parser failure for wall/corner objects
115 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
116 (id_ >= 0x100 && id_ <= 0x103)) {
117 LOG_DEBUG("RoomObject",
118 "EnsureTilesLoaded: obj=0x%03X parser failed: %s, trying legacy",
119 id_, std::string(parser_status.message()).c_str());
120 }
121
122 // Fallback to legacy method for compatibility with enhanced validation
123 auto rom_data = rom_->data();
124
125 // Determine which subtype table to use and compute the tile data offset.
126 SubtypeTableInfo sti = GetSubtypeTable(id_);
127 // Apply offset first (for Type 2/3 objects), then mask
128 int index = ((id_ - sti.id_offset) & sti.index_mask);
129 int tile_ptr = sti.base_ptr + (index * 2);
130
131 // Enhanced bounds checking
132 if (tile_ptr < 0 || tile_ptr + 1 >= (int)rom_->size()) {
133 // Log error but don't crash
134 LOG_DEBUG("RoomObject", "Tile pointer out of bounds for object %04X", id_);
135 tiles_.clear();
136 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
137 return;
138 }
139
140 int tile_rel = (int16_t)((rom_data[tile_ptr + 1] << 8) + rom_data[tile_ptr]);
141 int pos = kRoomObjectTileAddress + tile_rel;
142 tile_data_ptr_ = pos;
143
144 // Enhanced bounds checking for tile data
145 if (pos < 0 || pos + 7 >= (int)rom_->size()) {
146 // Log error but don't crash
147 LOG_DEBUG("RoomObject", "Tile data position out of bounds for object %04X",
148 id_);
149 tiles_.clear();
150 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
151 return;
152 }
153
154 // Read tile data with validation
155 uint16_t w0 = (uint16_t)(rom_data[pos] | (rom_data[pos + 1] << 8));
156 uint16_t w1 = (uint16_t)(rom_data[pos + 2] | (rom_data[pos + 3] << 8));
157 uint16_t w2 = (uint16_t)(rom_data[pos + 4] | (rom_data[pos + 5] << 8));
158 uint16_t w3 = (uint16_t)(rom_data[pos + 6] | (rom_data[pos + 7] << 8));
159
160 tiles_.clear();
161 tiles_.push_back(gfx::WordToTileInfo(w0));
162 tiles_.push_back(gfx::WordToTileInfo(w1));
163 tiles_.push_back(gfx::WordToTileInfo(w2));
164 tiles_.push_back(gfx::WordToTileInfo(w3));
165 tile_count_ = 1;
166 tiles_loaded_ = true;
167}
168
170 tiles_.clear();
171 tiles_loaded_ = false;
172 tile_count_ = 0;
173 tile_data_ptr_ = -1;
174}
175
177 all_bgs_ = IsAllBgsObjectId(id_);
178}
179
180void RoomObject::set_id(int16_t id) {
181 if (id_ == id) {
182 return;
183 }
184 id_ = id;
187}
188
190 if (rom_ == nullptr) {
191 return absl::InvalidArgumentError("ROM is null");
192 }
193
194 ObjectParser parser(rom_);
195 auto result = parser.ParseObject(id_);
196 if (!result.ok()) {
197 return result.status();
198 }
199
200 tiles_ = std::move(result.value());
201 tile_count_ = tiles_.size();
202 return absl::OkStatus();
203}
204
205absl::StatusOr<std::span<const gfx::TileInfo>> RoomObject::GetTiles() const {
206 if (!tiles_loaded_) {
207 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
208 }
209
210 if (tiles_.empty()) {
211 return absl::FailedPreconditionError("No tiles loaded for object");
212 }
213
214 return std::span<const gfx::TileInfo>(tiles_.data(), tiles_.size());
215}
216
217absl::StatusOr<const gfx::TileInfo*> RoomObject::GetTile(int index) const {
218 if (!tiles_loaded_) {
219 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
220 }
221
222 if (index < 0 || index >= static_cast<int>(tiles_.size())) {
223 return absl::OutOfRangeError(absl::StrFormat(
224 "Tile index %d out of range (0-%d)", index, tiles_.size() - 1));
225 }
226
227 return &tiles_[index];
228}
229
231 if (!tiles_loaded_) {
232 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
233 }
234
235 return tile_count_;
236}
237
238// ============================================================================
239// Object Encoding/Decoding Implementation (Phase 1, Task 1.1)
240// ============================================================================
241
242int RoomObject::DetermineObjectType(uint8_t b1, uint8_t b3) {
243 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
244 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
245 // would incorrectly trigger Type 3 decoding if we checked b3 first.
246 //
247 // Type 2: 111111xx xxxxyyyy yyiiiiii
248 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
249 if (b1 >= 0xFC) {
250 return 2;
251 }
252
253 // Type 3: Representable object IDs 0xF80-0xFFF
254 // These have b3 >= 0xF8 (top nibble is 0xF)
255 if (b3 >= 0xF8) {
256 return 3;
257 }
258
259 // Type 1: Representable object IDs 0x000-0x0F7
260 return 1;
261}
262
263RoomObject RoomObject::DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3,
264 uint8_t layer) {
265 uint8_t x = 0;
266 uint8_t y = 0;
267 uint8_t size = 0;
268 uint16_t id = 0;
269
270 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
271 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
272 // would incorrectly trigger Type 3 decoding if we checked b3 first.
273
274 // Type 2: 111111xx xxxxyyyy yyiiiiii
275 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
276 if (b1 >= 0xFC) {
277 id = (b3 & 0x3F) | 0x100;
278 x = ((b2 & 0xF0) >> 4) | ((b1 & 0x03) << 4);
279 y = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6);
280 size = 0;
281 LOG_DEBUG("ObjectParser",
282 "Type2: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
283 b2, b3, id, x, y, size);
284 }
285 // Type 3: xxxxxxii yyyyyyii 11111iii
286 // Discriminator: b3 >= 0xF8 (top 5 bits all 1)
287 else if (b3 >= 0xF8) {
288 id = (static_cast<uint16_t>(b3) << 4) | 0x80 |
289 ((static_cast<uint16_t>(b2 & 0x03) << 2) + (b1 & 0x03));
290 x = (b1 & 0xFC) >> 2;
291 y = (b2 & 0xFC) >> 2;
292 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
293 LOG_DEBUG("ObjectParser",
294 "Type3: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
295 b2, b3, id, x, y, size);
296 }
297 // Type 1: xxxxxxss yyyyyyss iiiiiiii
298 else {
299 id = b3;
300 x = (b1 & 0xFC) >> 2;
301 y = (b2 & 0xFC) >> 2;
302 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
303 LOG_DEBUG("ObjectParser",
304 "Type1: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
305 b2, b3, id, x, y, size);
306 }
307
308 auto obj = RoomObject(static_cast<int16_t>(id), x, y, size, layer);
309 obj.RefreshDerivedFlagsFromId();
310
311 return obj;
312}
313
315 ObjectBytes bytes;
316
317 // Determine type based on object ID
318 if (id_ >= 0x100 && id_ < 0x200) {
319 // Type 2: 111111xx xxxxyyyy yyiiiiii (representable IDs 0x100-0x13F)
320 bytes.b1 = 0xFC | ((x_ & 0x30) >> 4);
321 bytes.b2 = ((x_ & 0x0F) << 4) | ((y_ & 0x3C) >> 2);
322 bytes.b3 = ((y_ & 0x03) << 6) | (id_ & 0x3F);
323 } else if (id_ >= 0xF00) {
324 // Type 3: xxxxxxii yyyyyyii 11111iii (representable IDs 0xF80-0xFFF)
325 bytes.b1 = (x_ << 2) | (id_ & 0x03);
326 bytes.b2 = (y_ << 2) | ((id_ >> 2) & 0x03);
327 bytes.b3 = (id_ >> 4) & 0xFF;
328 } else {
329 // Type 1: xxxxxxss yyyyyyss iiiiiiii (representable IDs 0x000-0x0F7)
330 uint8_t clamped_size = size_ > 15 ? 15 : size_;
331 bytes.b1 = (x_ << 2) | ((clamped_size >> 2) & 0x03);
332 bytes.b2 = (y_ << 2) | (clamped_size & 0x03);
333 bytes.b3 = static_cast<uint8_t>(id_);
334 }
335
336 return bytes;
337}
338
339bool IsRoomObjectSizeEditable(int object_id) {
340 return object_id >= 0x000 && object_id <= 0x0F7;
341}
342
343uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size) {
344 if (IsRoomObjectSizeEditable(object_id)) {
345 return std::min<uint8_t>(requested_size, 15);
346 }
347 if (object_id >= 0x100 && object_id <= 0x13F) {
348 return 0;
349 }
350 if (object_id >= 0xF80 && object_id <= 0xFFF) {
351 return static_cast<uint8_t>(((object_id & 0x03) << 2) |
352 ((object_id >> 2) & 0x03));
353 }
354 return requested_size;
355}
356
357uint8_t DefaultRoomObjectSizeForPlacement(int object_id) {
358 if (IsRoomObjectSizeEditable(object_id)) {
359 return 2;
360 }
361 return CanonicalRoomObjectSize(object_id, 0);
362}
363
365 const int layer = object.GetLayerValue();
366 if (layer > 2) {
367 return absl::InvalidArgumentError(absl::StrFormat(
368 "Room object 0x%03X has invalid stream list index %d; expected 0..2",
369 object.id_, layer));
370 }
371 if (object.x_ > 63 || object.y_ > 63) {
372 return absl::InvalidArgumentError(absl::StrFormat(
373 "Room object 0x%03X has out-of-range coordinates (%d, %d); "
374 "expected 0..63",
375 object.id_, object.x_, object.y_));
376 }
377
378 int expected_type = 0;
379 if (object.id_ >= 0x000 && object.id_ <= 0x0F7) {
380 expected_type = 1;
381 } else if (object.id_ >= 0x100 && object.id_ <= 0x13F) {
382 expected_type = 2;
383 } else if (object.id_ >= 0xF80 && object.id_ <= 0xFFF) {
384 expected_type = 3;
385 } else {
386 return absl::InvalidArgumentError(absl::StrFormat(
387 "Room object ID 0x%03X is not representable without aliasing; "
388 "expected 0x000..0x0F7, 0x100..0x13F, or 0xF80..0xFFF",
389 object.id_));
390 }
391
392 const uint8_t canonical_size =
393 CanonicalRoomObjectSize(object.id_, object.size_);
394 if (object.size_ != canonical_size) {
395 if (expected_type == 1) {
396 return absl::InvalidArgumentError(
397 absl::StrFormat("Room object 0x%03X has noncanonical size 0x%02X; "
398 "Type 1 size must be 0x00..0x0F",
399 object.id_, object.size_));
400 }
401 return absl::InvalidArgumentError(absl::StrFormat(
402 "Room object 0x%03X has noncanonical size 0x%02X; expected 0x%02X",
403 object.id_, object.size_, canonical_size));
404 }
405
406 // Type 1/3 encode X in the upper six bits of byte 1. X=63 therefore
407 // produces the Type 2 discriminator (0xFC..0xFF) and decodes as another
408 // object family.
409 if (expected_type != 2 && object.x_ == 63) {
410 return absl::InvalidArgumentError(absl::StrFormat(
411 "Room object 0x%03X at x=63 collides with the Type 2 discriminator",
412 object.id_));
413 }
414
415 const RoomObject::ObjectBytes encoded = object.EncodeObjectToBytes();
416 if ((encoded.b1 == 0xFF && encoded.b2 == 0xFF) ||
417 (encoded.b1 == 0xF0 && encoded.b2 == 0xFF)) {
418 return absl::InvalidArgumentError(absl::StrFormat(
419 "Room object 0x%03X encodes with reserved stream-control prefix "
420 "0x%02X 0x%02X",
421 object.id_, encoded.b1, encoded.b2));
422 }
423
424 const int encoded_type =
425 RoomObject::DetermineObjectType(encoded.b1, encoded.b3);
426 if (encoded_type != expected_type) {
427 return absl::InvalidArgumentError(absl::StrFormat(
428 "Room object 0x%03X encodes with Type %d discriminator, expected "
429 "Type %d",
430 object.id_, encoded_type, expected_type));
431 }
432
434 encoded.b1, encoded.b2, encoded.b3,
435 static_cast<uint8_t>(object.GetLayerValue()));
436 if (decoded.id_ != object.id_ || decoded.x_ != object.x_ ||
437 decoded.y_ != object.y_) {
438 return absl::InvalidArgumentError(absl::StrFormat(
439 "Room object 0x%03X at (%d, %d) aliases to 0x%03X at (%d, %d) "
440 "after encoding",
441 object.id_, object.x_, object.y_, decoded.id_, decoded.x_, decoded.y_));
442 }
443
444 return absl::OkStatus();
445}
446
447} // namespace zelda3
448} // namespace yaze
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
Direct ROM parser for dungeon objects.
absl::StatusOr< std::vector< gfx::TileInfo > > ParseObject(int16_t object_id)
Parse object data directly from ROM.
absl::StatusOr< const gfx::TileInfo * > GetTile(int index) const
static RoomObject DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t layer)
std::vector< gfx::TileInfo > tiles_
ObjectBytes EncodeObjectToBytes() const
void set_id(int16_t id)
static int DetermineObjectType(uint8_t b1, uint8_t b3)
absl::Status LoadTilesWithParser()
uint8_t size() const
Definition room_object.h:91
absl::StatusOr< std::span< const gfx::TileInfo > > GetTiles() const
RoomObject(int16_t id, uint8_t x, uint8_t y, uint8_t size, uint8_t layer=0)
Definition room_object.h:64
#define LOG_DEBUG(category, format,...)
Definition log.h:103
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
SubtypeTableInfo GetSubtypeTable(int object_id)
uint8_t DefaultRoomObjectSizeForPlacement(int object_id)
bool IsRoomObjectSizeEditable(int object_id)
constexpr int kRoomObjectSubtype3
Definition room_object.h:47
ObjectOption operator|(ObjectOption lhs, ObjectOption rhs)
ObjectOption operator^(ObjectOption lhs, ObjectOption rhs)
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
constexpr int kRoomObjectSubtype1
Definition room_object.h:45
constexpr int kRoomObjectSubtype2
Definition room_object.h:46
constexpr int kRoomObjectTileAddress
Definition room_object.h:48
ObjectOption operator~(ObjectOption option)
ObjectOption operator&(ObjectOption lhs, ObjectOption rhs)
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)