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 "core/features.h"
8#include "util/log.h"
13
14namespace yaze {
15namespace zelda3 {
16
17namespace {
19 int base_ptr; // base address of subtype table in ROM (PC)
20 int index_mask; // mask to apply to object id for index
21 int id_offset; // offset to subtract from object_id before masking
22
23 SubtypeTableInfo(int base, int mask, int offset = 0)
24 : base_ptr(base), index_mask(mask), id_offset(offset) {}
25};
26
28 // Heuristic: 0x00-0xFF => subtype1, 0x100-0x1FF => subtype2, >=0xF80 =>
29 // subtype3. Type 3 IDs from decoding are 0xF80-0xFFF (b3 0xF8-0xFF shifted).
30 if (object_id >= 0xF80) {
31 // Type 3: IDs 0xF80-0xFFF map to table indices 0-127
32 // Subtract 0xF80 first, then mask with 0x7F
33 return SubtypeTableInfo(kRoomObjectSubtype3, 0x7F, 0xF80);
34 } else if (object_id >= 0x100) {
35 // Type 2: IDs 0x100-0x1FF map to table indices 0-255
36 return SubtypeTableInfo(kRoomObjectSubtype2, 0xFF, 0x100);
37 } else {
38 // Type 1: IDs 0x00-0xFF map directly to table indices
40 }
41}
42
43} // namespace
44
46 return static_cast<ObjectOption>(static_cast<int>(lhs) |
47 static_cast<int>(rhs));
48}
49
51 return static_cast<ObjectOption>(static_cast<int>(lhs) &
52 static_cast<int>(rhs));
53}
54
56 return static_cast<ObjectOption>(static_cast<int>(lhs) ^
57 static_cast<int>(rhs));
58}
59
61 return static_cast<ObjectOption>(~static_cast<int>(option));
62}
63
64// NOTE: DrawTile was legacy ZScream code that is no longer used.
65// Modern rendering uses ObjectDrawer which draws directly to BackgroundBuffer
66// bitmaps.
67
71 }
72 if (tiles_loaded_) {
73 return;
74 }
75
76 if (rom_ == nullptr) {
77 // DEBUG: Log wall/corner objects
78 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
79 (id_ >= 0x100 && id_ <= 0x103)) {
80 LOG_DEBUG("RoomObject", "EnsureTilesLoaded: obj=0x%03X ROM is NULL!",
81 id_);
82 }
83 return;
84 }
85
86 // Try the new parser first - this is more efficient and accurate
87 auto parser_status = LoadTilesWithParser();
88 if (parser_status.ok()) {
89 tiles_loaded_ = true;
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",
95 "EnsureTilesLoaded: obj=0x%03X loaded %zu tiles via parser",
96 id_, tiles_.size());
97 }
98 return;
99 }
100
101 // DEBUG: Log parser failure for wall/corner objects
102 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
103 (id_ >= 0x100 && id_ <= 0x103)) {
104 LOG_DEBUG("RoomObject",
105 "EnsureTilesLoaded: obj=0x%03X parser failed: %s, trying legacy",
106 id_, std::string(parser_status.message()).c_str());
107 }
108
109 // Fallback to legacy method for compatibility with enhanced validation
110 auto rom_data = rom_->data();
111
112 // Determine which subtype table to use and compute the tile data offset.
113 SubtypeTableInfo sti = GetSubtypeTable(id_);
114 // Apply offset first (for Type 2/3 objects), then mask
115 int index = ((id_ - sti.id_offset) & sti.index_mask);
116 int tile_ptr = sti.base_ptr + (index * 2);
117
118 // Enhanced bounds checking
119 if (tile_ptr < 0 || tile_ptr + 1 >= (int)rom_->size()) {
120 // Log error but don't crash
121 LOG_DEBUG("RoomObject", "Tile pointer out of bounds for object %04X", id_);
122 tiles_.clear();
123 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
125 return;
126 }
127
128 int tile_rel = (int16_t)((rom_data[tile_ptr + 1] << 8) + rom_data[tile_ptr]);
129 int pos = kRoomObjectTileAddress + tile_rel;
130 tile_data_ptr_ = pos;
131
132 // Enhanced bounds checking for tile data
133 if (pos < 0 || pos + 7 >= (int)rom_->size()) {
134 // Log error but don't crash
135 LOG_DEBUG("RoomObject", "Tile data position out of bounds for object %04X",
136 id_);
137 tiles_.clear();
138 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
140 return;
141 }
142
143 // Read tile data with validation
144 uint16_t w0 = (uint16_t)(rom_data[pos] | (rom_data[pos + 1] << 8));
145 uint16_t w1 = (uint16_t)(rom_data[pos + 2] | (rom_data[pos + 3] << 8));
146 uint16_t w2 = (uint16_t)(rom_data[pos + 4] | (rom_data[pos + 5] << 8));
147 uint16_t w3 = (uint16_t)(rom_data[pos + 6] | (rom_data[pos + 7] << 8));
148
149 tiles_.clear();
150 tiles_.push_back(gfx::WordToTileInfo(w0));
151 tiles_.push_back(gfx::WordToTileInfo(w1));
152 tiles_.push_back(gfx::WordToTileInfo(w2));
153 tiles_.push_back(gfx::WordToTileInfo(w3));
154 tile_count_ = 1;
155 tiles_loaded_ = true;
157}
158
160 tiles_.clear();
161 tiles_loaded_ = false;
162 tile_count_ = 0;
163 tile_data_ptr_ = -1;
165}
166
172
178
184
185void RoomObject::set_id(int16_t id) {
186 if (id_ == id) {
187 return;
188 }
189 id_ = id;
190 // A manual/custom routing override belongs to the old object identity.
191 // Built-in routing for the new ID is resolved through DrawRoutineRegistry.
192 all_bgs_ = false;
194}
195
197 if (rom_ == nullptr) {
198 return absl::InvalidArgumentError("ROM is null");
199 }
200
201 ObjectParser parser(rom_);
202 auto result = parser.ParseObject(id_);
203 if (!result.ok()) {
204 return result.status();
205 }
206
207 tiles_ = std::move(result.value());
208 tile_count_ = tiles_.size();
209 return absl::OkStatus();
210}
211
212absl::StatusOr<std::span<const gfx::TileInfo>> RoomObject::GetTiles() const {
213 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
214
215 if (tiles_.empty()) {
216 return absl::FailedPreconditionError("No tiles loaded for object");
217 }
218
219 return std::span<const gfx::TileInfo>(tiles_.data(), tiles_.size());
220}
221
222absl::StatusOr<const gfx::TileInfo*> RoomObject::GetTile(int index) const {
223 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
224
225 if (index < 0 || index >= static_cast<int>(tiles_.size())) {
226 return absl::OutOfRangeError(absl::StrFormat(
227 "Tile index %d out of range (0-%d)", index, tiles_.size() - 1));
228 }
229
230 return &tiles_[index];
231}
232
234 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
235
236 return tile_count_;
237}
238
239// ============================================================================
240// Object Encoding/Decoding Implementation (Phase 1, Task 1.1)
241// ============================================================================
242
243int RoomObject::DetermineObjectType(uint8_t b1, uint8_t b3) {
244 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
245 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
246 // would incorrectly trigger Type 3 decoding if we checked b3 first.
247 //
248 // Type 2: 111111xx xxxxyyyy yyiiiiii
249 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
250 if (b1 >= 0xFC) {
251 return 2;
252 }
253
254 // Type 3: Representable object IDs 0xF80-0xFFF
255 // These have b3 >= 0xF8 (top nibble is 0xF)
256 if (b3 >= 0xF8) {
257 return 3;
258 }
259
260 // Type 1: Representable object IDs 0x000-0x0F7
261 return 1;
262}
263
264RoomObject RoomObject::DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3,
265 uint8_t layer) {
266 uint8_t x = 0;
267 uint8_t y = 0;
268 uint8_t size = 0;
269 uint16_t id = 0;
270
271 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
272 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
273 // would incorrectly trigger Type 3 decoding if we checked b3 first.
274
275 // Type 2: 111111xx xxxxyyyy yyiiiiii
276 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
277 if (b1 >= 0xFC) {
278 id = (b3 & 0x3F) | 0x100;
279 x = ((b2 & 0xF0) >> 4) | ((b1 & 0x03) << 4);
280 y = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6);
281 size = 0;
282 LOG_DEBUG("ObjectParser",
283 "Type2: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
284 b2, b3, id, x, y, size);
285 }
286 // Type 3: xxxxxxii yyyyyyii 11111iii
287 // Discriminator: b3 >= 0xF8 (top 5 bits all 1)
288 else if (b3 >= 0xF8) {
289 id = (static_cast<uint16_t>(b3) << 4) | 0x80 |
290 ((static_cast<uint16_t>(b2 & 0x03) << 2) + (b1 & 0x03));
291 x = (b1 & 0xFC) >> 2;
292 y = (b2 & 0xFC) >> 2;
293 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
294 LOG_DEBUG("ObjectParser",
295 "Type3: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
296 b2, b3, id, x, y, size);
297 }
298 // Type 1: xxxxxxss yyyyyyss iiiiiiii
299 else {
300 id = b3;
301 x = (b1 & 0xFC) >> 2;
302 y = (b2 & 0xFC) >> 2;
303 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
304 LOG_DEBUG("ObjectParser",
305 "Type1: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
306 b2, b3, id, x, y, size);
307 }
308
309 return RoomObject(static_cast<int16_t>(id), x, y, size, layer);
310}
311
313 ObjectBytes bytes;
314
315 // Determine type based on object ID
316 if (id_ >= 0x100 && id_ < 0x200) {
317 // Type 2: 111111xx xxxxyyyy yyiiiiii (representable IDs 0x100-0x13F)
318 bytes.b1 = 0xFC | ((x_ & 0x30) >> 4);
319 bytes.b2 = ((x_ & 0x0F) << 4) | ((y_ & 0x3C) >> 2);
320 bytes.b3 = ((y_ & 0x03) << 6) | (id_ & 0x3F);
321 } else if (id_ >= 0xF00) {
322 // Type 3: xxxxxxii yyyyyyii 11111iii (representable IDs 0xF80-0xFFF)
323 bytes.b1 = (x_ << 2) | (id_ & 0x03);
324 bytes.b2 = (y_ << 2) | ((id_ >> 2) & 0x03);
325 bytes.b3 = (id_ >> 4) & 0xFF;
326 } else {
327 // Type 1: xxxxxxss yyyyyyss iiiiiiii (representable IDs 0x000-0x0F7)
328 uint8_t clamped_size = size_ > 15 ? 15 : size_;
329 bytes.b1 = (x_ << 2) | ((clamped_size >> 2) & 0x03);
330 bytes.b2 = (y_ << 2) | (clamped_size & 0x03);
331 bytes.b3 = static_cast<uint8_t>(id_);
332 }
333
334 return bytes;
335}
336
337bool IsRoomObjectSizeEditable(int object_id) {
338 return object_id >= 0x000 && object_id <= 0x0F7;
339}
340
341bool IsRoomObjectResizable(int object_id) {
342 return IsRoomObjectSizeEditable(object_id) &&
345}
346
347int RoomObjectSizeAxisStep(int object_id) {
348 if (!IsRoomObjectResizable(object_id)) {
349 return 0;
350 }
351 switch (DrawRoutineRegistry::Get().GetRoutineIdForObject(object_id)) {
356 return 4;
358 return 3;
363 return 2;
364 default:
365 return 0;
366 }
367}
368
369int RoomObjectSizeAxisTiles(int object_id, uint8_t size, bool horizontal) {
370 const int step = RoomObjectSizeAxisStep(object_id);
371 if (step == 0) {
372 return 0;
373 }
374 // USDASM $018CC7/$019733/$0193DC include fixed borders around their packed
375 // axes. Keep those extents in the existing dimension table, not UI formulas.
376 if (object_id == 0xC1 || object_id == 0xDC || object_id == 0xDD) {
377 const auto [width, height] =
378 ObjectDimensionTable::Get().GetDimensions(object_id, size & 0x0F);
379 return horizontal ? width : height;
380 }
381 const int axis = (size >> (horizontal ? 2 : 0)) & 0x03;
382 return (axis + 1) * step;
383}
384
385uint8_t ResizeRoomObjectByDelta(int object_id, uint8_t size, int delta,
386 bool horizontal) {
387 if (!IsRoomObjectResizable(object_id) || delta == 0) {
388 return size;
389 }
390 // Bound before adding so arbitrary caller deltas cannot overflow int.
391 delta = std::clamp(delta, -15, 15);
392 if (RoomObjectSizeAxisStep(object_id) != 0) {
393 const int shift = horizontal ? 2 : 0;
394 const int mask = 0x03 << shift;
395 const int axis = (size >> shift) & 0x03;
396 return static_cast<uint8_t>(((size & 0x0F) & ~mask) |
397 (std::clamp(axis + delta, 0, 3) << shift));
398 }
399 return static_cast<uint8_t>(
400 std::clamp(static_cast<int>(size) + delta, 0, 15));
401}
402
403uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size) {
404 if (IsRoomObjectSizeEditable(object_id)) {
405 return std::min<uint8_t>(requested_size, 15);
406 }
407 if (object_id >= 0x100 && object_id <= 0x13F) {
408 return 0;
409 }
410 if (object_id >= 0xF80 && object_id <= 0xFFF) {
411 return static_cast<uint8_t>(((object_id & 0x03) << 2) |
412 ((object_id >> 2) & 0x03));
413 }
414 return requested_size;
415}
416
417uint8_t DefaultRoomObjectSizeForPlacement(int object_id) {
418 if (IsRoomObjectSizeEditable(object_id)) {
419 return 2;
420 }
421 return CanonicalRoomObjectSize(object_id, 0);
422}
423
425 const int layer = object.GetLayerValue();
426 if (layer > 2) {
427 return absl::InvalidArgumentError(absl::StrFormat(
428 "Room object 0x%03X has invalid stream list index %d; expected 0..2",
429 object.id_, layer));
430 }
431 if (object.x_ > 63 || object.y_ > 63) {
432 return absl::InvalidArgumentError(absl::StrFormat(
433 "Room object 0x%03X has out-of-range coordinates (%d, %d); "
434 "expected 0..63",
435 object.id_, object.x_, object.y_));
436 }
437
438 int expected_type = 0;
439 if (object.id_ >= 0x000 && object.id_ <= 0x0F7) {
440 expected_type = 1;
441 } else if (object.id_ >= 0x100 && object.id_ <= 0x13F) {
442 expected_type = 2;
443 } else if (object.id_ >= 0xF80 && object.id_ <= 0xFFF) {
444 expected_type = 3;
445 } else {
446 return absl::InvalidArgumentError(absl::StrFormat(
447 "Room object ID 0x%03X is not representable without aliasing; "
448 "expected 0x000..0x0F7, 0x100..0x13F, or 0xF80..0xFFF",
449 object.id_));
450 }
451
452 const uint8_t canonical_size =
453 CanonicalRoomObjectSize(object.id_, object.size_);
454 if (object.size_ != canonical_size) {
455 if (expected_type == 1) {
456 return absl::InvalidArgumentError(
457 absl::StrFormat("Room object 0x%03X has noncanonical size 0x%02X; "
458 "Type 1 size must be 0x00..0x0F",
459 object.id_, object.size_));
460 }
461 return absl::InvalidArgumentError(absl::StrFormat(
462 "Room object 0x%03X has noncanonical size 0x%02X; expected 0x%02X",
463 object.id_, object.size_, canonical_size));
464 }
465
466 // Type 1/3 encode X in the upper six bits of byte 1. X=63 therefore
467 // produces the Type 2 discriminator (0xFC..0xFF) and decodes as another
468 // object family.
469 if (expected_type != 2 && object.x_ == 63) {
470 return absl::InvalidArgumentError(absl::StrFormat(
471 "Room object 0x%03X at x=63 collides with the Type 2 discriminator",
472 object.id_));
473 }
474
475 const RoomObject::ObjectBytes encoded = object.EncodeObjectToBytes();
476 if ((encoded.b1 == 0xFF && encoded.b2 == 0xFF) ||
477 (encoded.b1 == 0xF0 && encoded.b2 == 0xFF)) {
478 return absl::InvalidArgumentError(absl::StrFormat(
479 "Room object 0x%03X encodes with reserved stream-control prefix "
480 "0x%02X 0x%02X",
481 object.id_, encoded.b1, encoded.b2));
482 }
483
484 const int encoded_type =
485 RoomObject::DetermineObjectType(encoded.b1, encoded.b3);
486 if (encoded_type != expected_type) {
487 return absl::InvalidArgumentError(absl::StrFormat(
488 "Room object 0x%03X encodes with Type %d discriminator, expected "
489 "Type %d",
490 object.id_, encoded_type, expected_type));
491 }
492
494 encoded.b1, encoded.b2, encoded.b3,
495 static_cast<uint8_t>(object.GetLayerValue()));
496 if (decoded.id_ != object.id_ || decoded.x_ != object.x_ ||
497 decoded.y_ != object.y_) {
498 return absl::InvalidArgumentError(absl::StrFormat(
499 "Room object 0x%03X at (%d, %d) aliases to 0x%03X at (%d, %d) "
500 "after encoding",
501 object.id_, object.x_, object.y_, decoded.id_, decoded.x_, decoded.y_));
502 }
503
504 return absl::OkStatus();
505}
506
507} // namespace zelda3
508} // 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
static Flags & get()
Definition features.h:119
int GetSubtypeCount(int object_id) const
static CustomObjectManager & Get()
static DrawRoutineRegistry & Get()
std::pair< int, int > GetDimensions(int object_id, int size) const
static ObjectDimensionTable & Get()
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:89
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 ResizeRoomObjectByDelta(int object_id, uint8_t size, int delta, bool horizontal)
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
int RoomObjectSizeAxisStep(int object_id)
constexpr int kRoomObjectTileAddress
Definition room_object.h:48
ObjectOption operator~(ObjectOption option)
ObjectOption operator&(ObjectOption lhs, ObjectOption rhs)
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)
bool IsRoomObjectResizable(int object_id)
int RoomObjectSizeAxisTiles(int object_id, uint8_t size, bool horizontal)