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"
11
12namespace yaze {
13namespace zelda3 {
14
15namespace {
16
17constexpr int kDummyTileCount = 512;
18constexpr int kAnchorX = 0;
19
20std::vector<gfx::TileInfo> MakeDummyTiles() {
21 std::vector<gfx::TileInfo> tiles;
22 tiles.reserve(kDummyTileCount);
23 for (int i = 0; i < kDummyTileCount; ++i) {
24 // Non-zero tile IDs so writes are detectable in the buffer
25 tiles.push_back(gfx::TileInfo(static_cast<uint16_t>(i + 1), 0,
26 /*v=*/false, /*h=*/false, /*o=*/false));
27 }
28 return tiles;
29}
30
31// Choose an anchor Y that avoids clipping for routines that move upward.
32int ChooseAnchorY(const DrawRoutineInfo& routine, const RoomObject& object) {
33 // Acute diagonals move upward (y - s); give them headroom.
35 (routine.id == 5 || routine.id == 17)) {
36 int size = object.size_ & 0x0F;
37 int count = (routine.id == 5) ? (size + 7) : (size + 6);
38 // Need count-1 tiles of upward travel plus 4 extra rows for the column.
39 int min_anchor = count - 1;
40 int max_anchor = DrawContext::kMaxTilesY - 5; // leave 4 rows below ceiling
41 if (min_anchor > max_anchor)
42 min_anchor = max_anchor;
43 return std::clamp(min_anchor, 0, max_anchor);
44 }
45
46 // Default: start at top of canvas.
47 return 0;
48}
49
50} // namespace
51
53 static ObjectGeometry instance;
54 return instance;
55}
56
60
62 routines_.clear();
63 routine_map_.clear();
64
65 // Use the unified DrawRoutineRegistry to ensure consistent routine IDs
66 // between ObjectGeometry and ObjectDrawer
67 const auto& registry = DrawRoutineRegistry::Get();
68 routines_ = registry.GetAllRoutines();
69
70 for (const auto& info : routines_) {
71 routine_map_[info.id] = info;
72 }
73}
74
75const DrawRoutineInfo* ObjectGeometry::LookupRoutine(int routine_id) const {
76 auto it = routine_map_.find(routine_id);
77 if (it == routine_map_.end()) {
78 return nullptr;
79 }
80 return &it->second;
81}
82
83absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByRoutineId(
84 int routine_id, const RoomObject& object) const {
85 const DrawRoutineInfo* info = LookupRoutine(routine_id);
86 if (info == nullptr) {
87 return absl::InvalidArgumentError(
88 absl::StrFormat("Unknown routine id %d", routine_id));
89 }
90 return MeasureRoutine(*info, object);
91}
92
93absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutine(
94 const DrawRoutineInfo& routine, const RoomObject& object) const {
95 // Anchor object so routines that move upward stay within bounds.
96 RoomObject adjusted = object;
97 adjusted.x_ = kAnchorX;
98 const int anchor_y = ChooseAnchorY(routine, object);
99 adjusted.y_ = anchor_y;
100
101 // Allocate a dummy tile list large enough for every routine.
102 static const std::vector<gfx::TileInfo> kTiles = MakeDummyTiles();
103
106
107 DrawContext ctx{
108 .target_bg = bg,
109 .object = adjusted,
110 .tiles = std::span<const gfx::TileInfo>(kTiles.data(), kTiles.size()),
111 .state = nullptr,
112 .rom = nullptr,
113 .room_id = 0,
114 .room_gfx_buffer = nullptr,
115 .secondary_bg = nullptr,
116 };
117
118 // Execute the routine to mark tiles in the buffer.
119 routine.function(ctx);
120
121 // Scan buffer for written tiles.
122 const int tiles_w = DrawContext::kMaxTilesX;
123 const int tiles_h = DrawContext::kMaxTilesY;
124
125 int min_x = std::numeric_limits<int>::max();
126 int min_y = std::numeric_limits<int>::max();
127 int max_x = std::numeric_limits<int>::min();
128 int max_y = std::numeric_limits<int>::min();
129
130 for (int y = 0; y < tiles_h; ++y) {
131 for (int x = 0; x < tiles_w; ++x) {
132 if (bg.GetTileAt(x, y) == 0)
133 continue;
134 min_x = std::min(min_x, x);
135 min_y = std::min(min_y, y);
136 max_x = std::max(max_x, x);
137 max_y = std::max(max_y, y);
138 }
139 }
140
141 // Handle routines that intentionally draw nothing.
142 if (max_x == std::numeric_limits<int>::min()) {
143 return GeometryBounds{};
144 }
145
146 GeometryBounds bounds;
147 bounds.min_x_tiles = min_x - kAnchorX;
148 bounds.min_y_tiles = min_y - anchor_y;
149 bounds.width_tiles = (max_x - min_x) + 1;
150 bounds.height_tiles = (max_y - min_y) + 1;
151 bounds.is_bg2_overlay = false; // Default, set by MeasureForLayerCompositing
152 return bounds;
153}
154
155absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectId(
156 const RoomObject& object) const {
157 int routine_id =
159 if (routine_id < 0) {
160 return absl::NotFoundError(
161 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
162 }
163
164 // Check cache
165 CacheKey key{routine_id, object.id_, object.size_};
166 auto cache_it = cache_.find(key);
167 if (cache_it != cache_.end()) {
168 return cache_it->second;
169 }
170
171 // Measure and cache
172 auto result = MeasureByRoutineId(routine_id, object);
173 if (result.ok()) {
174 cache_[key] = *result;
175 }
176 return result;
177}
178
180
181absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureForLayerCompositing(
182 int routine_id, const RoomObject& object) const {
183 auto result = MeasureByRoutineId(routine_id, object);
184 if (!result.ok()) {
185 return result;
186 }
187
188 GeometryBounds bounds = *result;
189
190 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
191 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
192 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
193
194 return bounds;
195}
196
198 // Layer 1 routines are those that explicitly draw to BG2 only.
199 // Most objects draw to the current layer pointer; this list is for
200 // routines that have special BG2-only behavior.
201 //
202 // From ASM analysis:
203 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
204 // - The routine itself doesn't determine the layer; the object's position
205 // in the room data determines which layer pointer it uses
206 //
207 // This method is primarily for documentation; actual layer determination
208 // comes from the object's layer_ field set during room loading.
209 (void)
210 routine_id; // Currently unused - layer determined by object, not routine
211 return false;
212}
213
215 // Diagonal ceiling routines from draw_routine_registry.h
216 // kDiagonalCeilingTopLeft = 75
217 // kDiagonalCeilingBottomLeft = 76
218 // kDiagonalCeilingTopRight = 77
219 // kDiagonalCeilingBottomRight = 78
220 return routine_id >= 75 && routine_id <= 78;
221}
222
224 GeometryBounds render_bounds, int routine_id) {
225 if (!IsDiagonalCeilingRoutine(routine_id)) {
226 // Not a diagonal ceiling - return render bounds unchanged
227 return render_bounds;
228 }
229
230 // For diagonal ceilings, compute a tighter selection box.
231 // The visual triangle fills roughly 50% of the bounding box area.
232 // We use a selection rectangle that's 70% of the size, centered,
233 // to provide a reasonable hit target without excessive false positives.
234
235 int reduced_width = std::max(1, (render_bounds.width_tiles * 7) / 10);
236 int reduced_height = std::max(1, (render_bounds.height_tiles * 7) / 10);
237
238 // Center the reduced selection box within the render bounds
239 int offset_x = (render_bounds.width_tiles - reduced_width) / 2;
240 int offset_y = (render_bounds.height_tiles - reduced_height) / 2;
241
242 SelectionRect selection;
243 selection.x_tiles = render_bounds.min_x_tiles + offset_x;
244 selection.y_tiles = render_bounds.min_y_tiles + offset_y;
245 selection.width_tiles = reduced_width;
246 selection.height_tiles = reduced_height;
247
248 render_bounds.selection_bounds = selection;
249 return render_bounds;
250}
251
252} // namespace zelda3
253} // 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()
Side-car geometry engine that replays draw routines against an off-screen buffer to calculate real ex...
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 > 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.
const std::vector< gfx::TileInfo > & tiles() const
Definition room_object.h:91
int ChooseAnchorY(const DrawRoutineInfo &routine, const RoomObject &object)
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.