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 "absl/status/status.h"
4#include "absl/strings/str_format.h"
5#include "util/log.h"
7
8namespace yaze {
9namespace zelda3 {
10
11namespace {
13 int base_ptr; // base address of subtype table in ROM (PC)
14 int index_mask; // mask to apply to object id for index
15 int id_offset; // offset to subtract from object_id before masking
16
17 SubtypeTableInfo(int base, int mask, int offset = 0)
18 : base_ptr(base), index_mask(mask), id_offset(offset) {}
19};
20
22 // Heuristic: 0x00-0xFF => subtype1, 0x100-0x1FF => subtype2, >=0xF80 =>
23 // subtype3. Type 3 IDs from decoding are 0xF80-0xFFF (b3 0xF8-0xFF shifted).
24 if (object_id >= 0xF80) {
25 // Type 3: IDs 0xF80-0xFFF map to table indices 0-127
26 // Subtract 0xF80 first, then mask with 0x7F
27 return SubtypeTableInfo(kRoomObjectSubtype3, 0x7F, 0xF80);
28 } else if (object_id >= 0x100) {
29 // Type 2: IDs 0x100-0x1FF map to table indices 0-255
30 return SubtypeTableInfo(kRoomObjectSubtype2, 0xFF, 0x100);
31 } else {
32 // Type 1: IDs 0x00-0xFF map directly to table indices
34 }
35}
36
37bool IsAllBgsObjectId(int object_id) {
38 // Objects that should be treated as drawing to both BG1 and BG2.
39 //
40 // NOTE: This is editor/runtime metadata for our renderer, not a ROM field.
41 // Keep this list in sync with DecodeObjectFromBytes behavior and any
42 // special-cased BothBG handling in ObjectDrawer.
43 const int id = object_id;
44 if ((id >= 0x03 && id <= 0x04) || // USDASM: Rightwards2x4spaced4_1to16 writes to both tilemaps
45 (id >= 0x63 && id <= 0x64) || // Routine 9 objects
46 // Routine 17 (Acute Diagonals)
47 id == 0x0C || id == 0x0D || id == 0x10 || id == 0x11 || id == 0x14 ||
48 id == 0x15 || id == 0x18 || id == 0x19 || id == 0x1C || id == 0x1D ||
49 id == 0x20 ||
50 // Routine 18 (Grave Diagonals)
51 id == 0x0E || id == 0x0F || id == 0x12 || id == 0x13 || id == 0x16 ||
52 id == 0x17 || id == 0x1A || id == 0x1B || id == 0x1E || id == 0x1F) {
53 return true;
54 }
55 return false;
56}
57} // namespace
58
60 return static_cast<ObjectOption>(static_cast<int>(lhs) |
61 static_cast<int>(rhs));
62}
63
65 return static_cast<ObjectOption>(static_cast<int>(lhs) &
66 static_cast<int>(rhs));
67}
68
70 return static_cast<ObjectOption>(static_cast<int>(lhs) ^
71 static_cast<int>(rhs));
72}
73
75 return static_cast<ObjectOption>(~static_cast<int>(option));
76}
77
78// NOTE: DrawTile was legacy ZScream code that is no longer used.
79// Modern rendering uses ObjectDrawer which draws directly to BackgroundBuffer
80// bitmaps.
81
83 if (tiles_loaded_) {
84 return;
85 }
86
87 if (rom_ == nullptr) {
88 // DEBUG: Log wall/corner objects
89 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
90 (id_ >= 0x100 && id_ <= 0x103)) {
91 LOG_DEBUG("RoomObject", "EnsureTilesLoaded: obj=0x%03X ROM is NULL!",
92 id_);
93 }
94 return;
95 }
96
97 // Try the new parser first - this is more efficient and accurate
98 auto parser_status = LoadTilesWithParser();
99 if (parser_status.ok()) {
100 tiles_loaded_ = true;
101 // DEBUG: Log 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 loaded %zu tiles via parser",
106 id_, tiles_.size());
107 }
108 return;
109 }
110
111 // DEBUG: Log parser failure for wall/corner objects
112 if (id_ == 0x001 || id_ == 0x002 || id_ == 0x061 || id_ == 0x062 ||
113 (id_ >= 0x100 && id_ <= 0x103)) {
114 LOG_DEBUG("RoomObject",
115 "EnsureTilesLoaded: obj=0x%03X parser failed: %s, trying legacy",
116 id_, std::string(parser_status.message()).c_str());
117 }
118
119 // Fallback to legacy method for compatibility with enhanced validation
120 auto rom_data = rom_->data();
121
122 // Determine which subtype table to use and compute the tile data offset.
123 SubtypeTableInfo sti = GetSubtypeTable(id_);
124 // Apply offset first (for Type 2/3 objects), then mask
125 int index = ((id_ - sti.id_offset) & sti.index_mask);
126 int tile_ptr = sti.base_ptr + (index * 2);
127
128 // Enhanced bounds checking
129 if (tile_ptr < 0 || tile_ptr + 1 >= (int)rom_->size()) {
130 // Log error but don't crash
131 LOG_DEBUG("RoomObject", "Tile pointer out of bounds for object %04X", id_);
132 tiles_.clear();
133 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
134 return;
135 }
136
137 int tile_rel = (int16_t)((rom_data[tile_ptr + 1] << 8) + rom_data[tile_ptr]);
138 int pos = kRoomObjectTileAddress + tile_rel;
139 tile_data_ptr_ = pos;
140
141 // Enhanced bounds checking for tile data
142 if (pos < 0 || pos + 7 >= (int)rom_->size()) {
143 // Log error but don't crash
144 LOG_DEBUG("RoomObject", "Tile data position out of bounds for object %04X",
145 id_);
146 tiles_.clear();
147 tiles_loaded_ = true; // Mark as loaded (empty) to prevent retry
148 return;
149 }
150
151 // Read tile data with validation
152 uint16_t w0 = (uint16_t)(rom_data[pos] | (rom_data[pos + 1] << 8));
153 uint16_t w1 = (uint16_t)(rom_data[pos + 2] | (rom_data[pos + 3] << 8));
154 uint16_t w2 = (uint16_t)(rom_data[pos + 4] | (rom_data[pos + 5] << 8));
155 uint16_t w3 = (uint16_t)(rom_data[pos + 6] | (rom_data[pos + 7] << 8));
156
157 tiles_.clear();
158 tiles_.push_back(gfx::WordToTileInfo(w0));
159 tiles_.push_back(gfx::WordToTileInfo(w1));
160 tiles_.push_back(gfx::WordToTileInfo(w2));
161 tiles_.push_back(gfx::WordToTileInfo(w3));
162 tile_count_ = 1;
163 tiles_loaded_ = true;
164}
165
167 tiles_.clear();
168 tiles_loaded_ = false;
169 tile_count_ = 0;
170 tile_data_ptr_ = -1;
171}
172
174 all_bgs_ = IsAllBgsObjectId(id_);
175}
176
177void RoomObject::set_id(int16_t id) {
178 if (id_ == id) {
179 return;
180 }
181 id_ = id;
184}
185
187 if (rom_ == nullptr) {
188 return absl::InvalidArgumentError("ROM is null");
189 }
190
191 ObjectParser parser(rom_);
192 auto result = parser.ParseObject(id_);
193 if (!result.ok()) {
194 return result.status();
195 }
196
197 tiles_ = std::move(result.value());
198 tile_count_ = tiles_.size();
199 return absl::OkStatus();
200}
201
202absl::StatusOr<std::span<const gfx::TileInfo>> RoomObject::GetTiles() const {
203 if (!tiles_loaded_) {
204 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
205 }
206
207 if (tiles_.empty()) {
208 return absl::FailedPreconditionError("No tiles loaded for object");
209 }
210
211 return std::span<const gfx::TileInfo>(tiles_.data(), tiles_.size());
212}
213
214absl::StatusOr<const gfx::TileInfo*> RoomObject::GetTile(int index) const {
215 if (!tiles_loaded_) {
216 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
217 }
218
219 if (index < 0 || index >= static_cast<int>(tiles_.size())) {
220 return absl::OutOfRangeError(absl::StrFormat(
221 "Tile index %d out of range (0-%d)", index, tiles_.size() - 1));
222 }
223
224 return &tiles_[index];
225}
226
228 if (!tiles_loaded_) {
229 const_cast<RoomObject*>(this)->EnsureTilesLoaded();
230 }
231
232 return tile_count_;
233}
234
235// ============================================================================
236// Object Encoding/Decoding Implementation (Phase 1, Task 1.1)
237// ============================================================================
238
239int RoomObject::DetermineObjectType(uint8_t b1, uint8_t b3) {
240 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
241 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
242 // would incorrectly trigger Type 3 decoding if we checked b3 first.
243 //
244 // Type 2: 111111xx xxxxyyyy yyiiiiii
245 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
246 if (b1 >= 0xFC) {
247 return 2;
248 }
249
250 // Type 3: Objects with ID >= 0xF00
251 // These have b3 >= 0xF8 (top nibble is 0xF)
252 if (b3 >= 0xF8) {
253 return 3;
254 }
255
256 // Type 1: Standard objects (ID 0x00-0xFF)
257 return 1;
258}
259
260RoomObject RoomObject::DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3,
261 uint8_t layer) {
262 uint8_t x = 0;
263 uint8_t y = 0;
264 uint8_t size = 0;
265 uint16_t id = 0;
266
267 // IMPORTANT: Check Type 2 FIRST to avoid boundary collision with Type 3.
268 // Type 2 objects with certain Y positions can produce b3 >= 0xF8, which
269 // would incorrectly trigger Type 3 decoding if we checked b3 first.
270
271 // Type 2: 111111xx xxxxyyyy yyiiiiii
272 // Discriminator: b1 >= 0xFC (top 6 bits all 1)
273 if (b1 >= 0xFC) {
274 id = (b3 & 0x3F) | 0x100;
275 x = ((b2 & 0xF0) >> 4) | ((b1 & 0x03) << 4);
276 y = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6);
277 size = 0;
278 LOG_DEBUG("ObjectParser",
279 "Type2: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
280 b2, b3, id, x, y, size);
281 }
282 // Type 3: xxxxxxii yyyyyyii 11111iii
283 // Discriminator: b3 >= 0xF8 (top 5 bits all 1)
284 else if (b3 >= 0xF8) {
285 id = (static_cast<uint16_t>(b3) << 4) | 0x80 |
286 ((static_cast<uint16_t>(b2 & 0x03) << 2) + (b1 & 0x03));
287 x = (b1 & 0xFC) >> 2;
288 y = (b2 & 0xFC) >> 2;
289 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
290 LOG_DEBUG("ObjectParser",
291 "Type3: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
292 b2, b3, id, x, y, size);
293 }
294 // Type 1: xxxxxxss yyyyyyss iiiiiiii
295 else {
296 id = b3;
297 x = (b1 & 0xFC) >> 2;
298 y = (b2 & 0xFC) >> 2;
299 size = ((b1 & 0x03) << 2) | (b2 & 0x03);
300 LOG_DEBUG("ObjectParser",
301 "Type1: b1=%02X b2=%02X b3=%02X -> id=%04X x=%d y=%d size=%d", b1,
302 b2, b3, id, x, y, size);
303 }
304
305 auto obj = RoomObject(static_cast<int16_t>(id), x, y, size, layer);
306 obj.RefreshDerivedFlagsFromId();
307
308 return obj;
309}
310
312 ObjectBytes bytes;
313
314 // Determine type based on object ID
315 if (id_ >= 0x100 && id_ < 0x200) {
316 // Type 2: 111111xx xxxxyyyy yyiiiiii
317 bytes.b1 = 0xFC | ((x_ & 0x30) >> 4);
318 bytes.b2 = ((x_ & 0x0F) << 4) | ((y_ & 0x3C) >> 2);
319 bytes.b3 = ((y_ & 0x03) << 6) | (id_ & 0x3F);
320 } else if (id_ >= 0xF00) {
321 // Type 3: xxxxxxii yyyyyyii 11111iii
322 bytes.b1 = (x_ << 2) | (id_ & 0x03);
323 bytes.b2 = (y_ << 2) | ((id_ >> 2) & 0x03);
324 bytes.b3 = (id_ >> 4) & 0xFF;
325 } else {
326 // Type 1: xxxxxxss yyyyyyss iiiiiiii
327 uint8_t clamped_size = size_ > 15 ? 15 : size_;
328 bytes.b1 = (x_ << 2) | ((clamped_size >> 2) & 0x03);
329 bytes.b2 = (y_ << 2) | (clamped_size & 0x03);
330 bytes.b3 = static_cast<uint8_t>(id_);
331 }
332
333 return bytes;
334}
335
336} // namespace zelda3
337} // namespace yaze
auto data() const
Definition rom.h:139
auto size() const
Definition rom.h:138
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:81
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:54
#define LOG_DEBUG(category, format,...)
Definition log.h:103
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
SubtypeTableInfo GetSubtypeTable(int object_id)
constexpr int kRoomObjectSubtype3
Definition room_object.h:46
ObjectOption operator|(ObjectOption lhs, ObjectOption rhs)
ObjectOption operator^(ObjectOption lhs, ObjectOption rhs)
constexpr int kRoomObjectSubtype1
Definition room_object.h:44
constexpr int kRoomObjectSubtype2
Definition room_object.h:45
constexpr int kRoomObjectTileAddress
Definition room_object.h:47
ObjectOption operator~(ObjectOption option)
ObjectOption operator&(ObjectOption lhs, ObjectOption rhs)