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"
13
14namespace yaze {
15namespace zelda3 {
16
17namespace {
18
19constexpr int kDummyTileCount = 512;
20
21struct AnchorPos {
22 int x = 0;
23 int y = 0;
24};
25
27 public:
28 explicit MeasurementDungeonState(bool water_face_active)
29 : water_face_active_(water_face_active) {}
30
31 bool IsChestOpen(int /*room_id*/, int /*chest_index*/) const override {
32 return false;
33 }
34 bool IsBigChestOpen() const override { return false; }
35 bool IsDoorOpen(int /*room_id*/, int /*door_index*/) const override {
36 return false;
37 }
38 bool IsDoorSwitchActive(int /*room_id*/) const override { return false; }
39 bool IsWaterFaceActive(int /*room_id*/) const override {
40 return water_face_active_;
41 }
42 bool IsDamFloodgateOpen(int /*room_id*/) const override { return false; }
43 bool IsWallMoved(int /*room_id*/) const override { return false; }
44 bool IsFloorBombable(int /*room_id*/) const override { return false; }
45 bool IsRupeeFloorCleared(int /*room_id*/) const override { return false; }
46 bool IsCrystalSwitchBlue() const override { return true; }
47
48 private:
49 bool water_face_active_ = false;
50};
51
53 // Geometry drives selection and hit-testing, so routines with a stable
54 // stateful expansion should replay their largest footprint here.
55 static const MeasurementDungeonState kActiveWaterFaceState(
56 /*water_face_active=*/true);
57
58 if (routine.id == DrawRoutineIds::kEmptyWaterFace) {
59 return &kActiveWaterFaceState;
60 }
61 return nullptr;
62}
63
64std::vector<gfx::TileInfo> MakeDummyTiles() {
65 std::vector<gfx::TileInfo> tiles;
66 tiles.reserve(kDummyTileCount);
67 for (int i = 0; i < kDummyTileCount; ++i) {
68 // Non-zero tile IDs so writes are detectable in the buffer
69 tiles.push_back(gfx::TileInfo(static_cast<uint16_t>(i + 1), 0,
70 /*v=*/false, /*h=*/false, /*o=*/false));
71 }
72 return tiles;
73}
74
75// Choose an anchor (x, y) that avoids buffer clipping for routines that draw
76// leftward or upward from the object origin. Without enough headroom, the
77// off-screen replay in MeasureRoutine loses rows/columns that the real draw
78// routine would have emitted, producing undersized bounds.
80 const RoomObject& object) {
81 AnchorPos anchor;
82 const int size_nibble = object.size_ & 0x0F;
83
84 // Acute diagonals (ids 5, 17) move upward by (y - s) per step.
86 (routine.id == 5 || routine.id == 17)) {
87 const int count = (routine.id == 5) ? (size_nibble + 7) : (size_nibble + 6);
88 const int max_anchor =
89 DrawContext::kMaxTilesY - 5; // 4 rows headroom below
90 anchor.y = std::clamp(count - 1, 0, std::max(0, max_anchor));
91 return anchor;
92 }
93
94 // Diagonal ceilings (Corner category, ids 75-78) anchor at the visual corner.
95 // The draw routine offsets base by -(side-1) for the mirrored axis, so
96 // bottom-anchored (76, 78) writes into negative Y and right-anchored (77, 78)
97 // writes into negative X without headroom. See corner_routines.cc.
99 routine.id >= 75 && routine.id <= 78) {
100 const int side = size_nibble + 4;
101 const bool mirror_x = (routine.id == 77 || routine.id == 78);
102 const bool mirror_y = (routine.id == 76 || routine.id == 78);
103 if (mirror_x) {
104 anchor.x = std::clamp(side - 1, 0, DrawContext::kMaxTilesX - 1);
105 }
106 if (mirror_y) {
107 anchor.y = std::clamp(side - 1, 0, DrawContext::kMaxTilesY - 1);
108 }
109 return anchor;
110 }
111
112 // The west-moving wall (0xCD) grows 8/16/24/32 fill columns left from its
113 // three-column platform anchor. Preserve that left extent during replay.
114 if (routine.id == DrawRoutineIds::kMovingWallWest) {
115 anchor.x = moving_wall::ObjectCountForSize(object.size_);
116 return anchor;
117 }
118
119 // Default: top-left of canvas.
120 return anchor;
121}
122
123} // namespace
124
126 static ObjectGeometry instance;
127 return instance;
128}
129
133
135 routines_.clear();
136 routine_map_.clear();
137
138 // Use the unified DrawRoutineRegistry to ensure consistent routine IDs
139 // between ObjectGeometry and ObjectDrawer
140 const auto& registry = DrawRoutineRegistry::Get();
141 routines_ = registry.GetAllRoutines();
142
143 for (const auto& info : routines_) {
144 routine_map_[info.id] = info;
145 }
146}
147
148const DrawRoutineInfo* ObjectGeometry::LookupRoutine(int routine_id) const {
149 auto it = routine_map_.find(routine_id);
150 if (it == routine_map_.end()) {
151 return nullptr;
152 }
153 return &it->second;
154}
155
156absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByRoutineId(
157 int routine_id, const RoomObject& object) const {
158 const DrawRoutineInfo* info = LookupRoutine(routine_id);
159 if (info == nullptr) {
160 return absl::InvalidArgumentError(
161 absl::StrFormat("Unknown routine id %d", routine_id));
162 }
163 auto bounds = MeasureRoutine(*info, object);
164 if (!bounds.ok()) {
165 return bounds;
166 }
167 return ApplySelectionBounds(*bounds, routine_id);
168}
169
170std::pair<int, int> ObjectGeometry::ResolveAnchor(int16_t object_id,
171 uint8_t size_byte) const {
172 const int routine_id =
174 if (routine_id < 0) {
175 return {0, 0};
176 }
177 const DrawRoutineInfo* info = LookupRoutine(routine_id);
178 if (info == nullptr) {
179 return {0, 0};
180 }
181 RoomObject probe(object_id, 0, 0, size_byte, 0);
182 AnchorPos anchor = ChooseAnchor(*info, probe);
183 return {anchor.x, anchor.y};
184}
185
186absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutine(
187 const DrawRoutineInfo& routine, const RoomObject& object) const {
188 return MeasureRoutineForState(routine, object,
189 SelectMeasurementState(routine));
190}
191
192absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutineForState(
193 const DrawRoutineInfo& routine, const RoomObject& object,
194 const DungeonState* state) const {
195 // Anchor object so routines that move upward or leftward stay within bounds.
196 RoomObject adjusted = object;
197 const AnchorPos anchor = ChooseAnchor(routine, object);
198 adjusted.x_ = anchor.x;
199 adjusted.y_ = anchor.y;
200
201 // Allocate a dummy tile list large enough for every routine. Chest geometry
202 // is payload-sensitive: the canonical subtype-3 small-chest objects
203 // (0xF99/0xF9A) use routine 39 with four tiles, while a 16+ tile span makes
204 // that routine take its 4x4 subtype-1 chest path. Big chests use their own
205 // routine (114), so constrain only those two small-chest object IDs to the
206 // routine's declared minimum payload.
207 static const std::vector<gfx::TileInfo> kTiles = MakeDummyTiles();
208 const uint16_t object_id = static_cast<uint16_t>(object.id_);
209 const bool is_small_chest = routine.id == DrawRoutineIds::kChest &&
210 (object_id == 0x0F99 || object_id == 0x0F9A);
211 const size_t measurement_tile_count =
212 is_small_chest ? static_cast<size_t>(routine.min_tiles) : kTiles.size();
213
216
217 DrawContext ctx{
218 .target_bg = bg,
219 .object = adjusted,
220 .tiles =
221 std::span<const gfx::TileInfo>(kTiles.data(), measurement_tile_count),
222 .state = state,
223 .rom = nullptr,
224 .room_id = 0,
225 .room_gfx_buffer = nullptr,
226 .secondary_bg = nullptr,
227 };
228
229 // Execute the routine to mark tiles in the buffer.
230 routine.function(ctx);
231
232 // Scan buffer for written tiles.
233 const int tiles_w = DrawContext::kMaxTilesX;
234 const int tiles_h = DrawContext::kMaxTilesY;
235
236 int min_x = std::numeric_limits<int>::max();
237 int min_y = std::numeric_limits<int>::max();
238 int max_x = std::numeric_limits<int>::min();
239 int max_y = std::numeric_limits<int>::min();
240
241 for (int y = 0; y < tiles_h; ++y) {
242 for (int x = 0; x < tiles_w; ++x) {
243 if (bg.GetTileAt(x, y) == 0)
244 continue;
245 min_x = std::min(min_x, x);
246 min_y = std::min(min_y, y);
247 max_x = std::max(max_x, x);
248 max_y = std::max(max_y, y);
249 }
250 }
251
252 // Handle routines that intentionally draw nothing.
253 if (max_x == std::numeric_limits<int>::min()) {
254 return GeometryBounds{};
255 }
256
257 GeometryBounds bounds;
258 bounds.min_x_tiles = min_x - anchor.x;
259 bounds.min_y_tiles = min_y - anchor.y;
260 bounds.width_tiles = (max_x - min_x) + 1;
261 bounds.height_tiles = (max_y - min_y) + 1;
262 bounds.is_bg2_overlay = false; // Default, set by MeasureForLayerCompositing
263 return bounds;
264}
265
266absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectId(
267 const RoomObject& object) const {
268 int routine_id = DrawRoutineRegistry::Get().GetRoutineIdForObject(object.id_);
269 if (routine_id < 0) {
270 return absl::NotFoundError(
271 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
272 }
273
274 // Check cache
275 CacheKey key{routine_id, object.id_, object.size_};
276 auto cache_it = cache_.find(key);
277 if (cache_it != cache_.end()) {
278 return cache_it->second;
279 }
280
281 // Measure and cache
282 auto result = MeasureByRoutineId(routine_id, object);
283 if (result.ok()) {
284 cache_[key] = *result;
285 }
286 return result;
287}
288
289absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectIdForState(
290 const RoomObject& object, const DungeonState* state) const {
291 const int routine_id =
293 if (routine_id < 0) {
294 return absl::NotFoundError(
295 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
296 }
297
298 const DrawRoutineInfo* routine = LookupRoutine(routine_id);
299 if (routine == nullptr) {
300 return absl::InvalidArgumentError(
301 absl::StrFormat("Unknown routine id %d", routine_id));
302 }
303
304 auto bounds = MeasureRoutineForState(*routine, object, state);
305 if (!bounds.ok()) {
306 return bounds;
307 }
308 return ApplySelectionBounds(*bounds, routine_id);
309}
310
312 cache_.clear();
313}
314
315absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureForLayerCompositing(
316 int routine_id, const RoomObject& object) const {
317 auto result = MeasureByRoutineId(routine_id, object);
318 if (!result.ok()) {
319 return result;
320 }
321
322 GeometryBounds bounds = *result;
323
324 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
325 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
326 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
327
328 return bounds;
329}
330
332 // Layer 1 routines are those that explicitly draw to BG2 only.
333 // Most objects draw to the current layer pointer; this list is for
334 // routines that have special BG2-only behavior.
335 //
336 // From ASM analysis:
337 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
338 // - The routine itself doesn't determine the layer; the object's position
339 // in the room data determines which layer pointer it uses
340 //
341 // This method is primarily for documentation; actual layer determination
342 // comes from the object's layer_ field set during room loading.
343 (void)
344 routine_id; // Currently unused - layer determined by object, not routine
345 return false;
346}
347
349 // Diagonal ceiling routines from draw_routine_registry.h
350 // kDiagonalCeilingTopLeft = 75
351 // kDiagonalCeilingBottomLeft = 76
352 // kDiagonalCeilingTopRight = 77
353 // kDiagonalCeilingBottomRight = 78
354 return routine_id >= 75 && routine_id <= 78;
355}
356
358 GeometryBounds render_bounds, int routine_id) {
359 if (!IsDiagonalCeilingRoutine(routine_id)) {
360 // Not a diagonal ceiling - return render bounds unchanged
361 return render_bounds;
362 }
363
364 // For diagonal ceilings, compute a tighter selection box.
365 // The visual triangle fills roughly 50% of the bounding box area.
366 // We use a selection rectangle that's 70% of the size, centered,
367 // to provide a reasonable hit target without excessive false positives.
368
369 int reduced_width = std::max(1, (render_bounds.width_tiles * 7) / 10);
370 int reduced_height = std::max(1, (render_bounds.height_tiles * 7) / 10);
371
372 // Center the reduced selection box within the render bounds
373 int offset_x = (render_bounds.width_tiles - reduced_width) / 2;
374 int offset_y = (render_bounds.height_tiles - reduced_height) / 2;
375
376 SelectionRect selection;
377 selection.x_tiles = render_bounds.min_x_tiles + offset_x;
378 selection.y_tiles = render_bounds.min_y_tiles + offset_y;
379 selection.width_tiles = reduced_width;
380 selection.height_tiles = reduced_height;
381
382 render_bounds.selection_bounds = selection;
383 return render_bounds;
384}
385
386} // namespace zelda3
387} // namespace yaze
uint16_t GetTileAt(int x, int y) const
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
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.
static bool IsDiagonalCeilingRoutine(int routine_id)
Check if a routine ID corresponds to a diagonal ceiling.
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 GeometryBounds ApplySelectionBounds(GeometryBounds render_bounds, int routine_id)
Compute tighter selection bounds for diagonal shapes.
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
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.
std::optional< SelectionRect > selection_bounds
Simple rectangle for selection bounds.