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 // Ganon's triforce floor draws its bottom-left 4x4 two columns left of the
114 // anchor (RoomDraw_GanonTriforceFloorDecor, ADC #$01FC).
116 anchor.x = 2;
117 return anchor;
118 }
119
120 // Default: top-left of canvas.
121 return anchor;
122}
123
124} // namespace
125
127 static ObjectGeometry instance;
128 return instance;
129}
130
134
136 routines_.clear();
137 routine_map_.clear();
138
139 // Use the unified DrawRoutineRegistry to ensure consistent routine IDs
140 // between ObjectGeometry and ObjectDrawer
141 const auto& registry = DrawRoutineRegistry::Get();
142 routines_ = registry.GetAllRoutines();
143
144 for (const auto& info : routines_) {
145 routine_map_[info.id] = info;
146 }
147}
148
149const DrawRoutineInfo* ObjectGeometry::LookupRoutine(int routine_id) const {
150 auto it = routine_map_.find(routine_id);
151 if (it == routine_map_.end()) {
152 return nullptr;
153 }
154 return &it->second;
155}
156
157absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByRoutineId(
158 int routine_id, const RoomObject& object) const {
159 const DrawRoutineInfo* info = LookupRoutine(routine_id);
160 if (info == nullptr) {
161 return absl::InvalidArgumentError(
162 absl::StrFormat("Unknown routine id %d", routine_id));
163 }
164 return MeasureRoutine(*info, object);
165}
166
167std::pair<int, int> ObjectGeometry::ResolveAnchor(int16_t object_id,
168 uint8_t size_byte) const {
169 const int routine_id =
171 if (routine_id < 0) {
172 return {0, 0};
173 }
174 const DrawRoutineInfo* info = LookupRoutine(routine_id);
175 if (info == nullptr) {
176 return {0, 0};
177 }
178 RoomObject probe(object_id, 0, 0, size_byte, 0);
179 AnchorPos anchor = ChooseAnchor(*info, probe);
180 return {anchor.x, anchor.y};
181}
182
183absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutine(
184 const DrawRoutineInfo& routine, const RoomObject& object) const {
185 return MeasureRoutineForState(routine, object,
186 SelectMeasurementState(routine));
187}
188
189absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutineForState(
190 const DrawRoutineInfo& routine, const RoomObject& object,
191 const DungeonState* state) const {
192 // Anchor object so routines that move upward or leftward stay within bounds.
193 RoomObject adjusted = object;
194 const AnchorPos anchor = ChooseAnchor(routine, object);
195 adjusted.x_ = anchor.x;
196 adjusted.y_ = anchor.y;
197
198 // Allocate a dummy tile list large enough for every routine. Chest geometry
199 // is payload-sensitive: the canonical subtype-3 small-chest objects
200 // (0xF99/0xF9A) use routine 39 with four tiles, while a 16+ tile span makes
201 // that routine take its 4x4 subtype-1 chest path. Big chests use their own
202 // routine (114), so constrain only those two small-chest object IDs to the
203 // routine's declared minimum payload.
204 static const std::vector<gfx::TileInfo> kTiles = MakeDummyTiles();
205 const uint16_t object_id = static_cast<uint16_t>(object.id_);
206 const bool is_small_chest = routine.id == DrawRoutineIds::kChest &&
207 (object_id == 0x0F99 || object_id == 0x0F9A);
208 const size_t measurement_tile_count =
209 is_small_chest ? static_cast<size_t>(routine.min_tiles) : kTiles.size();
210
213
214 DrawContext ctx{
215 .target_bg = bg,
216 .object = adjusted,
217 .tiles =
218 std::span<const gfx::TileInfo>(kTiles.data(), measurement_tile_count),
219 .state = state,
220 .rom = nullptr,
221 .room_id = 0,
222 .room_gfx_buffer = nullptr,
223 .secondary_bg = nullptr,
224 };
225
226 // Execute the routine to mark tiles in the buffer.
227 routine.function(ctx);
228
229 // Scan buffer for written tiles.
230 const int tiles_w = DrawContext::kMaxTilesX;
231 const int tiles_h = DrawContext::kMaxTilesY;
232
233 int min_x = std::numeric_limits<int>::max();
234 int min_y = std::numeric_limits<int>::max();
235 int max_x = std::numeric_limits<int>::min();
236 int max_y = std::numeric_limits<int>::min();
237
238 for (int y = 0; y < tiles_h; ++y) {
239 for (int x = 0; x < tiles_w; ++x) {
240 if (bg.GetTileAt(x, y) == 0)
241 continue;
242 min_x = std::min(min_x, x);
243 min_y = std::min(min_y, y);
244 max_x = std::max(max_x, x);
245 max_y = std::max(max_y, y);
246 }
247 }
248
249 // Handle routines that intentionally draw nothing.
250 if (max_x == std::numeric_limits<int>::min()) {
251 return GeometryBounds{};
252 }
253
254 GeometryBounds bounds;
255 bounds.min_x_tiles = min_x - anchor.x;
256 bounds.min_y_tiles = min_y - anchor.y;
257 bounds.width_tiles = (max_x - min_x) + 1;
258 bounds.height_tiles = (max_y - min_y) + 1;
259 bounds.is_bg2_overlay = false; // Default, set by MeasureForLayerCompositing
260 return bounds;
261}
262
263absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectId(
264 const RoomObject& object) const {
265 int routine_id = DrawRoutineRegistry::Get().GetRoutineIdForObject(object.id_);
266 if (routine_id < 0) {
267 return absl::NotFoundError(
268 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
269 }
270
271 // Check cache
272 CacheKey key{routine_id, object.id_, object.size_,
274 auto cache_it = cache_.find(key);
275 if (cache_it != cache_.end()) {
276 return cache_it->second;
277 }
278
279 // Measure and cache
280 auto result = MeasureByRoutineId(routine_id, object);
281 if (result.ok()) {
282 cache_[key] = *result;
283 }
284 return result;
285}
286
287absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectIdForState(
288 const RoomObject& object, const DungeonState* state) const {
289 const int routine_id =
291 if (routine_id < 0) {
292 return absl::NotFoundError(
293 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
294 }
295
296 const DrawRoutineInfo* routine = LookupRoutine(routine_id);
297 if (routine == nullptr) {
298 return absl::InvalidArgumentError(
299 absl::StrFormat("Unknown routine id %d", routine_id));
300 }
301
302 return MeasureRoutineForState(*routine, object, state);
303}
304
306 cache_.clear();
307}
308
309absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureForLayerCompositing(
310 int routine_id, const RoomObject& object) const {
311 auto result = MeasureByRoutineId(routine_id, object);
312 if (!result.ok()) {
313 return result;
314 }
315
316 GeometryBounds bounds = *result;
317
318 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
319 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
320 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
321
322 return bounds;
323}
324
326 // Layer 1 routines are those that explicitly draw to BG2 only.
327 // Most objects draw to the current layer pointer; this list is for
328 // routines that have special BG2-only behavior.
329 //
330 // From ASM analysis:
331 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
332 // - The routine itself doesn't determine the layer; the object's position
333 // in the room data determines which layer pointer it uses
334 //
335 // This method is primarily for documentation; actual layer determination
336 // comes from the object's layer_ field set during room loading.
337 (void)
338 routine_id; // Currently unused - layer determined by object, not routine
339 return false;
340}
341
342} // namespace zelda3
343} // 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.