yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
door_position.cc
Go to the documentation of this file.
1#include "door_position.h"
2
3#include <algorithm>
4#include <cmath>
5#include <tuple>
6
7namespace yaze {
8namespace zelda3 {
9
10// ROM addresses for door position tables (PC addresses, bank $00)
11// Each direction has TWO consecutive tables for a total of 12 positions:
12// - Positions 0-5: Wall table (outer edges of room)
13// - Positions 6-11: Middle table (internal seams between quadrants)
14//
15// Table layout in ROM (consecutive in memory):
16// NorthWall ($997E): $021C,$023C,$025C,$039C,$03BC,$03DC (6 entries)
17// NorthMiddle ($998A): $121C,$123C,$125C,$139C,$13BC,$13DC (6 entries)
18// SouthMiddle ($9996): $0D1C,$0D3C,$0D5C,$0B9C,$0BBC,$0BDC,$1D1C,$1D3C,$1D5C (9 entries)
19// LowerLayerEntrance ($99A8): $1B9C,$1BBC,$1BDC (3 entries)
20// WestWall ($99AE): $0784,$0F84,$1784,$078A,$0F8A,$178A (6 entries)
21// WestMiddle ($99BA): $07C4,$0FC4,$17C4,$07CA,$0FCA,$17CA (6 entries)
22// EastMiddle ($99C6): $07B4,$0FB4,$17B4,$07AE,$0FAE,$17AE (6 entries)
23// EastWall ($99D2): $07F4,$0FF4,$17F4,$07EE,$0FEE,$17EE (6 entries)
24//
25// VRAM offset to room tile conversion:
26// room_x = (offset % 0x80) / 2 (NO margin subtraction for editor bitmap)
27// room_y = (offset / 0x80) - 4 (subtract 4-tile top margin for Y only)
28// Note: The 2-tile left margin should NOT be subtracted for the editor's
29// 64x64 tile bitmap, as the room content fills the entire bitmap.
30//
31// Room layout: 64x64 tiles divided into 4 quadrants (32x32 each)
32// X positions for N/S doors: 14, 30, 46 (left/center/right, +2 X offset from VRAM)
33// Y positions for E/W doors: 15, 31, 47 (distributed across 64 tiles, +4 Y offset from VRAM)
34[[maybe_unused]] constexpr int kDoorPosNorthAddr = 0x197E;
35[[maybe_unused]] constexpr int kDoorPosSouthAddr = 0x198A;
36[[maybe_unused]] constexpr int kDoorPosWestAddr = 0x1996;
37[[maybe_unused]] constexpr int kDoorPosEastAddr = 0x19A2;
38
40 DoorDirection direction) {
41 // Return valid snap positions based on the actual ROM lookup tables
42 // These are the coordinates where doors can be placed on each wall
43 switch (direction) {
45 // Valid X positions for north doors (left/center/right on wall)
46 return {14, 30, 46};
47
49 // Valid X positions for south doors (left/center/right on wall)
50 return {14, 30, 46};
51
53 // Valid Y positions for west doors (top/middle/bottom on wall)
54 return {15, 31, 47};
55
57 // Valid Y positions for east doors (top/middle/bottom on wall)
58 return {15, 31, 47};
59 }
60 return {};
61}
62
63uint8_t DoorPositionManager::SnapToNearestPosition(int canvas_x, int canvas_y,
64 DoorDirection direction) {
65 // First detect which section (outer wall vs inner seam) we're on
66 DoorDirection detected_dir;
67 bool is_inner = false;
68 if (!DetectWallSection(canvas_x, canvas_y, detected_dir, is_inner)) {
69 // Fallback: use outer wall positions
70 is_inner = false;
71 }
72
73 // Get the starting position index for this section
74 uint8_t start_pos = GetSectionStartPosition(direction, is_inner);
75
76 // Convert canvas pixels to tile coordinates
77 int tile_x = canvas_x / kTileSize;
78 int tile_y = canvas_y / kTileSize;
79
80 // Determine which coordinate to snap based on direction
81 // For North/South walls, we snap the X position (horizontal placement)
82 // For East/West walls, we snap the Y position (vertical placement)
83 int coord = (direction == DoorDirection::North ||
84 direction == DoorDirection::South)
85 ? tile_x
86 : tile_y;
87
88 // Get valid snap positions for this direction
89 auto valid_positions = GetSnapPositions(direction);
90 if (valid_positions.empty()) {
91 return start_pos; // Fallback
92 }
93
94 // Find the nearest valid X/Y position (index 0, 1, or 2)
95 int nearest_idx = 0;
96 int min_dist = std::abs(coord - valid_positions[0]);
97 for (size_t i = 1; i < valid_positions.size(); ++i) {
98 int dist = std::abs(coord - valid_positions[i]);
99 if (dist < min_dist) {
100 min_dist = dist;
101 nearest_idx = static_cast<int>(i);
102 }
103 }
104
105 // Return position index offset by section start
106 // Positions 0,1,2 and 3,4,5 have same X coords but different Y for layer variation
107 // For simplicity, we return the base position (0,1,2 offset by start_pos)
108 return static_cast<uint8_t>(start_pos + nearest_idx);
109}
110
112 uint8_t position, DoorDirection direction) {
113 // Door positions are indices (0-11+) into lookup tables from ROM.
114 // Tables are laid out consecutively so position 6+ reads into the next table.
115 //
116 // Room layout: 64x64 tiles (512x512 pixels), divided into 4 quadrants
117 // Each quadrant is 32x32 tiles. Doors can be on:
118 // - Outer walls (positions 0-5): edges of the room
119 // - Middle seams (positions 6-11): between quadrants
120 //
121 // Table layout in ROM (consecutive in memory):
122 // North: NorthWall (6) + NorthMiddle (6) = 12 positions
123 // South: SouthMiddle (9) + LowerLayerEntrance (3) = 12 positions
124 // West: WestWall (6) + WestMiddle (6) = 12 positions
125 // East: EastMiddle (6) + EastWall (6) = 12 positions
126 //
127 // VRAM offset to room tile conversion:
128 // room_x = (offset % 0x80) / 2 - 2 (subtract VRAM left margin)
129 // room_y = (offset / 0x80) - 4 (subtract VRAM top margin)
130
131 int pos_idx = position & 0x0F;
132
133 // Extended tables with 12 positions each (wall + middle seam)
134 // X positions: 12, 28, 44 divide the 64-tile room into thirds
135 // Y positions for N/S: edge vs middle seam
136 // X positions for E/W: edge vs middle seam
137
138 // North door positions (NorthWall + NorthMiddle):
139 // Positions 0-5: Outer north wall (Y=0 for upper layer, Y=3 for lower layer)
140 // Positions 6-11: Middle horizontal seam (Y=32 for upper, Y=35 for lower)
141 // X positions: 14, 30, 46 (left/center/right on wall)
142 // Note: +2 offset from raw VRAM calculation to correct for editor bitmap alignment
143 static constexpr int kNorthDoorX[] = {14, 30, 46, 14, 30, 46,
144 14, 30, 46, 14, 30, 46};
145 // Y positions: +4 offset from raw VRAM to account for top margin in editor bitmap
146 static constexpr int kNorthDoorY[] = {4, 4, 4, 7, 7, 7,
147 36, 36, 36, 39, 39, 39};
148
149 // South door positions (SouthMiddle + LowerLayerEntrance):
150 // In ALTTP, south doors use SouthMiddle table which gives positions
151 // relative to the current quadrant. For the 64-tile room:
152 // Positions 0-2: Upper quadrant south doors (Y=22 from quadrant top)
153 // Positions 3-5: Same, slightly higher (Y=19)
154 // Positions 6-8: Lower quadrant south doors (Y=54 in full room)
155 // Positions 9-11: Lower layer entrance (Y=51)
156 // For most single-screen rooms, positions 0-5 map to the actual south wall
157 // which should be at Y ≈ 60 in our 64-tile bitmap.
158 // X positions: 14, 30, 46 (+2 offset from raw VRAM for editor alignment)
159 static constexpr int kSouthDoorX[] = {14, 30, 46, 14, 30, 46,
160 14, 30, 46, 14, 30, 46};
161 // Y positions: +4 offset from raw VRAM, clamped to 61 max (room is 64 tiles, door is 3 tiles)
162 // Pos 0-2: Quadrant seam (Y=26), Pos 3-5: Slightly higher (Y=23)
163 // Pos 6-8: Outer south wall (Y=58), Pos 9-11: Slightly higher (Y=55)
164 static constexpr int kSouthDoorY[] = {26, 26, 26, 23, 23, 23,
165 58, 58, 58, 55, 55, 55};
166
167 // West door positions (WestWall + WestMiddle):
168 // Positions 0-5: Outer west wall (X=2 for upper, X=5 for lower layer)
169 // Positions 6-11: Middle vertical seam (X=34 for upper, X=37 for lower)
170 // Y positions: 15, 31, 47 (+4 offset from raw VRAM)
171 static constexpr int kWestDoorX[] = {2, 2, 2, 5, 5, 5,
172 34, 34, 34, 37, 37, 37};
173 static constexpr int kWestDoorY[] = {15, 31, 47, 15, 31, 47,
174 15, 31, 47, 15, 31, 47};
175
176 // East door positions (EastMiddle + EastWall):
177 // Note: East uses EastMiddle FIRST, then EastWall!
178 // Positions 0-5: Middle vertical seam (X=26 for upper, X=23 for lower)
179 // Positions 6-11: Outer east wall (X=58 for upper, X=55 for lower)
180 // Y positions: 15, 31, 47 (+4 offset from raw VRAM)
181 static constexpr int kEastDoorX[] = {26, 26, 26, 23, 23, 23,
182 58, 58, 58, 55, 55, 55};
183 static constexpr int kEastDoorY[] = {15, 31, 47, 15, 31, 47,
184 15, 31, 47, 15, 31, 47};
185
186 // Clamp to valid range (0-11)
187 if (pos_idx > 11) pos_idx = 11;
188
189 switch (direction) {
191 return {kNorthDoorX[pos_idx], kNorthDoorY[pos_idx]};
192
194 return {kSouthDoorX[pos_idx], kSouthDoorY[pos_idx]};
195
197 return {kWestDoorX[pos_idx], kWestDoorY[pos_idx]};
198
200 return {kEastDoorX[pos_idx], kEastDoorY[pos_idx]};
201 }
202
203 return {0, 0};
204}
205
207 uint8_t position, DoorDirection direction) {
208 auto [tile_x, tile_y] = PositionToTileCoords(position, direction);
209 return {tile_x * kTileSize, tile_y * kTileSize};
210}
211
213 switch (direction) {
215 return 0;
217 return kRoomHeightTiles - 3; // 3 tiles from bottom for door height
219 return 0;
221 return kRoomWidthTiles - 3; // 3 tiles from right for door width
222 }
223 return 0;
224}
225
227 DoorDirection direction) {
228 // Position must fit in 5 bits
229 if (position > 0x1F) {
230 return false;
231 }
232
233 // Check that resulting tile position is within room bounds
234 // and leaves room for door dimensions
235 int tile = (position & 0x1F) * 2;
236 auto dims = GetDoorDimensions(direction);
237
238 // For horizontal doors (N/S), check X doesn't overflow
239 // For vertical doors (E/W), check Y doesn't overflow
240 if (direction == DoorDirection::North ||
241 direction == DoorDirection::South) {
242 return tile + dims.width_tiles <= kRoomWidthTiles;
243 } else {
244 return tile + dims.height_tiles <= kRoomHeightTiles;
245 }
246}
247
248bool DoorPositionManager::DetectWallFromPosition(int canvas_x, int canvas_y,
249 DoorDirection& out_direction) {
250 // Convert to tile coordinates
251 int tile_x = canvas_x / kTileSize;
252 int tile_y = canvas_y / kTileSize;
253
254 // Check each wall edge with threshold
255 int threshold = kWallDetectionThreshold;
256
257 // North wall (top edge)
258 if (tile_y < threshold) {
259 out_direction = DoorDirection::North;
260 return true;
261 }
262
263 // South wall (bottom edge)
264 if (tile_y >= kRoomHeightTiles - threshold) {
265 out_direction = DoorDirection::South;
266 return true;
267 }
268
269 // West wall (left edge)
270 if (tile_x < threshold) {
271 out_direction = DoorDirection::West;
272 return true;
273 }
274
275 // East wall (right edge)
276 if (tile_x >= kRoomWidthTiles - threshold) {
277 out_direction = DoorDirection::East;
278 return true;
279 }
280
281 return false;
282}
283
284bool DoorPositionManager::DetectWallSection(int canvas_x, int canvas_y,
285 DoorDirection& out_direction,
286 bool& out_is_inner) {
287 // Convert to tile coordinates
288 int tile_x = canvas_x / kTileSize;
289 int tile_y = canvas_y / kTileSize;
290
291 // Room is 64x64 tiles, divided into 4 quadrants at tile 32
292 constexpr int kMiddleSeam = 32;
293 constexpr int kSeamThreshold = 6; // Detection range around seam
294 int threshold = kWallDetectionThreshold;
295
296 // Check outer walls first (edges of room)
297 // North wall (top edge)
298 if (tile_y < threshold) {
299 out_direction = DoorDirection::North;
300 out_is_inner = false;
301 return true;
302 }
303
304 // South wall (bottom edge)
305 if (tile_y >= kRoomHeightTiles - threshold) {
306 out_direction = DoorDirection::South;
307 out_is_inner = false; // South outer wall = positions 6-11
308 return true;
309 }
310
311 // West wall (left edge)
312 if (tile_x < threshold) {
313 out_direction = DoorDirection::West;
314 out_is_inner = false;
315 return true;
316 }
317
318 // East wall (right edge)
319 if (tile_x >= kRoomWidthTiles - threshold) {
320 out_direction = DoorDirection::East;
321 out_is_inner = false; // East outer wall = positions 6-11
322 return true;
323 }
324
325 // Check inner seams (middle of room between quadrants)
326 // Horizontal seam at Y=32 (between top and bottom quadrants)
327 if (std::abs(tile_y - kMiddleSeam) < kSeamThreshold) {
328 // Determine if North or South based on which side of seam
329 if (tile_y < kMiddleSeam) {
330 out_direction = DoorDirection::North;
331 } else {
332 out_direction = DoorDirection::South;
333 }
334 out_is_inner = true;
335 return true;
336 }
337
338 // Vertical seam at X=32 (between left and right quadrants)
339 if (std::abs(tile_x - kMiddleSeam) < kSeamThreshold) {
340 // Determine if West or East based on which side of seam
341 if (tile_x < kMiddleSeam) {
342 out_direction = DoorDirection::West;
343 } else {
344 out_direction = DoorDirection::East;
345 }
346 out_is_inner = true;
347 return true;
348 }
349
350 return false;
351}
352
354 bool is_inner) {
355 // Position ranges per direction:
356 // - North: Outer (0-5), Inner (6-11)
357 // - South: Inner (0-5), Outer (6-11) <- inverted!
358 // - West: Outer (0-5), Inner (6-11)
359 // - East: Inner (0-5), Outer (6-11) <- inverted!
360 switch (direction) {
363 return is_inner ? 6 : 0;
364
367 // South/East have inverted mapping
368 return is_inner ? 0 : 6;
369 }
370 return 0;
371}
372
373std::pair<uint8_t, uint8_t> DoorPositionManager::EncodeDoorBytes(
374 uint8_t position, DoorType type, DoorDirection direction) {
375 // Byte 1: position in bits 4-7, direction in bits 0-1
376 // This matches FromRomBytes decoding: position = (b1 >> 4) & 0x0F,
377 // direction = b1 & 0x03
378 uint8_t byte1 = ((position & 0x0F) << 4) |
379 (static_cast<uint8_t>(direction) & 0x03);
380
381 // Byte 2: door type (full byte, values 0x00, 0x02, 0x04, etc.)
382 uint8_t byte2 = static_cast<uint8_t>(type);
383
384 return {byte1, byte2};
385}
386
387std::tuple<int, int, int, int> DoorPositionManager::GetDoorBounds(
388 uint8_t position, DoorDirection direction) {
389 auto [pixel_x, pixel_y] = PositionToPixelCoords(position, direction);
390 auto dims = GetDoorDimensions(direction);
391
392 return {pixel_x, pixel_y, dims.width_pixels(), dims.height_pixels()};
393}
394
395} // namespace zelda3
396} // namespace yaze
static uint8_t GetSectionStartPosition(DoorDirection direction, bool is_inner)
Get the starting position index for outer/inner section.
static constexpr int kRoomHeightTiles
static std::pair< uint8_t, uint8_t > EncodeDoorBytes(uint8_t position, DoorType type, DoorDirection direction)
Encode door data for ROM storage.
static std::pair< int, int > PositionToPixelCoords(uint8_t position, DoorDirection direction)
Convert encoded position to pixel coordinates.
static std::tuple< int, int, int, int > GetDoorBounds(uint8_t position, DoorDirection direction)
Get the bounding rectangle for a door.
static bool DetectWallFromPosition(int canvas_x, int canvas_y, DoorDirection &out_direction)
Detect which wall the cursor is near.
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
static constexpr int kWallDetectionThreshold
static std::pair< int, int > PositionToTileCoords(uint8_t position, DoorDirection direction)
Convert encoded position to tile coordinates.
static bool DetectWallSection(int canvas_x, int canvas_y, DoorDirection &out_direction, bool &out_is_inner)
Detect wall with inner/outer section information.
static uint8_t SnapToNearestPosition(int canvas_x, int canvas_y, DoorDirection direction)
Convert canvas coordinates to nearest valid door position.
static int GetWallEdge(DoorDirection direction)
Get the wall edge coordinate for a direction.
static constexpr int kRoomWidthTiles
static std::vector< int > GetSnapPositions(DoorDirection direction)
Get all valid snap positions for a given direction.
constexpr DoorDimensions GetDoorDimensions(DoorDirection dir)
Get door dimensions based on direction.
Definition door_types.h:192
DoorType
Door types from ALTTP.
Definition door_types.h:33
constexpr int kDoorPosSouthAddr
constexpr int kDoorPosEastAddr
constexpr int kDoorPosNorthAddr
constexpr int kDoorPosWestAddr
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)