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) min_anchor = max_anchor;
42 return std::clamp(min_anchor, 0, max_anchor);
43 }
44
45 // Default: start at top of canvas.
46 return 0;
47}
48
49} // namespace
50
52 static ObjectGeometry instance;
53 return instance;
54}
55
57
59 routines_.clear();
60 routine_map_.clear();
61
62 // Use the unified DrawRoutineRegistry to ensure consistent routine IDs
63 // between ObjectGeometry and ObjectDrawer
64 const auto& registry = DrawRoutineRegistry::Get();
65 routines_ = registry.GetAllRoutines();
66
67 for (const auto& info : routines_) {
68 routine_map_[info.id] = info;
69 }
70}
71
72const DrawRoutineInfo* ObjectGeometry::LookupRoutine(int routine_id) const {
73 auto it = routine_map_.find(routine_id);
74 if (it == routine_map_.end()) {
75 return nullptr;
76 }
77 return &it->second;
78}
79
80absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByRoutineId(
81 int routine_id, const RoomObject& object) const {
82 const DrawRoutineInfo* info = LookupRoutine(routine_id);
83 if (info == nullptr) {
84 return absl::InvalidArgumentError(
85 absl::StrFormat("Unknown routine id %d", routine_id));
86 }
87 return MeasureRoutine(*info, object);
88}
89
90absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutine(
91 const DrawRoutineInfo& routine, const RoomObject& object) const {
92 // Anchor object so routines that move upward stay within bounds.
93 RoomObject adjusted = object;
94 adjusted.x_ = kAnchorX;
95 const int anchor_y = ChooseAnchorY(routine, object);
96 adjusted.y_ = anchor_y;
97
98 // Allocate a dummy tile list large enough for every routine.
99 static const std::vector<gfx::TileInfo> kTiles = MakeDummyTiles();
100
103
104 DrawContext ctx{
105 .target_bg = bg,
106 .object = adjusted,
107 .tiles = std::span<const gfx::TileInfo>(kTiles.data(), kTiles.size()),
108 .state = nullptr,
109 .rom = nullptr,
110 .room_id = 0,
111 .room_gfx_buffer = nullptr,
112 .secondary_bg = nullptr,
113 };
114
115 // Execute the routine to mark tiles in the buffer.
116 routine.function(ctx);
117
118 // Scan buffer for written tiles.
119 const int tiles_w = DrawContext::kMaxTilesX;
120 const int tiles_h = DrawContext::kMaxTilesY;
121
122 int min_x = std::numeric_limits<int>::max();
123 int min_y = std::numeric_limits<int>::max();
124 int max_x = std::numeric_limits<int>::min();
125 int max_y = std::numeric_limits<int>::min();
126
127 for (int y = 0; y < tiles_h; ++y) {
128 for (int x = 0; x < tiles_w; ++x) {
129 if (bg.GetTileAt(x, y) == 0) continue;
130 min_x = std::min(min_x, x);
131 min_y = std::min(min_y, y);
132 max_x = std::max(max_x, x);
133 max_y = std::max(max_y, y);
134 }
135 }
136
137 // Handle routines that intentionally draw nothing.
138 if (max_x == std::numeric_limits<int>::min()) {
139 return GeometryBounds{};
140 }
141
142 GeometryBounds bounds;
143 bounds.min_x_tiles = min_x - kAnchorX;
144 bounds.min_y_tiles = min_y - anchor_y;
145 bounds.width_tiles = (max_x - min_x) + 1;
146 bounds.height_tiles = (max_y - min_y) + 1;
147 bounds.is_bg2_overlay = false; // Default, set by MeasureForLayerCompositing
148 return bounds;
149}
150
151absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureForLayerCompositing(
152 int routine_id, const RoomObject& object) const {
153 auto result = MeasureByRoutineId(routine_id, object);
154 if (!result.ok()) {
155 return result;
156 }
157
158 GeometryBounds bounds = *result;
159
160 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
161 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
162 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
163
164 return bounds;
165}
166
168 // Layer 1 routines are those that explicitly draw to BG2 only.
169 // Most objects draw to the current layer pointer; this list is for
170 // routines that have special BG2-only behavior.
171 //
172 // From ASM analysis:
173 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
174 // - The routine itself doesn't determine the layer; the object's position
175 // in the room data determines which layer pointer it uses
176 //
177 // This method is primarily for documentation; actual layer determination
178 // comes from the object's layer_ field set during room loading.
179 (void)routine_id; // Currently unused - layer determined by object, not routine
180 return false;
181}
182
184 // Diagonal ceiling routines from draw_routine_registry.h
185 // kDiagonalCeilingTopLeft = 75
186 // kDiagonalCeilingBottomLeft = 76
187 // kDiagonalCeilingTopRight = 77
188 // kDiagonalCeilingBottomRight = 78
189 return routine_id >= 75 && routine_id <= 78;
190}
191
193 int routine_id) {
194 if (!IsDiagonalCeilingRoutine(routine_id)) {
195 // Not a diagonal ceiling - return render bounds unchanged
196 return render_bounds;
197 }
198
199 // For diagonal ceilings, compute a tighter selection box.
200 // The visual triangle fills roughly 50% of the bounding box area.
201 // We use a selection rectangle that's 70% of the size, centered,
202 // to provide a reasonable hit target without excessive false positives.
203
204 int reduced_width = std::max(1, (render_bounds.width_tiles * 7) / 10);
205 int reduced_height = std::max(1, (render_bounds.height_tiles * 7) / 10);
206
207 // Center the reduced selection box within the render bounds
208 int offset_x = (render_bounds.width_tiles - reduced_width) / 2;
209 int offset_y = (render_bounds.height_tiles - reduced_height) / 2;
210
211 SelectionRect selection;
212 selection.x_tiles = render_bounds.min_x_tiles + offset_x;
213 selection.y_tiles = render_bounds.min_y_tiles + offset_y;
214 selection.width_tiles = reduced_width;
215 selection.height_tiles = reduced_height;
216
217 render_bounds.selection_bounds = selection;
218 return render_bounds;
219}
220
221} // namespace zelda3
222} // namespace yaze
uint16_t GetTileAt(int x, int y) const
SNES 16-bit tile metadata container.
Definition snes_tile.h:50
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_
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()
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:88
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.