yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
entrance_layout.h
Go to the documentation of this file.
1#ifndef YAZE_ZELDA3_DUNGEON_ENTRANCE_LAYOUT_H_
2#define YAZE_ZELDA3_DUNGEON_ENTRANCE_LAYOUT_H_
3
4#include <algorithm>
5#include <array>
6#include <cstddef>
7#include <cstdint>
8#include <initializer_list>
9#include <span>
10#include <utility>
11
12#include "absl/status/status.h"
13#include "absl/status/statusor.h"
14#include "absl/strings/str_format.h"
16
17namespace yaze::zelda3 {
18
20
41
42// Table PC address and one record's byte width. Keep the existing 133 regular
43// entrance IDs; recognizing relocated storage does not activate additional IDs.
44using EntranceTable = std::pair<uint32_t, uint32_t>;
47 std::array<EntranceTable, static_cast<size_t>(EntranceField::kCount)> tables;
48
49 uint32_t table(EntranceField field) const {
50 return tables[static_cast<size_t>(field)].first;
51 }
52 uint32_t address(EntranceField field, int id) const {
53 const auto& [base, width] = tables[static_cast<size_t>(field)];
54 return base + static_cast<uint32_t>(id) * width;
55 }
56};
57
60 {{{0x14813, 2},
61 {0x1491D, 8},
62 {0x14E4F, 2},
63 {0x14D45, 2},
64 {0x14F59, 2},
65 {0x15063, 2},
66 {0x1516D, 2},
67 {0x15277, 2},
68 {0x15381, 1},
69 {0x15406, 1},
70 {0x1548B, 1},
71 {0x15510, 1},
72 {0x15595, 1},
73 {0x1561A, 1},
74 {0x1569F, 1},
75 {0x15724, 2},
76 {0x1582E, 1}}}};
77
80 {{{0x78000, 2},
81 {0x78200, 8},
82 {0x78C00, 2},
83 {0x78A00, 2},
84 {0x78E00, 2},
85 {0x79000, 2},
86 {0x79200, 2},
87 {0x79400, 2},
88 {0x79600, 1},
89 {0x79700, 1},
90 {0x79800, 1},
91 {0x79900, 1},
92 {0x79A00, 1},
93 {0x79B00, 1},
94 {0x79C00, 1},
95 {0x79D00, 2},
96 {0x79F00, 1}}}};
97
98// The actual expanded loader, unlike ZScream's CameraX/CameraY constant names,
99// reads vertical scroll from $0F8C00 and horizontal scroll from $0F8A00.
100// Each site names the instruction's PC offset, opcode and table being loaded.
102 uint32_t pc;
103 uint8_t opcode;
105};
106inline constexpr std::array<EntranceLoaderSite, 17> kEntranceLoaderSites = {{
107 {0x159A3, 0xBD, EntranceField::kRoom},
108 {0x15A1A, 0xB9, EntranceField::kScrollEdge},
109 {0x159AB, 0xBD, EntranceField::kYScroll},
110 {0x159B8, 0xBD, EntranceField::kXScroll},
111 {0x159CB, 0xBD, EntranceField::kYPosition},
112 {0x159D0, 0xBD, EntranceField::kXPosition},
113 {0x159D5, 0xBD, EntranceField::kCameraYTrigger},
114 {0x159E0, 0xBD, EntranceField::kCameraXTrigger},
115 {0x15A74, 0xBD, EntranceField::kBlockset},
116 {0x15A91, 0xBD, EntranceField::kFloor},
117 {0x15A96, 0xBD, EntranceField::kDungeon},
118 {0x15A9C, 0xBD, EntranceField::kDoor},
119 {0x15AA1, 0xBD, EntranceField::kLadderBG},
120 {0x15AB2, 0xBD, EntranceField::kScrolling},
121 {0x15AC2, 0xBD, EntranceField::kScrollQuadrant},
122 {0x159F0, 0xBD, EntranceField::kExit},
123 {0x15A7A, 0xBD, EntranceField::kMusic},
124}};
125
126// Resolve against this ROM's current bytes, never process-global settings or
127// a previous session's detection. A recognized but inconsistent expansion must
128// not fall back to the populated, stale vanilla tables.
129inline absl::StatusOr<EntranceLayout> ResolveEntranceLayout(
130 std::span<const uint8_t> data) {
131 const auto matches = [&](size_t pc, std::initializer_list<uint8_t> bytes) {
132 return pc <= data.size() && bytes.size() <= data.size() - pc &&
133 std::equal(bytes.begin(), bytes.end(), data.begin() + pc);
134 };
135 const bool marker = matches(0x7F000, {0x00, 0x01});
136 const bool hook = matches(0x1599F, {0x22});
137 const bool relocated_load = matches(0x15A74, {0xBD, 0x00, 0x96});
139 if (marker || hook || relocated_load) {
140 const auto invalid = [](const char* reason) {
141 return absl::FailedPreconditionError(absl::StrFormat(
142 "Unsupported or malformed expanded dungeon entrance layout: %s",
143 reason));
144 };
145 if (!marker || !matches(0x1599F, {0x22, 0x08, 0xF0, 0x0F}) ||
146 !matches(0x15ACE, {0x22, 0x15, 0xF0, 0x0F})) {
147 return invalid("marker or databank hooks do not match");
148 }
149 if (!matches(0x7F008, {0xAA, 0x0A, 0x0A, 0xA8, 0xE2, 0x20, 0xA9, 0x0F, 0x48,
150 0xAB, 0xC2, 0x20, 0x6B}) ||
151 !matches(0x7F015,
152 {0x29, 0x0F, 0x85, 0xAA, 0xA9, 0x02, 0x48, 0xAB, 0x6B})) {
153 return invalid("databank helpers do not match");
154 }
156 const auto operand_matches = [&](uint32_t pc, uint8_t opcode,
157 uint32_t table) {
158 const auto snes = static_cast<uint16_t>((table & 0x7FFF) | 0x8000);
159 return matches(pc, {opcode, static_cast<uint8_t>(snes),
160 static_cast<uint8_t>(snes >> 8)});
161 };
162 for (const auto& site : kEntranceLoaderSites) {
163 if (!operand_matches(site.pc, site.opcode, layout.table(site.field))) {
164 return invalid("loader table operand does not match");
165 }
166 }
167 for (int i = 1; i < 8; ++i) {
168 if (!operand_matches(0x15A1A + i * 6, 0xB9, 0x78200 + i)) {
169 return invalid("camera boundary operand does not match");
170 }
171 }
172 for (const auto& [pc, table] : std::array<std::pair<uint32_t, uint32_t>, 3>{
173 {{0x15AAA, 0x79A00}, {0x15ABB, 0x79B00}, {0x15ACB, 0x79C00}}}) {
174 if (!operand_matches(pc, 0xBD, table)) {
175 return invalid("secondary loader table operand does not match");
176 }
177 }
178 }
179 for (const auto& [base, width] : layout.tables) {
180 if (base + width * kNumRegularDungeonEntrances > data.size()) {
181 return absl::FailedPreconditionError(
182 "Dungeon entrance tables exceed ROM size");
183 }
184 }
185 return layout;
186}
187
188} // namespace yaze::zelda3
189#endif // YAZE_ZELDA3_DUNGEON_ENTRANCE_LAYOUT_H_
Zelda 3 specific classes and functions.
constexpr std::array< EntranceLoaderSite, 17 > kEntranceLoaderSites
constexpr EntranceLayout kExpandedEntranceLayout
constexpr int kNumRegularDungeonEntrances
constexpr EntranceLayout kLegacyEntranceLayout
std::pair< uint32_t, uint32_t > EntranceTable
absl::StatusOr< EntranceLayout > ResolveEntranceLayout(std::span< const uint8_t > data)
uint32_t table(EntranceField field) const
uint32_t address(EntranceField field, int id) const
std::array< EntranceTable, static_cast< size_t >(EntranceField::kCount)> tables