yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room_collision.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <vector>
7
8#include "rom/rom.h"
10#include "zelda3/dungeon/room.h"
12
13namespace yaze::zelda3 {
14namespace {
15
16// ROM tables (PC offsets, vanilla addresses from usdasm).
17constexpr int kUnderworldTileTypes = 0x071659; // $0E:9659, 0x180 bytes
18constexpr int kCustomTileTypesOffset = 0x071000; // $0E:9000, 21 words
19constexpr int kCustomUnderworldTileTypes = 0x07102A; // $0E:902A
20constexpr int kDungeonMask = 0x0018C0; // $00:98C0, 16 words
21constexpr int kRoomFlagMask = 0x001900; // $00:9900
22constexpr int kDoorPositionsNorthWall = 0x00197E; // $00:997E
23constexpr int kDoorPositionsNorthMiddle = 0x00198A; // $00:998A
24constexpr int kDoorPositionsSouthMiddle = 0x001996; // $00:9996
25constexpr int kDoorPositionsWestWall = 0x0019AE; // $00:99AE
26constexpr int kDoorPositionsWestMiddle = 0x0019BA; // $00:99BA
27constexpr int kDoorPositionsEastMiddle = 0x0019C6; // $00:99C6
28constexpr int kExplodingWallPositions = 0x0019DE; // $00:99DE
29constexpr int kDoorwayReplacementDoorGfx = 0x001A02; // $00:9A02
30constexpr int kDoorwayTileProperties = 0x001A52; // $00:9A52
31
32uint8_t RomByte(const Rom& rom, int pc) {
33 if (pc < 0 || static_cast<size_t>(pc) >= rom.size()) {
34 return 0;
35 }
36 return rom.data()[pc];
37}
38
39uint16_t RomWord(const Rom& rom, int pc) {
40 return static_cast<uint16_t>(RomByte(rom, pc) | (RomByte(rom, pc + 1) << 8));
41}
42
43// The low WRAM ($0000-$1FFF) the draw routines and attribute passes share.
44// Lists overlap in the real game ($06B0 stairs run into $06B8, chests at
45// $06E0 into $06EC), so they live in one byte array like the game's.
46class LowWram {
47 public:
48 uint16_t Word(int address) const {
49 return static_cast<uint16_t>(bytes_[address & 0x1FFF] |
50 (bytes_[(address + 1) & 0x1FFF] << 8));
51 }
52 void SetWord(int address, uint16_t value) {
53 bytes_[address & 0x1FFF] = static_cast<uint8_t>(value & 0xFF);
54 bytes_[(address + 1) & 0x1FFF] = static_cast<uint8_t>(value >> 8);
55 }
56
57 private:
58 std::array<uint8_t, 0x2000> bytes_{};
59};
60
61// Low WRAM addresses (usdasm wram.asm / bank_01 comments).
62constexpr int kDoorListIndex = 0x0460;
63constexpr int kDoorTypes = 0x1980; // (slot << 8) | type
64constexpr int kDoorPositions = 0x19A0; // tilemap offset (+$2000 = BG2)
65constexpr int kDoorDirections = 0x19C0;
66constexpr int kExitDoorCount = 0x19E0;
67constexpr int kExitDoorPositions = 0x19E2; // 4 words
68constexpr int kDoorOpenMask = 0x068C;
69constexpr int kRoomFlags = 0x0402;
70constexpr int kShutterController = 0x0468;
71constexpr int kLayerToggleCount = 0x044E; // list at $06C0
72constexpr int kDungeonToggleCount = 0x0450; // list at $06D0
73constexpr int kDoorSwitch = 0x04B0;
74constexpr int kStarCount = 0x0432; // list at $06A0
75constexpr int kManipulableCount = 0x042C; // $0500 type, $0540 position
76constexpr int kTorchEnd = 0x042E;
77constexpr int kChestCount = 0x0496; // list at $06E0
78constexpr int kBigKeyLockEnd = 0x0498;
79
80struct RomTables {
81 explicit RomTables(const Rom& rom) : rom(rom) {}
82 uint16_t DungeonMask(int x) const {
83 return RomWord(rom, kDungeonMask + (x & 0xFFFF));
84 }
85 const Rom& rom;
86};
87
88// ---------------------------------------------------------------------------
89// Replays the list bookkeeping of the object and door draw routines.
90
92 public:
93 RoomLoadReplay(const Rom& rom, const RoomCollisionInput& input,
94 const UnderworldRoomLoadState& state)
95 : rom_(rom), tables_(rom), input_(input), state_(state) {
96 tag1_ = input.tag1;
97 tag2_ = input.tag2;
98 // Underworld_LoadHeader (#_01B6D1).
99 const uint16_t save = state.room_save_flags;
100 w_.SetWord(kDoorOpenMask,
101 state.door_open_mask.value_or(
102 static_cast<uint16_t>((save & 0xF000) | 0x0F00)));
103 w_.SetWord(kRoomFlags, static_cast<uint16_t>((save & 0x0FF0) << 4));
104 w_.SetWord(kShutterController, state.shutter_controller);
105 }
106
107 LowWram& wram() { return w_; }
108 uint8_t tag1() const { return tag1_; }
109 uint8_t tag2() const { return tag2_; }
110
111 // Underworld_LoadRoom (#_01873A): room object lists 0, 1, 2 (doors follow
112 // the list that holds the $FFF0 marker, the last one in vanilla), then
113 // pushable blocks, then torches.
114 void Run() {
115 for (int list = 0; list < 3; ++list) {
116 for (const auto& object : input_.objects) {
117 if (!UsesRoomObjectStream(object) || object.GetLayerValue() != list) {
118 continue;
119 }
120 RecordObject(object, /*lower_layer=*/list == 1);
121 }
122 }
123 for (const auto& door : input_.doors) {
124 RecordDoor(door);
125 }
126 for (const auto& object : input_.objects) {
127 if ((object.options() & ObjectOption::Block) != ObjectOption::Nothing) {
128 RecordPushableBlock(object);
129 }
130 }
131 // $042E = $042C before the torches draw (#_01889C).
132 w_.SetWord(kTorchEnd, w_.Word(kManipulableCount));
133 for (const auto& object : input_.objects) {
134 if ((object.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
135 RecordTorch(object);
136 }
137 }
138 }
139
140 private:
141 // ---- objects -----------------------------------------------------------
142
143 static uint16_t TilemapOffset(const RoomObject& object) {
144 return static_cast<uint16_t>((object.y() & 0x3F) * 0x80 +
145 (object.x() & 0x3F) * 2);
146 }
147
148 // Sets each counter in `counters` to `value` (the stairs routines keep
149 // every later stairs list's end index in step).
150 void SetCounters(std::initializer_list<int> counters, uint16_t value) {
151 for (int counter : counters) {
152 w_.SetWord(counter, value);
153 }
154 }
155
156 // Appends `value` to a list at `list` whose end index lives at `counter`.
157 uint16_t Append(int counter, int list, uint16_t value) {
158 const uint16_t x = w_.Word(counter);
159 w_.SetWord(list + x, value);
160 return static_cast<uint16_t>(x + 2);
161 }
162
163 // DrawBigGraySegment / RoomDraw_SinglePot / RoomDraw_HammerPegSingle
164 // (#_01B33A, #_01B395, #_01B493): $0500 = type, $0540 = position.
165 void AddManipulable(uint16_t type, uint16_t y, bool lower_layer) {
166 const uint16_t x = w_.Word(kManipulableCount);
167 w_.SetWord(0x0500 + x, type);
168 w_.SetWord(kManipulableCount, static_cast<uint16_t>(x + 2));
169 w_.SetWord(0x0520 + x, 0); // $BA, the object stream offset (unused)
170 w_.SetWord(0x0540 + x, lower_layer ? (y | 0x2000) : y);
171 }
172
173 bool WaterFlagSet() const {
174 // LDA $0403 : AND DoorFlagMasks+1 (8-bit) = $0402 & 0x0800.
175 return (w_.Word(kRoomFlags) & 0x0800) != 0;
176 }
177 bool DamGateUnopened() const {
178 // $AF == 0x1B and the room's save word bit 0x0100 clear.
179 return tag2_ == 0x1B && (state_.room_save_flags & 0x0100) == 0;
180 }
181 // RoomDraw_WaterOverlayA/B: turns the "merged" water stairs into the
182 // regular ones.
184 w_.SetWord(0x0440, w_.Word(0x0442));
185 w_.SetWord(0x0448, w_.Word(0x0444));
186 w_.SetWord(0x0444, 0);
187 w_.SetWord(0x0442, 0);
188 w_.SetWord(0x049E, w_.Word(0x04AE));
189 w_.SetWord(0x04AE, 0);
190 }
191
192 void RecordObject(const RoomObject& object, bool lower_layer) {
193 const uint16_t y = TilemapOffset(object);
194 const uint16_t layer_y = lower_layer ? (y | 0x2000) : y;
195 const int count = (object.size() & 0x0F) + 1; // RoomDraw_GetSize_1to16
196 switch (object.id_) {
197 case 0x035: { // RoomDraw_DoorSwitcherer (#_01913F)
198 uint16_t value = layer_y;
199 if ((w_.Word(kRoomFlags) & 0x1000) == 0) {
200 value |= 0x8000; // not drawn: 5 columns instead of 6
201 }
202 w_.SetWord(kDoorSwitch, value);
203 break;
204 }
205 case 0x095: // RoomDraw_DownwardsPots2x2_1to16
206 for (int i = 0; i < count; ++i) {
207 AddManipulable(0x1111, static_cast<uint16_t>(y + i * 0x100),
208 lower_layer);
209 }
210 break;
211 case 0x0BC: // RoomDraw_RightwardsPots2x2_1to16
212 for (int i = 0; i < count; ++i) {
213 AddManipulable(0x1111, static_cast<uint16_t>(y + i * 4), lower_layer);
214 }
215 break;
216 case 0x096: // RoomDraw_DownwardsHammerPegs2x2_1to16
217 for (int i = 0; i < count; ++i) {
218 AddManipulable(0x4040, static_cast<uint16_t>(y + i * 0x100),
219 lower_layer);
220 }
221 break;
222 case 0x0BD: // RoomDraw_RightwardsHammerPegs2x2_1to16
223 for (int i = 0; i < count; ++i) {
224 AddManipulable(0x4040, static_cast<uint16_t>(y + i * 4), lower_layer);
225 }
226 break;
227 case 0xF96: // RoomDraw_HammerPegSingle (0x216)
228 AddManipulable(0x4040, y, lower_layer);
229 break;
230 case 0xFAF: // RoomDraw_SinglePot (0x22F)
231 AddManipulable(0x1111, y, lower_layer);
232 break;
233 case 0xFAB: // RoomDraw_WeirdGloveRequiredPot (0x22B)
234 AddManipulable(0x1010, y, lower_layer);
235 break;
236 case 0xFB0: // RoomDraw_WeirdUglyPot (0x230)
237 AddManipulable(0x1212, y, lower_layer);
238 break;
239 case 0xFAC: // RoomDraw_BigGrayRock (0x22C): four 2x2 segments
240 AddManipulable(0x2020, y, lower_layer);
241 AddManipulable(0x2121, static_cast<uint16_t>(y + 4), lower_layer);
242 AddManipulable(0x2222, static_cast<uint16_t>(y + 0x100), lower_layer);
243 AddManipulable(0x2323, static_cast<uint16_t>(y + 0x104), lower_layer);
244 break;
245 case 0xFC7: // RoomDraw_BombableFloor (0x247)
246 if (input_.room_id == 0x65 && (w_.Word(kRoomFlags) & 0x1000) != 0) {
247 break; // open Thieves' Town attic floor: a plain hole
248 }
249 AddManipulable(0x3030, y, lower_layer);
250 AddManipulable(0x3131, static_cast<uint16_t>(y + 4), lower_layer);
251 AddManipulable(0x3232, static_cast<uint16_t>(y + 0x100), lower_layer);
252 AddManipulable(0x3333, static_cast<uint16_t>(y + 0x104), lower_layer);
253 break;
254 case 0x0D8: // RoomDraw_WaterOverlayA8x8_1to16 (#_019501)
255 if (WaterFlagSet()) {
256 tag2_ = 0;
257 MoveWaterStairLists();
258 }
259 break;
260 case 0x0DA: // RoomDraw_WaterOverlayB8x8_1to16 (#_0195EF)
261 if (WaterFlagSet()) {
262 tag2_ = 0;
263 } else {
264 MoveWaterStairLists();
265 }
266 break;
267 case 0x11F: // RoomDraw_EnabledStarSwitch (#_019A6F)
268 w_.SetWord(kStarCount, Append(kStarCount, 0x06A0, layer_y >> 1));
269 break;
270 case 0x12D: // RoomDraw_InterRoomFatStairsUp (#_01A41B)
271 SetCounters({0x0438, 0x047E, 0x0482, 0x04A2, 0x04A4, 0x043A, 0x0480,
272 0x0484, 0x04A6, 0x04A8},
273 Append(0x0438, 0x06B0, layer_y >> 1));
274 break;
275 case 0x12E: // RoomDraw_InterRoomFatStairsDown_A
276 case 0x12F: // RoomDraw_InterRoomFatStairsDown_B
277 SetCounters({0x043A, 0x0480, 0x0484, 0x04A6, 0x04A8},
278 Append(0x043A, 0x06B0, layer_y >> 1));
279 break;
280 case 0x138: { // RoomDraw_SpiralStairsGoingUpUpper (#_01A4B4)
281 const uint16_t v = static_cast<uint16_t>(
282 (static_cast<uint16_t>(y - 0x80) | (lower_layer ? 0x2000 : 0)) >>
283 1);
284 SetCounters({0x047E, 0x0482, 0x04A2, 0x04A4, 0x043A, 0x0480, 0x0484,
285 0x04A6, 0x04A8},
286 Append(0x047E, 0x06B0, v));
287 break;
288 }
289 case 0x13A: { // RoomDraw_SpiralStairsGoingUpLower
290 const uint16_t v = static_cast<uint16_t>(
291 (static_cast<uint16_t>(y - 0x80) | (lower_layer ? 0x2000 : 0)) >>
292 1);
293 SetCounters(
294 {0x0482, 0x04A2, 0x04A4, 0x043A, 0x0480, 0x0484, 0x04A6, 0x04A8},
295 Append(0x0482, 0x06B0, v));
296 break;
297 }
298 case 0x139: { // RoomDraw_SpiralStairsGoingDownUpper
299 const uint16_t v = static_cast<uint16_t>(
300 (static_cast<uint16_t>(y - 0x80) | (lower_layer ? 0x2000 : 0)) >>
301 1);
302 SetCounters({0x0480, 0x0484, 0x04A6, 0x04A8},
303 Append(0x0480, 0x06B0, v));
304 break;
305 }
306 case 0x13B: { // RoomDraw_SpiralStairsGoingDownLower
307 const uint16_t v = static_cast<uint16_t>(
308 (static_cast<uint16_t>(y - 0x80) | (lower_layer ? 0x2000 : 0)) >>
309 1);
310 SetCounters({0x0484, 0x04A6, 0x04A8}, Append(0x0484, 0x06B0, v));
311 break;
312 }
313 // Straight inter-room stairs. The "Upper" variants ignore the layer.
314 case 0xF9E: // RoomDraw_StraightInterroomStairsGoingUpNorthUpper
315 SetCounters({0x04A2, 0x04A4, 0x043A, 0x0480, 0x0484, 0x04A6, 0x04A8},
316 Append(0x04A2, 0x06B0, y >> 1));
317 break;
318 case 0xFA6: // RoomDraw_StraightInterroomStairsGoingUpNorthLower
319 SetCounters({0x04A2, 0x04A4, 0x043A, 0x0480, 0x0484, 0x04A6, 0x04A8},
320 Append(0x04A2, 0x06B0, layer_y >> 1));
321 break;
322 case 0xF9F: // RoomDraw_StraightInterroomStairsGoingDownNorthUpper
323 SetCounters({0x04A6, 0x04A8}, Append(0x04A6, 0x06B0, y >> 1));
324 break;
325 case 0xFA7: // RoomDraw_StraightInterroomStairsGoingDownNorthLower
326 SetCounters({0x04A6, 0x04A8}, Append(0x04A6, 0x06B0, layer_y >> 1));
327 break;
328 case 0xFA0: // RoomDraw_StraightInterroomStairsGoingUpSouthUpper
329 SetCounters({0x04A4, 0x043A, 0x0480, 0x0484, 0x04A6, 0x04A8},
330 Append(0x04A4, 0x06B0, y >> 1));
331 break;
332 case 0xFA8: // RoomDraw_StraightInterroomStairsGoingUpSouthLower
333 SetCounters({0x04A4, 0x043A, 0x0480, 0x0484, 0x04A6, 0x04A8},
334 Append(0x04A4, 0x06B0, layer_y >> 1));
335 break;
336 case 0xFA1: // RoomDraw_StraightInterroomStairsGoingDownSouthUpper
337 SetCounters({0x04A8}, Append(0x04A8, 0x06B0, y >> 1));
338 break;
339 case 0xFA9: // RoomDraw_StraightInterroomStairsGoingDownSouthLower
340 SetCounters({0x04A8}, Append(0x04A8, 0x06B0, layer_y >> 1));
341 break;
342 // Auto stairs record Y >> 1 without the layer bit.
343 case 0x130: { // RoomDraw_AutoStairs_North_MultiLayer_A (#_01A25D)
344 const uint16_t end = Append(0x043C, 0x06B8, y >> 1);
345 SetCounters({0x043C, 0x0446, 0x0448}, end);
346 break;
347 }
348 case 0x131: { // RoomDraw_AutoStairs_North_MultiLayer_B
349 const uint16_t end = Append(0x043E, 0x06B8, y >> 1);
350 SetCounters({0x043E, 0x0446, 0x0448}, end);
351 break;
352 }
353 case 0x133: // RoomDraw_AutoStairs_North_MergedLayer_B (#_01A2DF)
354 if (!DamGateUnopened()) {
355 SetCounters({0x0442, 0x0444}, Append(0x0442, 0x06B8, y >> 1));
356 break;
357 }
358 [[fallthrough]]; // AutoStairsNorthMergedStart
359 case 0x132: // RoomDraw_AutoStairs_North_MergedLayer_A (#_01A2C7)
360 SetCounters({0x0440, 0x0446, 0x0448}, Append(0x0440, 0x06B8, y >> 1));
361 break;
362 case 0x135: // RoomDraw_WaterHopStairs_A (#_019B1E)
363 if (!DamGateUnopened()) {
364 w_.SetWord(0x0444, Append(0x0444, 0x06B8, y >> 1));
365 break;
366 }
367 [[fallthrough]];
368 case 0x136: // RoomDraw_WaterHopStairs_B (#_01A3AE)
369 SetCounters({0x0446, 0x0448}, Append(0x0446, 0x06B8, y >> 1));
370 break;
371 case 0xF9B: // RoomDraw_AutoStairs_South_MultiLayer_A (#_01A30C)
372 // Vanilla stores this one in the north list ($06B8) but counts it in
373 // $049A, which the attribute pass reads against $06EC.
374 w_.SetWord(0x049A, Append(0x049A, 0x06B8, y >> 1));
375 break;
376 case 0xF9C: // RoomDraw_AutoStairs_South_MultiLayer_B
377 w_.SetWord(0x049C, Append(0x049C, 0x06EC, y >> 1));
378 break;
379 case 0xFB3: // RoomDraw_AutoStairs_South_MergedLayer (#_01A380)
380 if (!DamGateUnopened()) {
381 w_.SetWord(0x04AE, Append(0x04AE, 0x06EC, y >> 1));
382 break;
383 }
384 [[fallthrough]]; // South_MergedStairs_BecomeMultiC
385 case 0xF9D: // RoomDraw_AutoStairs_South_MultiLayer_C
386 w_.SetWord(0x049E, Append(0x049E, 0x06EC, y >> 1));
387 break;
388 case 0xF98: { // RoomDraw_BigKeyLock (#_0198AE)
389 const uint16_t x = w_.Word(kBigKeyLockEnd);
390 const bool opened =
391 (w_.Word(kRoomFlags) & RomWord(rom_, kRoomFlagMask + x)) != 0;
392 w_.SetWord(0x06E0 + x, opened ? 0 : y);
393 w_.SetWord(kBigKeyLockEnd, static_cast<uint16_t>(x + 2));
394 break;
395 }
396 case 0xF99: // RoomDraw_Chest (#_0198D0)
397 case 0xFB1: { // RoomDraw_BigChest (#_0199BB)
398 const uint16_t x = w_.Word(kChestCount);
399 uint16_t value = layer_y;
400 if (object.id_ == 0xFB1) {
401 value |= 0x8000;
402 }
403 const bool opened =
404 (w_.Word(kRoomFlags) & RomWord(rom_, kRoomFlagMask + x)) != 0;
405 w_.SetWord(0x06E0 + x, opened ? 0 : value);
406 SetCounters({kChestCount, kBigKeyLockEnd},
407 static_cast<uint16_t>(x + 2));
408 break;
409 }
410 default:
411 break;
412 }
413 }
414
415 // RoomDraw_PushableBlock (#_01B4D6): $0500 = 0, $0540 = table word.
416 void RecordPushableBlock(const RoomObject& block) {
417 const uint16_t x = w_.Word(kManipulableCount);
418 w_.SetWord(kManipulableCount, static_cast<uint16_t>(x + 2));
419 w_.SetWord(0x0500 + x, 0);
420 w_.SetWord(0x0520 + x, 0);
421 uint16_t word = TilemapOffset(block);
422 if (block.GetLayerValue() & 1) {
423 word |= 0x2000;
424 }
425 word |= static_cast<uint16_t>((block.block_behavior_layer() & 1) << 14);
426 w_.SetWord(0x0540 + x, word);
427 }
428
429 // RoomDraw_LightableTorch (#_01B509): $0540,$042E = table word.
430 void RecordTorch(const RoomObject& torch) {
431 const uint16_t x = w_.Word(kTorchEnd);
432 uint16_t word = TilemapOffset(torch);
433 if (torch.GetLayerValue() & 1) {
434 word |= 0x2000;
435 }
436 if (torch.lit_) {
437 word |= 0x8000;
438 }
439 w_.SetWord(0x0540 + x, word);
440 w_.SetWord(kTorchEnd, static_cast<uint16_t>(x + 2));
441 }
442
443 // ---- doors -------------------------------------------------------------
444
445 // Door routine registers: $00 (door word), $02 (position * 2), $04 (type),
446 // $0A (graphics type), $08 (tilemap position).
447 struct DoorRegs {
448 uint16_t word = 0;
449 uint16_t position = 0;
450 uint16_t type = 0;
451 uint16_t gfx = 0;
452 };
453
454 struct FlagResult {
455 bool carry = false;
456 uint16_t gfx = 0; // returned in Y
457 uint16_t x = 0; // X on return
458 };
459
460 // RoomDraw_FlagDoorsAndGetFinalType (#_01B0DA).
461 FlagResult FlagDoors(uint16_t x, uint16_t direction, uint16_t y) {
462 w_.SetWord(kDoorDirections + x, direction);
463 w_.SetWord(kDoorPositions + x, y);
464 w_.SetWord(kDoorTypes + x,
465 static_cast<uint16_t>(((x >> 1) << 8) | (regs_.type & 0xFF)));
466 const uint16_t slot = x & 0x0F;
467 if (slot < 8 && (w_.Word(kDoorOpenMask) & tables_.DungeonMask(slot))) {
468 const uint16_t type = w_.Word(kDoorTypes + x) & 0xFF;
469 const bool shutter = type == 0x18 || type == 0x44;
470 if (!(shutter && w_.Word(kShutterController) != 0)) {
471 regs_.gfx = RomWord(rom_, kDoorwayReplacementDoorGfx + regs_.type);
472 }
473 }
474 FlagResult result;
475 result.gfx = regs_.gfx;
476 x = static_cast<uint16_t>(x + 2);
477 w_.SetWord(kDoorListIndex, x);
478 result.x = x;
479 if (result.gfx == 0x32 || result.gfx == 0x08) {
480 return result; // CLC_and_EXIT
481 }
482 if (regs_.type == 0x1A) {
483 // Both compares against $2F fall through to .flag_shutter_door.
484 x = static_cast<uint16_t>(x - 2);
485 w_.SetWord(0x0436,
486 static_cast<uint16_t>((x << 8) | ((regs_.word & 0x03) << 1)));
487 w_.SetWord(kDoorOpenMask, static_cast<uint16_t>(w_.Word(kDoorOpenMask) |
488 tables_.DungeonMask(x)));
489 result.gfx = 0;
490 result.x = x;
491 }
492 result.carry = true;
493 return result;
494 }
495
496 // Rewrites the type of the door just flagged ($197E,X after the INX INX).
497 void RewriteDoorType(const FlagResult& flag, uint16_t type) {
498 const int address = 0x197E + flag.x;
499 w_.SetWord(address,
500 static_cast<uint16_t>((w_.Word(address) & 0xFF00) | type));
501 }
502
503 // RoomDraw_ChangeTilemapAddressToLowerLayer (#_01A8FA).
505 const int address = 0x199E + w_.Word(kDoorListIndex);
506 w_.SetWord(address, static_cast<uint16_t>(w_.Word(address) | 0x2000));
507 }
508
509 // RoomDraw_MarkLayerToggleDoor / RoomDraw_MarkDungeonToggleDoor.
510 void MarkToggleDoor(int counter, int list, uint16_t position) {
511 w_.SetWord(counter, Append(counter, list, position >> 1));
512 }
513
514 void AddExitDoor(uint16_t y) {
515 const uint16_t x = w_.Word(kExitDoorCount);
516 w_.SetWord(kExitDoorPositions + x, y);
517 w_.SetWord(kExitDoorCount, static_cast<uint16_t>(x + 2));
518 }
519
520 uint16_t Slot() const { return w_.Word(kDoorListIndex); }
521
522 // The far side of a door on a middle seam goes in slot | 0x10 and does not
523 // advance $0460.
524 template <typename Fn>
525 void WithPartnerSlot(Fn fn) {
526 const uint16_t saved = Slot();
527 w_.SetWord(kDoorListIndex, static_cast<uint16_t>(saved | 0x10));
528 fn();
529 w_.SetWord(kDoorListIndex, saved);
530 }
531
532 void OneSidedShuttersNorth(uint16_t y) {
533 const auto flag = FlagDoors(Slot(), 0, y);
534 if (flag.carry) {
535 if (flag.gfx == 0x36) {
536 RewriteDoorType(flag, 0x18);
537 } else if (flag.gfx == 0x38) {
538 RewriteDoorType(flag, 0x00);
539 }
540 }
541 }
542 void OneSidedShuttersSouth(uint16_t y) {
543 const auto flag = FlagDoors(Slot(), 1, y);
544 if (flag.carry) {
545 if (flag.gfx == 0x1E || flag.gfx == 0x36) {
546 RewriteDoorType(flag, 0x00);
547 } else if (flag.gfx == 0x38) {
548 RewriteDoorType(flag, 0x18);
549 }
550 }
551 }
552 void OneSidedShuttersWest(uint16_t y) {
553 const auto flag = FlagDoors(Slot(), 2, y);
554 if (flag.carry) {
555 if (flag.gfx == 0x36) {
556 RewriteDoorType(flag, 0x18);
557 } else if (flag.gfx == 0x38) {
558 RewriteDoorType(flag, 0x00);
559 }
560 }
561 }
562 void OneSidedShuttersEast(uint16_t y) {
563 const auto flag = FlagDoors(Slot(), 3, y);
564 if (flag.carry) {
565 if (flag.gfx == 0x36) {
566 RewriteDoorType(flag, 0x00);
567 } else if (flag.gfx == 0x38) {
568 RewriteDoorType(flag, 0x18);
569 }
570 }
571 }
572 // RoomDraw_OneSidedLowerShutters_*: the rewrite ignores the carry.
573 void OneSidedLowerShutters(uint16_t direction, uint16_t y) {
574 const auto flag = FlagDoors(Slot(), direction, y);
575 const bool north_or_west = direction == 0 || direction == 2;
576 if (flag.gfx == 0x48) {
577 RewriteDoorType(flag, north_or_west ? 0x44 : 0x40);
578 } else if (flag.gfx == 0x4A) {
579 RewriteDoorType(flag, north_or_west ? 0x40 : 0x44);
580 }
581 MoveLastDoorToLowerLayer();
582 }
583
584 // RoomDraw_CheckIfLowerLayerDoors_Vertical (#_01AA66).
585 void CheckIfLowerLayerDoorsVertical(uint16_t type, uint16_t y) {
586 OneSidedShuttersSouth(y);
587 if (type == 0x08) {
588 MoveLastDoorToLowerLayer();
589 }
590 }
591
592 // RoomDraw_NormalRangedDoors_North (#_01A90F).
593 void NormalRangedDoorsNorth(uint16_t y) {
594 if (regs_.position >= 0x0C) {
595 WithPartnerSlot([&] {
596 CheckIfLowerLayerDoorsVertical(
597 regs_.type,
598 RomWord(rom_, kDoorPositionsNorthMiddle + regs_.position));
599 });
600 regs_.gfx = regs_.type;
601 }
602 OneSidedShuttersNorth(y);
603 }
604
605 // RoomDraw_NormalRangedDoors_East (#_01ABC8).
606 void NormalRangedDoorsEast(uint16_t type, uint16_t y) {
607 OneSidedShuttersEast(y);
608 if (type == 0x08) {
609 MoveLastDoorToLowerLayer();
610 }
611 }
612
613 // RoomDraw_NormalRangedDoors_West (#_01AB1F).
614 void NormalRangedDoorsWest(uint16_t y) {
615 if (regs_.position >= 0x0C) {
616 WithPartnerSlot([&] {
617 NormalRangedDoorsEast(
618 regs_.type,
619 RomWord(rom_, kDoorPositionsWestMiddle + regs_.position));
620 });
621 regs_.gfx = regs_.type;
622 }
623 OneSidedShuttersWest(y);
624 }
625
626 // RoomDraw_Door_ExplodingWall (#_01AC70).
628 const uint16_t y = RomWord(rom_, kExplodingWallPositions + regs_.position);
629 const uint16_t x = Slot();
630 w_.SetWord(kDoorPositions + x, static_cast<uint16_t>(y + 0x14));
631 w_.SetWord(kDoorTypes + x, static_cast<uint16_t>(((x >> 1) << 8) | 0x30));
632 if ((w_.Word(kDoorOpenMask) & tables_.DungeonMask(x & 0x0F)) == 0) {
633 w_.SetWord(kDoorDirections + x, 0);
634 } else if (tag1_ == 0x20 || tag1_ == 0x25 || tag1_ == 0x28) {
635 tag1_ = 0;
636 } else {
637 tag2_ = 0;
638 }
639 w_.SetWord(kDoorListIndex, static_cast<uint16_t>(x + 2));
640 }
641
642 // RoomDraw_Door_North (#_01A81C).
643 void DoorNorth() {
644 const uint16_t y = RomWord(rom_, kDoorPositionsNorthWall + regs_.position);
645 const uint16_t t = regs_.type;
646 if (t == 0x30) {
648 } else if (t == 0x16) {
649 MarkToggleDoor(kLayerToggleCount, 0x06C0,
650 static_cast<uint16_t>(y - 0xFE));
651 } else if (t == 0x32) { // RoomDraw_NorthCurtainDoor
652 FlagDoors(Slot(), 0, y);
653 } else if (t == 0x06) {
654 // RoomDraw_MakeDoorHighPriorityLowerLayer_North flags the slot in X,
655 // which still holds the position index here, not $0460.
656 FlagDoors(regs_.position, 0, y);
657 } else if (t == 0x14) {
658 MarkToggleDoor(kDungeonToggleCount, 0x06D0,
659 static_cast<uint16_t>(y - 0xFE));
660 } else if (t == 0x02) {
661 NormalRangedDoorsNorth(y);
662 } else if (t == 0x12) {
663 AddExitDoor(y);
664 } else if (t == 0x08) {
665 NormalRangedDoorsNorth(y);
666 MoveLastDoorToLowerLayer();
667 } else if (t == 0x20 || t == 0x22 || t == 0x24 || t == 0x26) {
668 const uint16_t x = Slot();
669 w_.SetWord(kDoorDirections + x, 0);
670 w_.SetWord(kDoorPositions + x, y);
671 w_.SetWord(kDoorTypes + x, static_cast<uint16_t>(((x >> 1) << 8) | t));
672 if (tables_.DungeonMask(x & 0x0F) & w_.Word(kDoorOpenMask)) {
673 w_.SetWord(kDoorListIndex, static_cast<uint16_t>(x + 2));
674 } else if (t < 0x24) {
675 OneSidedShuttersNorth(y);
676 } else {
677 FlagDoors(Slot(), 0, y);
678 MoveLastDoorToLowerLayer();
679 }
680 } else if (t >= 0x40) { // RoomDraw_HighRangeDoor_North (#_01AD41)
681 if (regs_.position >= 0x0C && t != 0x46) {
682 WithPartnerSlot([&] {
683 OneSidedLowerShutters(
684 1, RomWord(rom_, kDoorPositionsNorthMiddle + regs_.position));
685 });
686 }
687 OneSidedLowerShutters(0, y);
688 } else {
689 NormalRangedDoorsNorth(y);
690 }
691 }
692
693 // RoomDraw_Door_South (#_01A984).
694 void DoorSouth() {
695 uint16_t y = RomWord(rom_, kDoorPositionsSouthMiddle + regs_.position);
696 const uint16_t t = regs_.type;
697 if (t == 0x16) {
698 MarkToggleDoor(kLayerToggleCount, 0x06C0,
699 static_cast<uint16_t>(y + 0x202));
700 } else if (t == 0x06) {
701 FlagDoors(regs_.position, 1, y);
702 } else if (t == 0x14) {
703 MarkToggleDoor(kDungeonToggleCount, 0x06D0,
704 static_cast<uint16_t>(y + 0x202));
705 } else if (t == 0x12) {
706 AddExitDoor(y);
707 } else if (t >= 0x40) {
708 OneSidedLowerShutters(1, y);
709 } else if (t == 0x0A || t == 0x0E || t == 0x10) {
710 // Fancy dungeon exit / RoomDraw_CaveExitLight /
711 // RoomDraw_HighPriorityExitLight.
712 FlagDoors(Slot(), 1, y);
713 } else if (t == 0x0C || t == 0x04) {
714 // Lower-layer fancy exit / RoomDraw_CheckIfExitDoor: Y | $2000.
715 y |= 0x2000;
716 FlagDoors(Slot(), 1, y);
717 } else {
718 CheckIfLowerLayerDoorsVertical(t, y);
719 }
720 }
721
722 // RoomDraw_Door_West (#_01AAD7).
723 void DoorWest() {
724 const uint16_t y = RomWord(rom_, kDoorPositionsWestWall + regs_.position);
725 const uint16_t t = regs_.type;
726 if (t == 0x16) {
727 MarkToggleDoor(kLayerToggleCount, 0x06C0,
728 static_cast<uint16_t>(y + 0x7C));
729 } else if (t == 0x06) {
730 FlagDoors(regs_.position, 2, y);
731 } else if (t == 0x14) {
732 MarkToggleDoor(kDungeonToggleCount, 0x06D0,
733 static_cast<uint16_t>(y + 0x7C));
734 } else if (t == 0x08) {
735 NormalRangedDoorsWest(y);
736 MoveLastDoorToLowerLayer();
737 } else if (t >= 0x40) { // RoomDraw_HighRangeDoor_West (#_01AE40)
738 if (regs_.position >= 0x0C) {
739 WithPartnerSlot([&] {
740 OneSidedLowerShutters(
741 3, RomWord(rom_, kDoorPositionsWestMiddle + regs_.position));
742 });
743 }
744 OneSidedLowerShutters(2, y);
745 } else {
746 NormalRangedDoorsWest(y);
747 }
748 }
749
750 // RoomDraw_Door_East (#_01AB99).
751 void DoorEast() {
752 const uint16_t y = RomWord(rom_, kDoorPositionsEastMiddle + regs_.position);
753 const uint16_t t = regs_.type;
754 if (t == 0x16) {
755 MarkToggleDoor(kLayerToggleCount, 0x06C0,
756 static_cast<uint16_t>(y + 0x88));
757 } else if (t == 0x06) {
758 FlagDoors(regs_.position, 3, y);
759 } else if (t == 0x14) {
760 MarkToggleDoor(kDungeonToggleCount, 0x06D0,
761 static_cast<uint16_t>(y + 0x88));
762 } else if (t >= 0x40) {
763 OneSidedLowerShutters(3, y);
764 } else {
765 NormalRangedDoorsEast(t, y);
766 }
767 }
768
769 // RoomDraw_DoorObject (#_018916).
770 void RecordDoor(const Room::Door& door) {
771 regs_.word = static_cast<uint16_t>(door.byte1 | (door.byte2 << 8));
772 regs_.position = static_cast<uint16_t>((door.byte1 & 0xF0) >> 3);
773 regs_.type = door.byte2;
774 regs_.gfx = door.byte2;
775 switch (door.byte1 & 0x03) {
776 case 0:
777 DoorNorth();
778 break;
779 case 1:
780 DoorSouth();
781 break;
782 case 2:
783 DoorWest();
784 break;
785 default:
786 DoorEast();
787 break;
788 }
789 }
790
791 const Rom& rom_;
797 uint8_t tag1_ = 0;
798 uint8_t tag2_ = 0;
799};
800
801// ---------------------------------------------------------------------------
802// Attribute passes over the recorded lists.
803
805 public:
806 explicit AttributeWriter(RoomCollisionMaps& maps) : maps_(maps) {}
807
808 // STA.l $7F2000,X (16-bit): two bytes. Bytes outside COLMAPA/B belong to
809 // other WRAM and are dropped.
810 void Store(int x, uint16_t value, CollisionSource source) {
811 StoreByte(x, static_cast<uint8_t>(value & 0xFF), source);
812 StoreByte(x + 1, static_cast<uint8_t>(value >> 8), source);
813 }
814 void StoreByte(int x, uint8_t value, CollisionSource source) {
815 if (x < 0 || x >= static_cast<int>(kRoomCollisionBytes)) {
816 return;
817 }
818 maps_.attributes[x] = value;
819 maps_.sources[x] = source;
820 }
821 uint16_t Word(int x) const {
822 auto byte = [&](int i) -> uint16_t {
823 return (i >= 0 && i < static_cast<int>(kRoomCollisionBytes))
824 ? maps_.attributes[i]
825 : 0;
826 };
827 return static_cast<uint16_t>(byte(x) | (byte(x + 1) << 8));
828 }
829
830 private:
832};
833
834// Loops `index` from its current value to `end` in steps of 2 (the game's
835// INY INY / CPY end / BNE). A list whose end is below the start would run
836// through all of WRAM in the game; stop at 0x80 instead.
837template <typename Fn>
838void ForEachEntry(uint16_t& index, uint16_t end, Fn fn) {
839 do {
840 fn(index);
841 index = static_cast<uint16_t>(index + 2);
842 } while (index != end && index < 0x80);
843}
844
845// Underworld_LoadObjectAttribute (#_01B967).
846void ApplyObjectAttributes(LowWram& w, uint8_t tag1, uint8_t tag2,
847 AttributeWriter& out) {
848 using S = CollisionSource;
849 uint16_t y = 0;
850 if (w.Word(kStarCount) != 0) {
851 ForEachEntry(y, w.Word(kStarCount), [&](uint16_t i) {
852 const int x = w.Word(0x06A0 + i);
853 out.Store(x, 0x3B3B, S::kStar);
854 out.Store(x + 0x40, 0x3B3B, S::kStar);
855 });
856 }
857
858 // Stairs: one list at $06B0 with running end indices; $00 counts up
859 // from 0x3030 (0x34.. for the "down" lists).
860 uint16_t counter = 0x3030;
861 y = 0;
862 auto next_counter = [&] {
863 counter = static_cast<uint16_t>(counter + 0x101);
864 };
865 auto stairs = [&](int end_address, auto&& body) {
866 const uint16_t end = w.Word(end_address);
867 if (y == end) {
868 return;
869 }
870 ForEachEntry(y, end, [&](uint16_t i) { body(w.Word(0x06B0 + i)); });
871 };
872 auto spiral = [&](uint16_t value) {
873 return [&, value](int x) {
874 out.Store(x + 0x01, value, S::kStairs);
875 out.Store(x + 0x81, value, S::kStairs);
876 out.Store(x + 0xC1, value, S::kStairs);
877 out.Store(x + 0x41, counter, S::kStairs);
878 next_counter();
879 };
880 };
881 auto straight_north = [&](int x) {
882 out.Store(x + 0x81, 0x0000, S::kStairs);
883 out.Store(x + 0xC1, 0x0000, S::kStairs);
884 out.Store(x + 0x01, 0x3838, S::kStairs);
885 out.Store(x + 0x41, counter, S::kStairs);
886 next_counter();
887 };
888 auto straight_south = [&](int x) {
889 out.Store(x + 0x01, 0x0000, S::kStairs);
890 out.Store(x + 0x41, 0x0000, S::kStairs);
891 out.Store(x + 0xC1, 0x3939, S::kStairs);
892 out.Store(x + 0x81, counter, S::kStairs);
893 next_counter();
894 };
895 if (w.Word(0x0438) != 0) {
896 stairs(0x0438, [&](int x) { // .next_intraroom_stairs_up
897 out.Store(x + 0x81, 0x0000, S::kStairs);
898 out.Store(x + 0x01, 0x2626, S::kStairs);
899 out.Store(x + 0x41, counter, S::kStairs);
900 next_counter();
901 });
902 }
903 stairs(0x047E, spiral(0x5E5E));
904 stairs(0x0482, spiral(0x5F5F));
905 stairs(0x04A2, straight_north);
906 stairs(0x04A4, straight_south);
907 counter = static_cast<uint16_t>((counter & 0x0707) | 0x3434);
908 stairs(0x043A, [&](int x) { // .next_intra_stairs_south
909 out.Store(x + 0xC1, 0x2626, S::kStairs);
910 out.Store(x + 0x81, counter, S::kStairs);
911 next_counter();
912 });
913 stairs(0x0480, spiral(0x5E5E));
914 stairs(0x0484, spiral(0x5F5F));
915 stairs(0x04A6, straight_north);
916 stairs(0x04A8, straight_south);
917
918 // Auto north multi-layer / merged stairs ($06B8).
919 y = 0;
920 uint16_t end = 0;
921 uint16_t value = 0;
922 if (w.Word(0x043C) != 0) {
923 value = 0x1F1F;
924 end = w.Word(0x043C);
925 } else if (w.Word(0x043E) != 0) {
926 value = 0x1E1E;
927 end = w.Word(0x043E);
928 } else if (w.Word(0x0440) != 0) {
929 value = 0x1D1D;
930 end = w.Word(0x0440);
931 }
932 if (value != 0) {
933 ForEachEntry(y, end, [&](uint16_t i) {
934 const int x = w.Word(0x06B8 + i);
935 out.Store(x + 0x0000, 0x0002, S::kLayerStairs);
936 out.Store(x + 0x10C0, 0x0002, S::kLayerStairs);
937 out.Store(x + 0x0002, 0x0200, S::kLayerStairs);
938 out.Store(x + 0x10C2, 0x0200, S::kLayerStairs);
939 out.Store(x + 0x0040, 0x0001, S::kLayerStairs);
940 out.Store(x + 0x1080, 0x0001, S::kLayerStairs);
941 out.Store(x + 0x0042, 0x0100, S::kLayerStairs);
942 out.Store(x + 0x1082, 0x0100, S::kLayerStairs);
943 out.Store(x + 0x0041, value, S::kLayerStairs);
944 out.Store(x + 0x1041, value, S::kLayerStairs);
945 out.Store(x + 0x0081, value, S::kLayerStairs);
946 out.Store(x + 0x1081, value, S::kLayerStairs);
947 });
948 }
949 if (y != w.Word(0x0448)) { // .next_water_overlay_a
950 ForEachEntry(y, w.Word(0x0448), [&](uint16_t i) {
951 const int x = w.Word(0x06B8 + i);
952 out.Store(x + 0x0000, 0x0A03, S::kWater);
953 out.Store(x + 0x1000, 0x0A03, S::kWater);
954 out.Store(x + 0x0002, 0x030A, S::kWater);
955 out.Store(x + 0x1002, 0x030A, S::kWater);
956 out.Store(x + 0x0040, 0x0803, S::kWater);
957 out.Store(x + 0x0042, 0x0308, S::kWater);
958 });
959 }
960 y = 0;
961 if (w.Word(0x0442) != 0) { // .next_water_overlay_a2
962 ForEachEntry(y, w.Word(0x0442), [&](uint16_t i) {
963 const int x = w.Word(0x06B8 + i);
964 out.Store(x + 0x0000, 0x0003, S::kWater);
965 out.Store(x + 0x0002, 0x0300, S::kWater);
966 out.Store(x + 0x1000, 0x0A03, S::kWater);
967 out.Store(x + 0x1002, 0x030A, S::kWater);
968 out.Store(x + 0x0040, 0x0808, S::kWater);
969 out.Store(x + 0x0042, 0x0808, S::kWater);
970 });
971 }
972 if (y != w.Word(0x0444)) { // .next_water_ladder
973 ForEachEntry(y, w.Word(0x0444), [&](uint16_t i) {
974 const int x = w.Word(0x06B8 + i);
975 out.Store(x + 0x0000, 0x0003, S::kWater);
976 out.Store(x + 0x0002, 0x0300, S::kWater);
977 out.Store(x + 0x1000, 0x0A03, S::kWater);
978 out.Store(x + 0x1002, 0x030A, S::kWater);
979 });
980 }
981
982 // Manipulables ($0500 type, $0540 position): 0x70, 0x71, ... skipping
983 // bombable floor segments (type 0x3X) but still counting them.
984 y = 0;
985 if (w.Word(kManipulableCount) != 0) {
986 counter = 0x7070;
987 ForEachEntry(y, w.Word(kManipulableCount), [&](uint16_t i) {
988 if ((w.Word(0x0500 + i) & 0x00F0) != 0x0030) {
989 const int x = (w.Word(0x0540 + i) & 0x3FFF) >> 1;
990 out.Store(x, counter, S::kManipulable);
991 out.Store(x + 0x40, counter, S::kManipulable);
992 }
993 next_counter();
994 });
995 }
996 // Torches follow in the same $0540 list: 0xC0, 0xC1, ...
997 if (y != w.Word(kTorchEnd)) {
998 counter = 0xC0C0;
999 ForEachEntry(y, w.Word(kTorchEnd), [&](uint16_t i) {
1000 const int x = (w.Word(0x0540 + i) & 0x3FFF) >> 1;
1001 out.Store(x, counter, S::kTorch);
1002 out.Store(x + 0x40, counter, S::kTorch);
1003 counter = static_cast<uint16_t>((counter & 0xEFEF) + 0x0101);
1004 });
1005 }
1006
1007 // Chests and big key locks ($06E0): 0x58, 0x59, ...
1008 auto hides_chests = [](uint8_t tag) {
1009 return tag == 0x27 || tag == 0x3C || tag == 0x3E ||
1010 (tag >= 0x29 && tag < 0x33);
1011 };
1012 counter = 0x5858;
1013 y = 0;
1014 bool skip_locks = false;
1015 if (w.Word(kChestCount) != 0) {
1016 if (hides_chests(tag1) || hides_chests(tag2)) {
1017 skip_locks = true; // BEQ .hidden_chests skips the locks too
1018 } else {
1019 // Underworld_SetChestAttributes (#_01BDDB).
1020 ForEachEntry(y, w.Word(kChestCount), [&](uint16_t i) {
1021 const uint16_t entry = w.Word(0x06E0 + i);
1022 if (entry != 0) {
1023 const int x = (entry & 0x7FFF) >> 1;
1024 out.Store(x, counter, S::kChest);
1025 out.Store(x + 0x40, counter, S::kChest);
1026 if (entry & 0x8000) { // big chest: 4x4 tiles
1027 w.SetWord(0x06E0 + i, static_cast<uint16_t>(entry & 0x7FFF));
1028 out.Store(x + 0x42, counter, S::kChest);
1029 out.Store(x + 0x80, counter, S::kChest);
1030 out.Store(x + 0x82, counter, S::kChest);
1031 }
1032 }
1033 next_counter();
1034 });
1035 }
1036 }
1037 if (!skip_locks && y != w.Word(kBigKeyLockEnd)) {
1038 ForEachEntry(y, w.Word(kBigKeyLockEnd), [&](uint16_t i) {
1039 const uint16_t entry = w.Word(0x06E0 + i);
1040 w.SetWord(0x06E0 + i, static_cast<uint16_t>(entry | 0x8000));
1041 const int x = (entry & 0x7FFF) >> 1;
1042 out.Store(x, counter, S::kBigKeyLock);
1043 out.Store(x + 0x40, counter, S::kBigKeyLock);
1044 next_counter();
1045 });
1046 }
1047
1048 // Auto south multi-layer stairs ($06EC).
1049 y = 0;
1050 value = 0;
1051 if (w.Word(0x049A) != 0) {
1052 value = 0x3F3F;
1053 end = w.Word(0x049A);
1054 } else if (w.Word(0x049C) != 0) {
1055 value = 0x3E3E;
1056 end = w.Word(0x049C);
1057 } else if (w.Word(0x049E) != 0) {
1058 value = 0x3D3D;
1059 end = w.Word(0x049E);
1060 }
1061 if (value != 0) {
1062 ForEachEntry(y, end, [&](uint16_t i) {
1063 const int x = w.Word(0x06EC + i);
1064 out.Store(x + 0x1000, 0x0002, S::kLayerStairs);
1065 out.Store(x + 0x00C0, 0x0002, S::kLayerStairs);
1066 out.Store(x + 0x1040, 0x0001, S::kLayerStairs);
1067 out.Store(x + 0x0080, 0x0001, S::kLayerStairs);
1068 out.Store(x + 0x1002, 0x0200, S::kLayerStairs);
1069 out.Store(x + 0x00C2, 0x0200, S::kLayerStairs);
1070 out.Store(x + 0x1042, 0x0100, S::kLayerStairs);
1071 out.Store(x + 0x0082, 0x0100, S::kLayerStairs);
1072 out.Store(x + 0x0041, value, S::kLayerStairs);
1073 out.Store(x + 0x1041, value, S::kLayerStairs);
1074 out.Store(x + 0x0081, value, S::kLayerStairs);
1075 out.Store(x + 0x1081, value, S::kLayerStairs);
1076 });
1077 }
1078 y = 0;
1079 if (w.Word(0x04AE) != 0) { // .next_south_merged_auto_stairs
1080 ForEachEntry(y, w.Word(0x04AE), [&](uint16_t i) {
1081 const int x = w.Word(0x06EC + i);
1082 out.Store(x + 0x10C0, 0x0A03, S::kWater);
1083 out.Store(x + 0x10C2, 0x030A, S::kWater);
1084 out.Store(x + 0x00C0, 0x0003, S::kWater);
1085 out.Store(x + 0x00C2, 0x0300, S::kWater);
1086 out.Store(x + 0x0080, 0x0808, S::kWater);
1087 out.Store(x + 0x0082, 0x0808, S::kWater);
1088 });
1089 }
1090}
1091
1092// Underworld_LoadDoorAttribute (#_01BE17) and its callees.
1094 using S = CollisionSource;
1095 RomTables tables(rom);
1096 auto doorway_properties = [&](uint16_t type) {
1097 return RomWord(rom, kDoorwayTileProperties + type);
1098 };
1099 auto is_exit_door = [&](uint16_t position) {
1100 for (int i = 0; i < 4; ++i) {
1101 if (position == w.Word(kExitDoorPositions + i * 2)) {
1102 return true;
1103 }
1104 }
1105 return false;
1106 };
1107
1108 for (uint16_t y = 0; y < 0x20; y = static_cast<uint16_t>(y + 2)) {
1109 const uint16_t position = w.Word(kDoorPositions + y);
1110 if (position == 0) {
1111 continue;
1112 }
1113 const uint16_t type = w.Word(kDoorTypes + y) & 0x00FE;
1114 const uint16_t direction = w.Word(kDoorDirections + y) & 0x0003;
1115
1116 // Closed door: F0 | slot on the two middle rows/columns.
1117 auto locked = [&] {
1118 const uint8_t v = static_cast<uint8_t>((y >> 1) | 0xF0);
1119 const int x = position >> 1;
1120 out.Store(x + 0x41, static_cast<uint16_t>(v | (v << 8)), S::kLockedDoor);
1121 out.Store(x + 0x81, static_cast<uint16_t>(v | (v << 8)), S::kLockedDoor);
1122 };
1123
1124 // AddFullLongDoorDoorwayProps (#_01C0B8).
1125 auto full_long = [&] {
1126 uint16_t v = doorway_properties(type);
1127 if (direction == 0) {
1128 const int x = (position >> 1) & 0x783F;
1129 for (int k = 0; k < 10; ++k) {
1130 out.Store(x + 0x01 + 0x40 * k, v, S::kDoor);
1131 }
1132 } else if (direction == 1) {
1133 if (type == 0x0C || type == 0x10 || type == 0x04 ||
1134 is_exit_door(position & 0x1FFF)) {
1135 v = 0x8E8E;
1136 }
1137 const int x = (position >> 1) + 0x40;
1138 for (int k = 0; k < 8; ++k) {
1139 out.Store(x + 0x01 + 0x40 * k, v, S::kDoor);
1140 }
1141 } else {
1142 const int x =
1143 direction == 2 ? ((position >> 1) & 0xFFE0) : (position >> 1) + 1;
1144 const uint16_t v2 = static_cast<uint16_t>(v + 0x0101);
1145 for (int o : {0x40, 0x42, 0x44, 0x46, 0x80, 0x82, 0x84, 0x86}) {
1146 out.Store(x + o, v2, S::kDoor);
1147 }
1148 }
1149 };
1150
1151 // Underworld_LoadSingleDoorAttribute .apply_doorway (#_01BEB8).
1152 auto doorway = [&] {
1153 if (type >= 0x20 && type < 0x28) {
1154 return;
1155 }
1156 uint16_t v = doorway_properties(type);
1157 if (direction == 0) {
1158 if (is_exit_door(position)) {
1159 v = 0x8E8E;
1160 }
1161 const int x = (position >> 1) & 0x783F;
1162 for (int k = 0; k < 7; ++k) {
1163 out.Store(x + 0x01 + 0x40 * k, v, S::kDoor);
1164 }
1165 out.Store(x + 0x1C1, 0x0000, S::kDoor);
1166 } else if (direction == 1) {
1167 if (type == 0x0A || type == 0x0E || is_exit_door(position)) {
1168 v = 0x8E8E;
1169 }
1170 const int x = position >> 1;
1171 for (int o : {0x41, 0x81, 0xC1, 0x101, 0x141}) {
1172 out.Store(x + o, v, S::kDoor);
1173 }
1174 } else if (direction == 2) {
1175 const int x = (position >> 1) & 0xFFE0;
1176 const uint16_t v2 = static_cast<uint16_t>(v + 0x0101);
1177 for (int o : {0x40, 0x42, 0x80, 0x82}) {
1178 out.Store(x + o, v2, S::kDoor);
1179 }
1180 out.Store(x + 0x44, v2 & 0x00FF, S::kDoor);
1181 out.Store(x + 0x84, v2 & 0x00FF, S::kDoor);
1182 } else {
1183 const int x = position >> 1;
1184 const uint16_t v2 = static_cast<uint16_t>(v + 0x0101);
1185 for (int o : {0x42, 0x44, 0x82, 0x84}) {
1186 out.Store(x + o, v2, S::kDoor);
1187 }
1188 out.Store(x + 0x40, v2 & 0xFF00, S::kDoor);
1189 out.Store(x + 0x80, v2 & 0xFF00, S::kDoor);
1190 }
1191 };
1192
1193 const uint16_t open_mask = w.Word(kDoorOpenMask);
1194 if (type == 0x00 || type == 0x06 || type == 0x12 || type == 0x0A ||
1195 type == 0x0E) {
1196 doorway();
1197 } else if (type == 0x0C || type == 0x10 || type == 0x04 || type == 0x02 ||
1198 type == 0x08) {
1199 full_long();
1200 } else if (type == 0x30) {
1201 // Exploding wall: nothing until it is blown open.
1202 } else if (type >= 0x40) { // AddDoorwayPropsForWeirdos (#_01C085)
1203 if (type == 0x40 || type == 0x46 ||
1204 (open_mask & tables.DungeonMask(y & 0xFF))) {
1205 full_long();
1206 } else {
1207 locked();
1208 }
1209 } else {
1210 const uint16_t mask_index =
1211 type == 0x18 ? (y & 0xFF) : (y & 0x0F); // 0x44 is handled above.
1212 if (open_mask & tables.DungeonMask(mask_index)) {
1213 doorway();
1214 } else {
1215 locked();
1216 }
1217 }
1218 }
1219
1220 // Underworld_LoadSingleDoorTileType (#_01D51F): layer toggle doors OR
1221 // 0x10, dungeon toggle doors OR 0x20, on the upper map when the tile there
1222 // is a doorway (0x8X), otherwise on the lower map.
1223 auto toggles = [&](int counter, int list, uint16_t bits) {
1224 const uint16_t count = w.Word(counter);
1225 if (count == 0) {
1226 return;
1227 }
1228 uint16_t y = 0;
1229 ForEachEntry(y, count, [&](uint16_t i) {
1230 const int x = w.Word(list + i);
1231 if ((out.Word(x) & 0x00F0) == 0x0080) {
1232 const uint16_t v = out.Word(x) | bits;
1233 out.Store(x, v, S::kDoorLayerToggle);
1234 out.Store(x + 0x40, v, S::kDoorLayerToggle);
1235 } else {
1236 const uint16_t v = out.Word(x + 0x1000) | bits;
1237 out.Store(x + 0x1000, v, S::kDoorLayerToggle);
1238 out.Store(x + 0x1040, v, S::kDoorLayerToggle);
1239 }
1240 });
1241 };
1242 toggles(kLayerToggleCount, 0x06C0, 0x1010);
1243 toggles(kDungeonToggleCount, 0x06D0, 0x2020);
1244
1245 // ChangeDoorToSwitch (#_01C1BA).
1246 const uint16_t door_switch = w.Word(kDoorSwitch);
1247 if (door_switch != 0) {
1248 int x = (door_switch & 0x3FFF) >> 1;
1249 const int columns = (door_switch & 0x8000) ? 6 : 5;
1250 const bool open = (w.Word(kRoomFlags) & 0x1000) != 0;
1251 for (int i = 0; i < columns; ++i, x += 2) {
1252 if (open) {
1253 out.Store(x, 0x0101, S::kDoorSwitch);
1254 out.Store(x + 0x280, 0x0101, S::kDoorSwitch);
1255 for (int o : {0x80, 0x100, 0x180, 0x200}) {
1256 out.Store(x + o, 0x0000, S::kDoorSwitch);
1257 }
1258 } else {
1259 for (int o : {0x80, 0x100, 0x180, 0x200}) {
1260 out.Store(x + o, 0x2323, S::kDoorSwitch);
1261 }
1262 }
1263 }
1264 }
1265}
1266
1267} // namespace
1268
1270 switch (source) {
1271 case CollisionSource::kTilemap:
1272 return "tilemap";
1273 case CollisionSource::kStar:
1274 return "star";
1275 case CollisionSource::kStairs:
1276 return "stairs";
1277 case CollisionSource::kLayerStairs:
1278 return "layer stairs";
1279 case CollisionSource::kWater:
1280 return "water stairs";
1281 case CollisionSource::kManipulable:
1282 return "pot/block";
1283 case CollisionSource::kTorch:
1284 return "torch";
1285 case CollisionSource::kChest:
1286 return "chest";
1287 case CollisionSource::kBigKeyLock:
1288 return "big key lock";
1289 case CollisionSource::kDoor:
1290 return "door";
1291 case CollisionSource::kLockedDoor:
1292 return "locked door";
1293 case CollisionSource::kDoorLayerToggle:
1294 return "toggle door";
1295 case CollisionSource::kDoorSwitch:
1296 return "door switch";
1297 case CollisionSource::kCrystalPeg:
1298 return "crystal peg";
1299 }
1300 return "?";
1301}
1302
1303std::array<uint8_t, kTileAttributeTableSize> LoadUnderworldTileAttributeTable(
1304 const Rom& rom, uint8_t blockset) {
1305 std::array<uint8_t, kTileAttributeTableSize> table{};
1306 // LoadDefaultTileTypes (#_0E97D9).
1307 for (int i = 0; i < 0x140; ++i) {
1308 table[i] = RomByte(rom, kUnderworldTileTypes + i);
1309 }
1310 for (int i = 0; i < 0x40; ++i) {
1311 table[0x1C0 + i] = RomByte(rom, kUnderworldTileTypes + 0x140 + i);
1312 }
1313 // Underworld_LoadCustomTileTypes (#_0E942A).
1314 const uint16_t offset = RomWord(rom, kCustomTileTypesOffset + blockset * 2);
1315 for (int i = 0; i < 0x80; ++i) {
1316 table[0x140 + i] = RomByte(rom, kCustomUnderworldTileTypes + offset + i);
1317 }
1318 return table;
1319}
1320
1322 const std::array<uint8_t, kTileAttributeTableSize>& table,
1323 const RoomTilemaps& tilemaps) {
1324 RoomCollisionMaps maps;
1325 // Underworld_LoadBasicAttribute_full (#_01B8F3).
1326 auto attribute = [&](uint16_t word) -> uint8_t {
1327 const uint16_t tile = word & 0x03FF;
1328 // Tiles 0x200+ index past TILEATTR into $7F0000 (decompression buffer);
1329 // no vanilla room uses them. Treat them as 0.
1330 uint8_t value = tile < kTileAttributeTableSize ? table[tile] : 0;
1331 if (value >= 0x10 && value < 0x1C) {
1332 // bit 1 = vertical flip (word bit 15), bit 0 = horizontal (bit 14).
1333 value |=
1334 static_cast<uint8_t>(((word >> 15) & 1) << 1 | ((word >> 14) & 1));
1335 }
1336 return value;
1337 };
1338 for (size_t i = 0; i < kCollisionMapTiles; ++i) {
1339 const uint16_t upper = i < tilemaps.bg1.size() ? tilemaps.bg1[i] : 0;
1340 const uint16_t lower = i < tilemaps.bg2.size() ? tilemaps.bg2[i] : 0;
1341 maps.attributes[i] = attribute(upper);
1342 maps.attributes[kCollisionMapTiles + i] = attribute(lower);
1343 }
1344 maps.sources.fill(CollisionSource::kTilemap);
1345 return maps;
1346}
1347
1349 const Rom& rom, const RoomCollisionInput& input,
1350 const UnderworldRoomLoadState& state) {
1351 // Underworld_LoadAttributeTable (#_01B8BF).
1354
1355 RoomLoadReplay replay(rom, input, state);
1356 replay.Run();
1357
1358 AttributeWriter out(maps);
1359 ApplyObjectAttributes(replay.wram(), replay.tag1(), replay.tag2(), out);
1360 ApplyDoorAttributes(rom, replay.wram(), out);
1361
1362 if (state.crystal_pegs_swapped) { // Underworld_FlipCrystalPegAttribute
1363 for (size_t i = 0; i < kRoomCollisionBytes; ++i) {
1364 if (maps.attributes[i] == 0x66 || maps.attributes[i] == 0x67) {
1365 maps.attributes[i] ^= 1;
1366 maps.sources[i] = CollisionSource::kCrystalPeg;
1367 }
1368 }
1369 }
1370 return maps;
1371}
1372
1373std::vector<StaircaseSlotResolution> ResolveVanillaStaircaseSlots(
1374 const Rom& rom, const RoomCollisionInput& input,
1375 const UnderworldRoomLoadState& state) {
1376 struct Trigger {
1377 int edge = 0;
1378 int index = 0;
1379 uint8_t edge_type = 0;
1380 };
1381 std::vector<StaircaseSlotResolution> results;
1382 std::vector<Trigger> triggers;
1383 for (size_t i = 0; i < input.objects.size(); ++i) {
1384 const auto& object = input.objects[i];
1385 if (!UsesRoomObjectStream(object))
1386 continue;
1387 int edge_row = object.y();
1388 int index_row = object.y() + 1;
1389 int last_row = object.y() + 3;
1390 bool lower = object.GetLayerValue() == 1;
1391 uint8_t edge_type = 0;
1392 switch (object.id_) {
1393 case 0x12D:
1394 edge_type = 0x26;
1395 break;
1396 case 0x12E:
1397 case 0x12F:
1398 edge_type = 0x26;
1399 edge_row += 3;
1400 index_row += 1;
1401 break;
1402 case 0x138:
1403 case 0x139:
1404 case 0x13A:
1405 case 0x13B:
1406 edge_type = object.id_ < 0x13A ? 0x5E : 0x5F;
1407 --edge_row;
1408 --index_row;
1409 --last_row;
1410 break;
1411 case 0xF9E:
1412 case 0xF9F:
1413 lower = false; // Upper straight writers ignore the stream layer.
1414 [[fallthrough]];
1415 case 0xFA6:
1416 case 0xFA7:
1417 edge_type = 0x38;
1418 break;
1419 case 0xFA0:
1420 case 0xFA1:
1421 lower = false;
1422 [[fallthrough]];
1423 case 0xFA8:
1424 case 0xFA9:
1425 edge_type = 0x39;
1426 edge_row += 3;
1427 index_row += 1;
1428 break;
1429 default:
1430 continue;
1431 }
1433 result.object_index = i;
1434 if (object.GetLayerValue() > 2 || object.x() > 60 || edge_row < 0 ||
1435 last_row >= 64 || index_row >= 64) {
1436 result.status = StaircaseSlotStatus::kOutsideRoom;
1437 }
1438 const int layer = lower ? kCollisionMapTiles : 0;
1439 triggers.push_back({layer + edge_row * 64 + object.x() + 1,
1440 layer + index_row * 64 + object.x() + 1, edge_type});
1441 results.push_back(result);
1442 }
1443 // The position table shares storage with other lists after its four words.
1444 // Do not infer editable destinations from overflow/wrapped indices.
1445 if (results.size() > 4) {
1446 for (auto& result : results)
1447 result.status = StaircaseSlotStatus::kTooManyStairs;
1448 return results;
1449 }
1450 if (results.empty())
1451 return results;
1452 const auto maps = ComputeRoomCollisionMaps(rom, input, state);
1453 for (size_t i = 0; i < results.size(); ++i) {
1454 auto& result = results[i];
1455 if (result.status == StaircaseSlotStatus::kOutsideRoom)
1456 continue;
1457 const auto& t = triggers[i];
1458 bool overlap = false;
1459 for (size_t j = 0; j < results.size(); ++j) {
1460 if (i == j || results[j].status == StaircaseSlotStatus::kOutsideRoom)
1461 continue;
1462 for (int a : {t.edge, t.edge + 1, t.index, t.index + 1}) {
1463 const auto& other = triggers[j];
1464 for (int b : {other.edge, other.edge + 1, other.index, other.index + 1})
1465 overlap |= a == b;
1466 }
1467 }
1468 if (overlap) {
1469 result.status = StaircaseSlotStatus::kOverlappingStairs;
1470 continue;
1471 }
1472 const uint8_t attribute = maps.attributes[t.index];
1473 bool intact = (attribute & 0xF8) == 0x30;
1474 for (int x = 0; x < 2; ++x) {
1475 intact &= maps.sources[t.index + x] == CollisionSource::kStairs &&
1476 maps.sources[t.edge + x] == CollisionSource::kStairs &&
1477 maps.attributes[t.index + x] == attribute &&
1478 maps.attributes[t.edge + x] == t.edge_type;
1479 }
1480 if (intact) {
1481 result.status = StaircaseSlotStatus::kResolved;
1482 result.slot = attribute & 3; // $01C3CA: runtime $0462 & 3.
1483 }
1484 }
1485 return results;
1486}
1487
1489 const RoomTilemaps& tilemaps) {
1490 RoomCollisionInput input;
1491 input.room_id = room.id();
1492 input.blockset = room.blockset();
1493 input.tag1 = static_cast<uint8_t>(room.tag1());
1494 input.tag2 = static_cast<uint8_t>(room.tag2());
1495 input.objects = room.GetTileObjects();
1496 input.doors = room.GetDoors();
1497 input.tilemaps = tilemaps;
1498 return input;
1499}
1500
1504
1505} // namespace yaze::zelda3
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
uint8_t block_behavior_layer() const
uint8_t GetLayerValue() const
uint8_t blockset() const
Definition room.h:1015
const std::vector< Door > & GetDoors() const
Definition room.h:413
TagKey tag2() const
Definition room.h:988
TagKey tag1() const
Definition room.h:987
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
int id() const
Definition room.h:1012
void StoreByte(int x, uint8_t value, CollisionSource source)
void Store(int x, uint16_t value, CollisionSource source)
void SetCounters(std::initializer_list< int > counters, uint16_t value)
void AddManipulable(uint16_t type, uint16_t y, bool lower_layer)
FlagResult FlagDoors(uint16_t x, uint16_t direction, uint16_t y)
uint16_t Append(int counter, int list, uint16_t value)
void MarkToggleDoor(int counter, int list, uint16_t position)
void RecordObject(const RoomObject &object, bool lower_layer)
RoomLoadReplay(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
void ForEachEntry(uint16_t &index, uint16_t end, Fn fn)
void ApplyDoorAttributes(const Rom &rom, LowWram &w, AttributeWriter &out)
void ApplyObjectAttributes(LowWram &w, uint8_t tag1, uint8_t tag2, AttributeWriter &out)
Zelda 3 specific classes and functions.
@ ExplodingWall
Exploding wall.
RoomTilemaps ComposeYazeRoomTilemaps(const Room &room)
constexpr size_t kRoomCollisionBytes
constexpr size_t kTileAttributeTableSize
std::array< uint8_t, kTileAttributeTableSize > LoadUnderworldTileAttributeTable(const Rom &rom, uint8_t blockset)
RoomCollisionMaps ComputeBasicCollisionMaps(const std::array< uint8_t, kTileAttributeTableSize > &table, const RoomTilemaps &tilemaps)
RoomCollisionMaps ComputeRoomCollisionMaps(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
bool UsesRoomObjectStream(const RoomObject &object)
RoomCollisionInput MakeRoomCollisionInput(const Room &room, const RoomTilemaps &tilemaps)
std::vector< StaircaseSlotResolution > ResolveVanillaStaircaseSlots(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
const char * CollisionSourceName(CollisionSource source)
std::vector< RoomObject > objects
std::vector< Room::Door > doors
std::array< CollisionSource, kRoomCollisionBytes > sources
std::array< uint8_t, kRoomCollisionBytes > attributes
Represents a door in a dungeon room.
Definition room.h:339
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:344
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:345
std::optional< uint16_t > door_open_mask