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
88 }
89 if (tiles_loaded_) {
90 return;
91 }
92
93 if (rom_ == nullptr) {
94 // DEBUG: Log wall/corner objects
95 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
96 (id_ >= 0x100 && id_ <= 0x103)) {
97 LOG_DEBUG("RoomObject", "EnsureTilesLoaded: obj=0x%03X ROM is NULL!",
98 id_);
99 }
100 return;
101 }
102
103 // Try the new parser first - this is more efficient and accurate
104 auto parser_status = LoadTilesWithParser();
105 if (parser_status.ok()) {
106 tiles_loaded_ = true;
108 // DEBUG: Log wall/corner objects
109 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
110 (id_ >= 0x100 && id_ <= 0x103)) {
111 LOG_DEBUG("RoomObject",
112 "EnsureTilesLoaded: obj=0x%03X loaded %zu tiles via parser",
113 id_, tiles_.size());
114 }
115 return;
116 }
117
118 // DEBUG: Log parser failure for wall/corner objects
119 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
120 (id_ >= 0x100 && id_ <= 0x103)) {
121 LOG_DEBUG("RoomObject",
122 "EnsureTilesLoaded: obj=0x%03X parser failed: %s, trying legacy",
123 id_, std::string(parser_status.message()).c_str());
124 }
125
126 // Fallback to legacy method for compatibility with enhanced validation
127 auto rom_data = rom_->data();
128
129 // Determine which subtype table to use and compute the tile data offset.
130 SubtypeTableInfo sti = GetSubtypeTable(id_);
131 // Apply offset first (for Type 2/3 objects), then mask
132 int index = ((id_ - sti.id_offset) & sti.index_mask);
133 int tile_ptr = sti.base_ptr + (index * 2);
134
135 // Enhanced bounds checking
136 if (tile_ptr < 0 || tile_ptr + 1 >= (int)rom_->size()) {
137 // Log error but don't crash
138 LOG_DEBUG("RoomObject", "Tile pointer out of bounds for object %04X", id_);
139 tiles_.clear();
140 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
142 return;
143 }
144
145 int tile_rel = (int16_t)((rom_data[tile_ptr + 1] << 8) + rom_data[tile_ptr]);
146 int pos = kRoomObjectTileAddress + tile_rel;
147 tile_data_ptr_ = pos;
148
149 // Enhanced bounds checking for tile data
150 if (pos < 0 || pos + 7 >= (int)rom_->size()) {
151 // Log error but don't crash
152 LOG_DEBUG("RoomObject", "Tile data position out of bounds for object %04X",
153 id_);
154 tiles_.clear();
155 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
157 return;
158 }
159
160 // Read tile data with validation
161 uint16_t w0 = (uint16_t)(rom_data[pos] | (rom_data[pos + 1] << 8));
162 uint16_t w1 = (uint16_t)(rom_data[pos + 2] | (rom_data[pos + 3] << 8));
163 uint16_t w2 = (uint16_t)(rom_data[pos + 4] | (rom_data[pos + 5] << 8));
164 uint16_t w3 = (uint16_t)(rom_data[pos + 6] | (rom_data[pos + 7] << 8));
165
166 tiles_.clear();
167 tiles_.push_back(gfx::WordToTileInfo(w0));
168 tiles_.push_back(gfx::WordToTileInfo(w1));
169 tiles_.push_back(gfx::WordToTileInfo(w2));
170 tiles_.push_back(gfx::WordToTileInfo(w3));
171 tile_count_ = 1;
172 tiles_loaded_ = true;
174}
175
177 tiles_.clear();
178 tiles_loaded_ = false;
179 tile_count_ = 0;
180 tile_data_ptr_ = -1;
182}
183
189
195
201
203 all_bgs_ = IsAllBgsObjectId(id_);
204}
205
206void RoomObject::set_id(int16_t id) {
207 if (id_ == id) {
208 return;
209 }
210 id_ = id;
213}
214
216 if (rom_ == nullptr) {
217 return absl::InvalidArgumentError("ROM is null");
218 }
219
220 ObjectParser parser(rom_);
221 auto result = parser.ParseObject(id_);
222 if (!result.ok()) {
223 return result.status();
224 }
225
226 tiles_ = std::move(result.value());
227 tile_count_ = tiles_.size();
228 return absl::OkStatus();
229}
230
231absl::StatusOr<std::span<const gfx::TileInfo>> RoomObject::GetTiles() const {
232 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
233
234 if (tiles_.empty()) {
235 return absl::FailedPreconditionError("No tiles loaded for object");
236 }
237
238 return std::span<const gfx::TileInfo>(tiles_.data(), tiles_.size());
239}
240
241absl::StatusOr<const gfx::TileInfo*> RoomObject::GetTile(int index) const {
242 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
243
244 if (index < 0 || index >= static_cast<int>(tiles_.size())) {
245 return absl::OutOfRangeError(absl::StrFormat(
246 "Tile index %d out of range (0-%d)", index, tiles_.size() - 1));
247 }
248
249 return &tiles_[index];
250}
251
253 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
254
255 return tile_count_;
256}
257
258// ============================================================================
259// Object Encoding/Decoding Implementation (Phase 1, Task 1.1)
260// ============================================================================
261
262int RoomObject::DetermineObjectType(uint8_t b1, uint8_t b3) {
263 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
264 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
265 // would incorrectly trigger Type 3 decoding if we checked b3 first.
266 //
267 // Type 2: 111111xx xxxxyyyy yyiiiiii
268 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
269 if (b1 >= 0xFC) {
270 return 2;
271 }
272
273 // Type 3: Representable object IDs 0xF80-0xFFF
274 // These have b3 >= 0xF8 (top nibble is 0xF)
275 if (b3 >= 0xF8) {
276 return 3;
277 }
278
279 // Type 1: Representable object IDs 0x000-0x0F7
280 return 1;
281}
282
283RoomObject RoomObject::DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3,
284 uint8_t layer) {
285 uint8_t x = 0;
286 uint8_t y = 0;
287 uint8_t size = 0;
288 uint16_t id = 0;
289
290 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
291 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
292 // would incorrectly trigger Type 3 decoding if we checked b3 first.
293
294 // Type 2: 111111xx xxxxyyyy yyiiiiii
295 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
296 if (b1 >= 0xFC) {
297 id = (b3 & 0x3F) | 0x100;
298 x = ((b2 & 0xF0) >> 4) | ((b1 & 0x03) << 4);
299 y = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6);
300 size = 0;
301 LOG_DEBUG("ObjectParser",
302 "Type2: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
303 b2, b3, id, x, y, size);
304 }
305 // Type 3: xxxxxxii yyyyyyii 11111iii
306 // Discriminator: b3 >= 0xF8 (top 5 bits all 1)
307 else if (b3 >= 0xF8) {
308 id = (static_cast<uint16_t>(b3) << 4) | 0x80 |
309 ((static_cast<uint16_t>(b2 & 0x03) << 2) + (b1 & 0x03));
310 x = (b1 & 0xFC) >> 2;
311 y = (b2 & 0xFC) >> 2;
312 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
313 LOG_DEBUG("ObjectParser",
314 "Type3: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
315 b2, b3, id, x, y, size);
316 }
317 // Type 1: xxxxxxss yyyyyyss iiiiiiii
318 else {
319 id = b3;
320 x = (b1 & 0xFC) >> 2;
321 y = (b2 & 0xFC) >> 2;
322 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
323 LOG_DEBUG("ObjectParser",
324 "Type1: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
325 b2, b3, id, x, y, size);
326 }
327
328 auto obj = RoomObject(static_cast<int16_t>(id), x, y, size, layer);
329 obj.RefreshDerivedFlagsFromId();
330
331 return obj;
332}
333
335 ObjectBytes bytes;
336
337 // Determine type based on object ID
338 if (id_ >= 0x100 && id_ < 0x200) {
339 // Type 2: 111111xx xxxxyyyy yyiiiiii (representable IDs 0x100-0x13F)
340 bytes.b1 = 0xFC | ((x_ & 0x30) >> 4);
341 bytes.b2 = ((x_ & 0x0F) << 4) | ((y_ & 0x3C) >> 2);
342 bytes.b3 = ((y_ & 0x03) << 6) | (id_ & 0x3F);
343 } else if (id_ >= 0xF00) {
344 // Type 3: xxxxxxii yyyyyyii 11111iii (representable IDs 0xF80-0xFFF)
345 bytes.b1 = (x_ << 2) | (id_ & 0x03);
346 bytes.b2 = (y_ << 2) | ((id_ >> 2) & 0x03);
347 bytes.b3 = (id_ >> 4) & 0xFF;
348 } else {
349 // Type 1: xxxxxxss yyyyyyss iiiiiiii (representable IDs 0x000-0x0F7)
350 uint8_t clamped_size = size_ > 15 ? 15 : size_;
351 bytes.b1 = (x_ << 2) | ((clamped_size >> 2) & 0x03);
352 bytes.b2 = (y_ << 2) | (clamped_size & 0x03);
353 bytes.b3 = static_cast<uint8_t>(id_);
354 }
355
356 return bytes;
357}
358
359bool IsRoomObjectSizeEditable(int object_id) {
360 return object_id >= 0x000 && object_id <= 0x0F7;
361}
362
363uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size) {
364 if (IsRoomObjectSizeEditable(object_id)) {
365 return std::min<uint8_t>(requested_size, 15);
366 }
367 if (object_id >= 0x100 && object_id <= 0x13F) {
368 return 0;
369 }
370 if (object_id >= 0xF80 && object_id <= 0xFFF) {
371 return static_cast<uint8_t>(((object_id & 0x03) << 2) |
372 ((object_id >> 2) & 0x03));
373 }
374 return requested_size;
375}
376
377uint8_t DefaultRoomObjectSizeForPlacement(int object_id) {
378 if (IsRoomObjectSizeEditable(object_id)) {
379 return 2;
380 }
381 return CanonicalRoomObjectSize(object_id, 0);
382}
383
385 const int layer = object.GetLayerValue();
386 if (layer > 2) {
387 return absl::InvalidArgumentError(absl::StrFormat(
388 "Room object 0x%03X has invalid stream list index %d; expected 0..2",
389 object.id_, layer));
390 }
391 if (object.x_ > 63 || object.y_ > 63) {
392 return absl::InvalidArgumentError(absl::StrFormat(
393 "Room object 0x%03X has out-of-range coordinates (%d, %d); "
394 "expected 0..63",
395 object.id_, object.x_, object.y_));
396 }
397
398 int expected_type = 0;
399 if (object.id_ >= 0x000 && object.id_ <= 0x0F7) {
400 expected_type = 1;
401 } else if (object.id_ >= 0x100 && object.id_ <= 0x13F) {
402 expected_type = 2;
403 } else if (object.id_ >= 0xF80 && object.id_ <= 0xFFF) {
404 expected_type = 3;
405 } else {
406 return absl::InvalidArgumentError(absl::StrFormat(
407 "Room object ID 0x%03X is not representable without aliasing; "
408 "expected 0x000..0x0F7, 0x100..0x13F, or 0xF80..0xFFF",
409 object.id_));
410 }
411
412 const uint8_t canonical_size =
413 CanonicalRoomObjectSize(object.id_, object.size_);
414 if (object.size_ != canonical_size) {
415 if (expected_type == 1) {
416 return absl::InvalidArgumentError(
417 absl::StrFormat("Room object 0x%03X has noncanonical size 0x%02X; "
418 "Type 1 size must be 0x00..0x0F",
419 object.id_, object.size_));
420 }
421 return absl::InvalidArgumentError(absl::StrFormat(
422 "Room object 0x%03X has noncanonical size 0x%02X; expected 0x%02X",
423 object.id_, object.size_, canonical_size));
424 }
425
426 // Type 1/3 encode X in the upper six bits of byte 1. X=63 therefore
427 // produces the Type 2 discriminator (0xFC..0xFF) and decodes as another
428 // object family.
429 if (expected_type != 2 && object.x_ == 63) {
430 return absl::InvalidArgumentError(absl::StrFormat(
431 "Room object 0x%03X at x=63 collides with the Type 2 discriminator",
432 object.id_));
433 }
434
435 const RoomObject::ObjectBytes encoded = object.EncodeObjectToBytes();
436 if ((encoded.b1 == 0xFF && encoded.b2 == 0xFF) ||
437 (encoded.b1 == 0xF0 && encoded.b2 == 0xFF)) {
438 return absl::InvalidArgumentError(absl::StrFormat(
439 "Room object 0x%03X encodes with reserved stream-control prefix "
440 "0x%02X 0x%02X",
441 object.id_, encoded.b1, encoded.b2));
442 }
443
444 const int encoded_type =
445 RoomObject::DetermineObjectType(encoded.b1, encoded.b3);
446 if (encoded_type != expected_type) {
447 return absl::InvalidArgumentError(absl::StrFormat(
448 "Room object 0x%03X encodes with Type %d discriminator, expected "
449 "Type %d",
450 object.id_, encoded_type, expected_type));
451 }
452
454 encoded.b1, encoded.b2, encoded.b3,
455 static_cast<uint8_t>(object.GetLayerValue()));
456 if (decoded.id_ != object.id_ || decoded.x_ != object.x_ ||
457 decoded.y_ != object.y_) {
458 return absl::InvalidArgumentError(absl::StrFormat(
459 "Room object 0x%03X at (%d, %d) aliases to 0x%03X at (%d, %d) "
460 "after encoding",
461 object.id_, object.x_, object.y_, decoded.id_, decoded.x_, decoded.y_));
462 }
463
464 return absl::OkStatus();
465}
466
467} // namespace zelda3
468} // namespace yaze
uint64_t object_tile_revision() const
Definition rom.h:164
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
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_
bool IsTrackedTileCacheCurrent() const
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
const Rom * tile_cache_rom_identity_
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)