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::MeasureForLayerCompositing(
156 int routine_id, const RoomObject& object) const {
157 auto result = MeasureByRoutineId(routine_id, object);
158 if (!result.ok()) {
159 return result;
160 }
161
162 GeometryBounds bounds = *result;
163
164 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
165 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
166 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
167
168 return bounds;
169}
170
172 // Layer 1 routines are those that explicitly draw to BG2 only.
173 // Most objects draw to the current layer pointer; this list is for
174 // routines that have special BG2-only behavior.
175 //
176 // From ASM analysis:
177 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
178 // - The routine itself doesn't determine the layer; the object's position
179 // in the room data determines which layer pointer it uses
180 //
181 // This method is primarily for documentation; actual layer determination
182 // comes from the object's layer_ field set during room loading.
183 (void)
184 routine_id; // Currently unused - layer determined by object, not routine
185 return false;
186}
187
189 // Diagonal ceiling routines from draw_routine_registry.h
190 // kDiagonalCeilingTopLeft = 75
191 // kDiagonalCeilingBottomLeft = 76
192 // kDiagonalCeilingTopRight = 77
193 // kDiagonalCeilingBottomRight = 78
194 return routine_id >= 75 && routine_id <= 78;
195}
196
198 GeometryBounds render_bounds, int routine_id) {
199 if (!IsDiagonalCeilingRoutine(routine_id)) {
200 // Not a diagonal ceiling - return render bounds unchanged
201 return render_bounds;
202 }
203
204 // For diagonal ceilings, compute a tighter selection box.
205 // The visual triangle fills roughly 50% of the bounding box area.
206 // We use a selection rectangle that's 70% of the size, centered,
207 // to provide a reasonable hit target without excessive false positives.
208
209 int reduced_width = std::max(1, (render_bounds.width_tiles * 7) / 10);
210 int reduced_height = std::max(1, (render_bounds.height_tiles * 7) / 10);
211
212 // Center the reduced selection box within the render bounds
213 int offset_x = (render_bounds.width_tiles - reduced_width) / 2;
214 int offset_y = (render_bounds.height_tiles - reduced_height) / 2;
215
216 SelectionRect selection;
217 selection.x_tiles = render_bounds.min_x_tiles + offset_x;
218 selection.y_tiles = render_bounds.min_y_tiles + offset_y;
219 selection.width_tiles = reduced_width;
220 selection.height_tiles = reduced_height;
221
222 render_bounds.selection_bounds = selection;
223 return render_bounds;
224}
225
226} // namespace zelda3
227} // namespace yaze
uint16_t GetTileAt(int x, int y) const
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
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.