yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_geometry.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <limits>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
14
15namespace yaze {
16namespace zelda3 {
17
18namespace {
19
20constexpr int kDummyTileCount = 512;
21
22struct AnchorPos {
23 int x = 0;
24 int y = 0;
25};
26
28 public:
29 explicit MeasurementDungeonState(bool water_face_active)
30 : water_face_active_(water_face_active) {}
31
32 bool IsChestOpen(int /*room_id*/, int /*chest_index*/) const override {
33 return false;
34 }
35 bool IsBigChestOpen() const override { return false; }
36 bool IsDoorOpen(int /*room_id*/, int /*door_index*/) const override {
37 return false;
38 }
39 bool IsDoorSwitchActive(int /*room_id*/) const override { return false; }
40 bool IsWaterFaceActive(int /*room_id*/) const override {
41 return water_face_active_;
42 }
43 bool IsDamFloodgateOpen(int /*room_id*/) const override { return false; }
44 bool IsWallMoved(int /*room_id*/) const override { return false; }
45 bool IsFloorBombable(int /*room_id*/) const override { return false; }
46 bool IsRupeeFloorCleared(int /*room_id*/) const override { return false; }
47 bool IsCrystalSwitchBlue() const override { return true; }
48
49 private:
50 bool water_face_active_ = false;
51};
52
54 // Geometry drives selection and hit-testing, so routines with a stable
55 // stateful expansion should replay their largest footprint here.
56 static const MeasurementDungeonState kActiveWaterFaceState(
57 /*water_face_active=*/true);
58
59 if (routine.id == DrawRoutineIds::kEmptyWaterFace) {
60 return &kActiveWaterFaceState;
61 }
62 return nullptr;
63}
64
65std::vector<gfx::TileInfo> MakeDummyTiles() {
66 std::vector<gfx::TileInfo> tiles;
67 tiles.reserve(kDummyTileCount);
68 for (int i = 0; i < kDummyTileCount; ++i) {
69 // Non-zero tile IDs so writes are detectable in the buffer
70 tiles.push_back(gfx::TileInfo(static_cast<uint16_t>(i + 1), 0,
71 /*v=*/false, /*h=*/false, /*o=*/false));
72 }
73 return tiles;
74}
75
76// Choose an anchor (x, y) that avoids buffer clipping for routines that draw
77// leftward or upward from the object origin. Without enough headroom, the
78// off-screen replay in MeasureRoutine loses rows/columns that the real draw
79// routine would have emitted, producing undersized bounds.
81 const RoomObject& object) {
82 AnchorPos anchor;
83 const int size_nibble = object.size_ & 0x0F;
84
85 // Acute diagonals (ids 5, 17) move upward by (y - s) per step.
87 (routine.id == 5 || routine.id == 17)) {
88 const int count = size_nibble + 6;
89 const int max_anchor =
90 DrawContext::kMaxTilesY - 5; // 4 rows headroom below
91 anchor.y = std::clamp(count - 1, 0, std::max(0, max_anchor));
92 return anchor;
93 }
94
95 // Only the bottom-right diagonal ceiling (78) walks upward from the encoded
96 // origin. Give that replay enough Y headroom so geometry is not clipped.
98 routine.id >= 75 && routine.id <= 78) {
99 const int side = size_nibble + 4;
100 if (routine.id == 78) {
101 anchor.y = std::clamp(side - 1, 0, DrawContext::kMaxTilesY - 1);
102 }
103 return anchor;
104 }
105
106 // The west-moving wall (0xCD) grows 8/16/24/32 fill columns left from its
107 // three-column platform anchor. Preserve that left extent during replay.
108 if (routine.id == DrawRoutineIds::kMovingWallWest) {
109 anchor.x = moving_wall::ObjectCountForSize(object.size_);
110 return anchor;
111 }
112
113 // Default: top-left of canvas.
114 return anchor;
115}
116
117} // namespace
118
120 static ObjectGeometry instance;
121 return instance;
122}
123
127
129 routines_.clear();
130 routine_map_.clear();
131
132 // Use the unified DrawRoutineRegistry to ensure consistent routine IDs
133 // between ObjectGeometry and ObjectDrawer
134 const auto& registry = DrawRoutineRegistry::Get();
135 routines_ = registry.GetAllRoutines();
136
137 for (const auto& info : routines_) {
138 routine_map_[info.id] = info;
139 }
140}
141
142const DrawRoutineInfo* ObjectGeometry::LookupRoutine(int routine_id) const {
143 auto it = routine_map_.find(routine_id);
144 if (it == routine_map_.end()) {
145 return nullptr;
146 }
147 return &it->second;
148}
149
150absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByRoutineId(
151 int routine_id, const RoomObject& object) const {
152 const DrawRoutineInfo* info = LookupRoutine(routine_id);
153 if (info == nullptr) {
154 return absl::InvalidArgumentError(
155 absl::StrFormat("Unknown routine id %d", routine_id));
156 }
157 return MeasureRoutine(*info, object);
158}
159
160std::pair<int, int> ObjectGeometry::ResolveAnchor(int16_t object_id,
161 uint8_t size_byte) const {
162 const int routine_id =
164 if (routine_id < 0) {
165 return {0, 0};
166 }
167 const DrawRoutineInfo* info = LookupRoutine(routine_id);
168 if (info == nullptr) {
169 return {0, 0};
170 }
171 RoomObject probe(object_id, 0, 0, size_byte, 0);
172 AnchorPos anchor = ChooseAnchor(*info, probe);
173 return {anchor.x, anchor.y};
174}
175
176absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutine(
177 const DrawRoutineInfo& routine, const RoomObject& object) const {
178 return MeasureRoutineForState(routine, object,
179 SelectMeasurementState(routine));
180}
181
182absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutineForState(
183 const DrawRoutineInfo& routine, const RoomObject& object,
184 const DungeonState* state) const {
185 // Anchor object so routines that move upward or leftward stay within bounds.
186 RoomObject adjusted = object;
187 const AnchorPos anchor = ChooseAnchor(routine, object);
188 adjusted.x_ = anchor.x;
189 adjusted.y_ = anchor.y;
190
191 // Allocate a dummy tile list large enough for every routine. Chest geometry
192 // is payload-sensitive: the canonical subtype-3 small-chest objects
193 // (0xF99/0xF9A) use routine 39 with four tiles, while a 16+ tile span makes
194 // that routine take its 4x4 subtype-1 chest path. Big chests use their own
195 // routine (114), so constrain only those two small-chest object IDs to the
196 // routine's declared minimum payload.
197 static const std::vector<gfx::TileInfo> kTiles = MakeDummyTiles();
198 const uint16_t object_id = static_cast<uint16_t>(object.id_);
199 const bool is_small_chest = routine.id == DrawRoutineIds::kChest &&
200 (object_id == 0x0F99 || object_id == 0x0F9A);
201 const size_t measurement_tile_count =
202 is_small_chest ? static_cast<size_t>(routine.min_tiles) : kTiles.size();
203
206
207 DrawContext ctx{
208 .target_bg = bg,
209 .object = adjusted,
210 .tiles =
211 std::span<const gfx::TileInfo>(kTiles.data(), measurement_tile_count),
212 .state = state,
213 .rom = nullptr,
214 .room_id = 0,
215 .room_gfx_buffer = nullptr,
216 .secondary_bg = nullptr,
217 };
218
219 // Execute the routine to mark tiles in the buffer.
220 routine.function(ctx);
221
222 // Scan buffer for written tiles.
223 const int tiles_w = DrawContext::kMaxTilesX;
224 const int tiles_h = DrawContext::kMaxTilesY;
225
226 int min_x = std::numeric_limits<int>::max();
227 int min_y = std::numeric_limits<int>::max();
228 int max_x = std::numeric_limits<int>::min();
229 int max_y = std::numeric_limits<int>::min();
230
231 for (int y = 0; y < tiles_h; ++y) {
232 for (int x = 0; x < tiles_w; ++x) {
233 if (bg.GetTileAt(x, y) == 0)
234 continue;
235 min_x = std::min(min_x, x);
236 min_y = std::min(min_y, y);
237 max_x = std::max(max_x, x);
238 max_y = std::max(max_y, y);
239 }
240 }
241
242 // Handle routines that intentionally draw nothing.
243 if (max_x == std::numeric_limits<int>::min()) {
244 return GeometryBounds{};
245 }
246
247 GeometryBounds bounds;
248 bounds.min_x_tiles = min_x - anchor.x;
249 bounds.min_y_tiles = min_y - anchor.y;
250 bounds.width_tiles = (max_x - min_x) + 1;
251 bounds.height_tiles = (max_y - min_y) + 1;
252 bounds.is_bg2_overlay = false; // Default, set by MeasureForLayerCompositing
253 return bounds;
254}
255
256absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectId(
257 const RoomObject& object) const {
258 int routine_id = DrawRoutineRegistry::Get().GetRoutineIdForObject(object.id_);
259 if (routine_id < 0) {
260 return absl::NotFoundError(
261 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
262 }
263
264 // Check cache
265 CacheKey key{routine_id, object.id_, object.size_,
267 auto cache_it = cache_.find(key);
268 if (cache_it != cache_.end()) {
269 return cache_it->second;
270 }
271
272 // Measure and cache
273 auto result = MeasureByRoutineId(routine_id, object);
274 if (result.ok()) {
275 cache_[key] = *result;
276 }
277 return result;
278}
279
280absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectIdForState(
281 const RoomObject& object, const DungeonState* state) const {
282 const int routine_id =
284 if (routine_id < 0) {
285 return absl::NotFoundError(
286 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
287 }
288
289 const DrawRoutineInfo* routine = LookupRoutine(routine_id);
290 if (routine == nullptr) {
291 return absl::InvalidArgumentError(
292 absl::StrFormat("Unknown routine id %d", routine_id));
293 }
294
295 return MeasureRoutineForState(*routine, object, state);
296}
297
299 cache_.clear();
300}
301
302absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureForLayerCompositing(
303 int routine_id, const RoomObject& object) const {
304 auto result = MeasureByRoutineId(routine_id, object);
305 if (!result.ok()) {
306 return result;
307 }
308
309 GeometryBounds bounds = *result;
310
311 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
312 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
313 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
314
315 return bounds;
316}
317
319 // Layer 1 routines are those that explicitly draw to BG2 only.
320 // Most objects draw to the current layer pointer; this list is for
321 // routines that have special BG2-only behavior.
322 //
323 // From ASM analysis:
324 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
325 // - The routine itself doesn't determine the layer; the object's position
326 // in the room data determines which layer pointer it uses
327 //
328 // This method is primarily for documentation; actual layer determination
329 // comes from the object's layer_ field set during room loading.
330 (void)
331 routine_id; // Currently unused - layer determined by object, not routine
332 return false;
333}
334
335} // namespace zelda3
336} // namespace yaze
uint16_t GetTileAt(int x, int y) const
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
static CustomObjectManager & Get()
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
Interface for accessing dungeon game state.
Side-car geometry engine that replays draw routines against an off-screen buffer to calculate real ex...
std::pair< int, int > ResolveAnchor(int16_t object_id, uint8_t size_byte) const
Resolve the canvas anchor (x, y) for a given object's draw routine.
std::vector< DrawRoutineInfo > routines_
absl::StatusOr< GeometryBounds > MeasureForLayerCompositing(int routine_id, const RoomObject &object) const
Measure bounds for a BG2 overlay object and mark it for masking.
absl::StatusOr< GeometryBounds > MeasureRoutine(const DrawRoutineInfo &routine, const RoomObject &object) const
std::unordered_map< int, DrawRoutineInfo > routine_map_
absl::StatusOr< GeometryBounds > MeasureByObjectIdForState(const RoomObject &object, const DungeonState *state) const
absl::StatusOr< GeometryBounds > MeasureByObjectId(const RoomObject &object) const
const DrawRoutineInfo * LookupRoutine(int routine_id) const
absl::StatusOr< GeometryBounds > MeasureByRoutineId(int routine_id, const RoomObject &object) const
static ObjectGeometry & Get()
std::unordered_map< CacheKey, GeometryBounds, CacheKeyHash > cache_
static bool IsLayerOneRoutine(int routine_id)
Get list of routine IDs that draw to BG2 layer.
absl::StatusOr< GeometryBounds > MeasureRoutineForState(const DrawRoutineInfo &routine, const RoomObject &object, const DungeonState *state) const
const std::vector< gfx::TileInfo > & tiles() const
Definition room_object.h:99
AnchorPos ChooseAnchor(const DrawRoutineInfo &routine, const RoomObject &object)
const DungeonState * SelectMeasurementState(const DrawRoutineInfo &routine)
constexpr int ObjectCountForSize(int size)
Context passed to draw routines containing all necessary state.
static constexpr int kMaxTilesY
gfx::BackgroundBuffer & target_bg
static constexpr int kMaxTilesX
Metadata about a draw routine.
Bounding box result for a draw routine execution.