yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_map.cc
Go to the documentation of this file.
1#include "dungeon_map.h"
2
3#include <algorithm>
4#include <fstream>
5#include <vector>
6
7#include "absl/strings/str_format.h"
13#include "app/platform/window.h"
14#include "rom/snes.h"
15#include "rom/transaction.h"
16#include "util/file_util.h"
17#include "util/hex.h"
18#include "zelda3/game_data.h"
19
20namespace yaze::zelda3 {
21
22absl::StatusOr<std::vector<DungeonMap>> LoadDungeonMaps(
23 Rom& rom, DungeonMapLabels& dungeon_map_labels) {
24 std::vector<DungeonMap> dungeon_maps;
25 std::vector<std::array<uint8_t, kNumRooms>> current_floor_rooms_d;
26 std::vector<std::array<uint8_t, kNumRooms>> current_floor_gfx_d;
27 int total_floors_d;
28 uint8_t nbr_floor_d;
29 uint8_t nbr_basement_d;
30
31 for (int d = 0; d < kNumDungeons; d++) {
32 current_floor_rooms_d.clear();
33 current_floor_gfx_d.clear();
34 ASSIGN_OR_RETURN(int ptr, rom.ReadWord(kDungeonMapRoomsPtr + (d * 2)));
35 ASSIGN_OR_RETURN(int ptr_gfx, rom.ReadWord(kDungeonMapGfxPtr + (d * 2)));
36 ptr |= 0x0A0000; // Add bank to the short ptr
37 ptr_gfx |= 0x0A0000; // Add bank to the short ptr
38 int pc_ptr = SnesToPc(ptr); // Contains data for the next 25 rooms
39 int pc_ptr_gfx = SnesToPc(ptr_gfx); // Contains data for the next 25 rooms
40
41 ASSIGN_OR_RETURN(uint16_t boss_room_d,
42 rom.ReadWord(kDungeonMapBossRooms + (d * 2)));
43
44 ASSIGN_OR_RETURN(nbr_basement_d, rom.ReadByte(kDungeonMapFloors + (d * 2)));
45 nbr_basement_d &= 0x0F;
46
47 ASSIGN_OR_RETURN(nbr_floor_d, rom.ReadByte(kDungeonMapFloors + (d * 2)));
48 nbr_floor_d &= 0xF0;
49 nbr_floor_d = nbr_floor_d >> 4;
50
51 total_floors_d = nbr_basement_d + nbr_floor_d;
52
53 // for each floor in the dungeon
54 for (int i = 0; i < total_floors_d; i++) {
55 dungeon_map_labels[d].emplace_back();
56
57 std::array<uint8_t, kNumRooms> rdata;
58 std::array<uint8_t, kNumRooms> gdata;
59
60 // for each room on the floor
61 for (int j = 0; j < kNumRooms; j++) {
62 gdata[j] = 0xFF;
63 rdata[j] = rom.data()[pc_ptr + j + (i * kNumRooms)]; // Set the rooms
64
65 gdata[j] = rdata[j] == 0x0F ? 0xFF : rom.data()[pc_ptr_gfx++];
66
67 std::string label = util::HexByte(rdata[j]);
68 dungeon_map_labels[d][i][j] = label;
69 }
70
71 current_floor_gfx_d.push_back(gdata); // Add new floor gfx data
72 current_floor_rooms_d.push_back(rdata); // Add new floor data
73 }
74
75 dungeon_maps.emplace_back(boss_room_d, nbr_floor_d, nbr_basement_d,
76 current_floor_rooms_d, current_floor_gfx_d);
77 }
78
79 return dungeon_maps;
80}
81
82absl::StatusOr<DungeonMapWritePlan> BuildDungeonMapWritePlan(
83 const std::vector<DungeonMap>& dungeon_maps, size_t rom_size,
84 bool relocated_tile16) {
85 // Repacking a prefix would overwrite streams owned by omitted maps.
86 if (dungeon_maps.size() != kNumDungeons) {
87 return absl::InvalidArgumentError(
88 "A full set of 14 dungeon maps is required");
89 }
90 std::array<uint16_t, kNumDungeons> boss_floors;
91 boss_floors.fill(0xFFFF);
92 for (size_t d = 0; d < dungeon_maps.size(); ++d) {
93 const auto& map = dungeon_maps[d];
94 const size_t count = map.nbr_of_floor + map.nbr_of_basement;
95 if (map.nbr_of_floor > 0x0F || map.nbr_of_basement > 0x0F ||
96 map.floor_rooms.size() != count || map.floor_gfx.size() != count) {
97 return absl::InvalidArgumentError(
98 absl::StrFormat("Invalid floor dimensions for dungeon %d", d));
99 }
100 if (map.boss_room == 0x000F || map.boss_room == 0xFFFF)
101 continue;
102 if (map.boss_room > 0xFF) {
103 return absl::InvalidArgumentError(
104 absl::StrFormat("Boss room cannot fit a map cell for dungeon %d", d));
105 }
106 for (size_t f = 0; f < count; ++f) {
107 const auto& rooms = map.floor_rooms[f];
108 if (std::find(rooms.begin(), rooms.end(), map.boss_room) != rooms.end()) {
109 // The game uses a signed LOW byte, but FF in the HIGH byte means no
110 // boss. In particular B1 must be 00FF, never sign-extended to FFFF.
111 boss_floors[d] =
112 static_cast<uint8_t>(static_cast<int>(f) - map.nbr_of_basement);
113 break; // Preserve the existing first-occurrence selection.
114 }
115 }
116 if (boss_floors[d] == 0xFFFF) {
117 return absl::NotFoundError(
118 absl::StrFormat("Boss room not found for dungeon %d", d));
119 }
120 }
121
123 // The room pointer/floor block AND the GFX pointer block are fixed owners.
124 // A stream must fit contiguously in one remaining interval; loaders cannot
125 // skip a table in the middle of a room or graphics stream.
126 constexpr std::array<std::pair<int, int>, 3> regions = {{
130 }};
131 int cursor =
132 relocated_tile16 ? kDungeonMapDataStart : kDungeonMapDataReservedEnd + 1;
133 auto allocate = [&](std::vector<uint8_t> bytes) -> absl::StatusOr<uint16_t> {
134 // Empty streams have a valid bank0A pointer but consume no storage.
135 int start = relocated_tile16 ? kDungeonMapDataStart
137 if (!bytes.empty()) {
138 bool found = false;
139 for (const auto& [begin, end] : regions) {
140 start = std::max(cursor, begin);
141 if (start < end && bytes.size() <= static_cast<size_t>(end - start)) {
142 found = true;
143 break;
144 }
145 }
146 if (!found) {
147 return absl::OutOfRangeError(
148 "Dungeon map streams exceed safe capacity");
149 }
150 }
151 const int snes = PcToSnes(start);
152 const int last =
153 start + static_cast<int>(bytes.empty() ? 0 : bytes.size() - 1);
154 if ((snes >> 16) != 0x0A || (snes & 0xFFFF) < 0x8000 ||
155 (PcToSnes(last) >> 16) != 0x0A ||
156 static_cast<size_t>(last) >= rom_size) {
157 return absl::OutOfRangeError(
158 "Dungeon map stream violates ROM/bank bounds");
159 }
160 if (!bytes.empty()) {
161 cursor = start + bytes.size();
162 plan.writes.push_back({start, std::move(bytes)});
163 }
164 return static_cast<uint16_t>(snes);
165 };
166 auto word = [&](int address, uint16_t value) {
167 plan.writes.push_back(
168 {address,
169 {static_cast<uint8_t>(value), static_cast<uint8_t>(value >> 8)}});
170 };
171 std::array<std::vector<uint8_t>, kNumDungeons> room_streams, gfx_streams;
172 std::array<uint16_t, kNumDungeons> room_pointers, gfx_pointers;
173 for (int d = 0; d < kNumDungeons; ++d) {
174 const auto& map = dungeon_maps[d];
175 for (size_t f = 0; f < map.floor_rooms.size(); ++f) {
176 for (int r = 0; r < kNumRooms; ++r) {
177 room_streams[d].push_back(map.floor_rooms[f][r]);
178 if (map.floor_rooms[f][r] != 0x0F)
179 gfx_streams[d].push_back(map.floor_gfx[f][r]);
180 }
181 }
182 }
183 // Vanilla fills the room-data interval before its separate graphics interval.
184 // Keep that ordering so interleaving cannot fragment an otherwise valid fit.
185 for (int d = 0; d < kNumDungeons; ++d) {
186 ASSIGN_OR_RETURN(room_pointers[d], allocate(std::move(room_streams[d])));
187 }
188 for (int d = 0; d < kNumDungeons; ++d) {
189 ASSIGN_OR_RETURN(gfx_pointers[d], allocate(std::move(gfx_streams[d])));
190 }
191 for (int d = 0; d < kNumDungeons; ++d) {
192 const auto& map = dungeon_maps[d];
193 word(kDungeonMapRoomsPtr + 2 * d, room_pointers[d]);
194 word(kDungeonMapGfxPtr + 2 * d, gfx_pointers[d]);
195 word(kDungeonMapFloors + 2 * d,
196 (map.nbr_of_floor << 4) | map.nbr_of_basement);
197 word(kDungeonMapBossRooms + 2 * d, map.boss_room);
198 word(kDungeonMapBossFloors + 2 * d, boss_floors[d]);
199 }
200 // Validate the complete output, including fixed tables, without touching ROM.
201 std::vector<std::pair<int, int>> extents;
202 for (const auto& write : plan.writes) {
203 const int end = write.pc_address + write.data.size();
204 if (write.pc_address < 0 || static_cast<size_t>(end) > rom_size ||
205 (PcToSnes(write.pc_address) >> 16) != 0x0A ||
206 (PcToSnes(end - 1) >> 16) != 0x0A) {
207 return absl::OutOfRangeError(
208 "Dungeon map write violates ROM/bank bounds");
209 }
210 extents.emplace_back(write.pc_address, end);
211 }
212 std::sort(extents.begin(), extents.end());
213 for (size_t i = 1; i < extents.size(); ++i) {
214 if (extents[i].first < extents[i - 1].second)
215 return absl::InternalError("Overlapping dungeon map write plan");
216 }
217 return plan;
218}
219
220absl::StatusOr<DungeonMapWritePlan> BuildDungeonMapWritePlan(
221 Rom& rom, const std::vector<DungeonMap>& dungeon_maps) {
223 if (marker != 0xB9 && marker != 0x20) {
224 return absl::FailedPreconditionError(
225 "Unsupported dungeon map Tile16 layout");
226 }
227 const bool relocated_tile16 = marker == 0x20;
228 const int tile16 = GetDungeonMapTile16Expanded();
229 const int64_t tile16_end =
230 static_cast<int64_t>(tile16) + kNumDungeonMapTile16 * 8;
231 if (relocated_tile16) {
232 if (tile16 < 0 || tile16_end > rom.size() ||
233 (tile16 < kDungeonMapDataLimit && tile16_end > kDungeonMapDataStart)) {
234 return absl::FailedPreconditionError(
235 "Invalid relocated dungeon map Tile16 extent");
236 }
237 }
238 ASSIGN_OR_RETURN(auto plan, BuildDungeonMapWritePlan(dungeon_maps, rom.size(),
239 relocated_tile16));
240 if (relocated_tile16) {
241 for (const auto& write : plan.writes) {
242 const int64_t write_end = static_cast<int64_t>(write.pc_address) +
243 static_cast<int64_t>(write.data.size());
244 if ((write.pc_address < tile16_end) && (write_end > tile16)) {
245 return absl::FailedPreconditionError(
246 "Dungeon map write overlaps relocated Tile16 artwork");
247 }
248 }
249 }
250 return plan;
251}
252
253absl::Status SaveDungeonMaps(Rom& rom, std::vector<DungeonMap>& dungeon_maps) {
254 ASSIGN_OR_RETURN(auto plan, BuildDungeonMapWritePlan(rom, dungeon_maps));
255 ScopedRomTransaction transaction(rom);
256 for (const auto& write : plan.writes) {
257 RETURN_IF_ERROR(rom.WriteVector(write.pc_address, write.data));
258 }
259 transaction.Commit();
260 return absl::OkStatus();
261}
262
263absl::Status LoadDungeonMapTile16(gfx::Tilemap& tile16_blockset, Rom& rom,
264 GameData* game_data,
265 const std::vector<uint8_t>& gfx_data,
266 bool bin_mode) {
267 tile16_blockset.tile_size = {16, 16};
268 tile16_blockset.map_size = {186, 186};
269 tile16_blockset.atlas.Create(256, 192, 8,
270 std::vector<uint8_t>(256 * 192, 0x00));
271
272 for (int i = 0; i < kNumDungeonMapTile16; i++) {
273 int addr = kDungeonMapTile16;
274 if (rom.data()[kDungeonMapExpCheck] != 0xB9) {
276 }
277
278 ASSIGN_OR_RETURN(auto tl, rom.ReadWord(addr + (i * 8)));
279 gfx::TileInfo t1 = gfx::WordToTileInfo(tl); // Top left
280
281 ASSIGN_OR_RETURN(auto tr, rom.ReadWord(addr + 2 + (i * 8)));
282 gfx::TileInfo t2 = gfx::WordToTileInfo(tr); // Top right
283
284 ASSIGN_OR_RETURN(auto bl, rom.ReadWord(addr + 4 + (i * 8)));
285 gfx::TileInfo t3 = gfx::WordToTileInfo(bl); // Bottom left
286
287 ASSIGN_OR_RETURN(auto br, rom.ReadWord(addr + 6 + (i * 8)));
288 gfx::TileInfo t4 = gfx::WordToTileInfo(br); // Bottom right
289
290 int sheet_offset = 212;
291 if (bin_mode) {
292 sheet_offset = 0;
293 }
294 ComposeTile16(tile16_blockset, gfx_data, t1, t2, t3, t4, sheet_offset);
295 }
296
297 if (game_data) {
298 tile16_blockset.atlas.SetPalette(
300 }
301
302 // Queue texture creation via Arena's deferred system
304 &tile16_blockset.atlas);
305
306 return absl::OkStatus();
307}
308
309absl::Status SaveDungeonMapTile16(gfx::Tilemap& tile16_blockset, Rom& rom) {
310 for (int i = 0; i < kNumDungeonMapTile16; i++) {
311 int addr = kDungeonMapTile16;
312 if (rom.data()[kDungeonMapExpCheck] != 0xB9) {
314 }
315
316 gfx::TileInfo t1 = tile16_blockset.tile_info[i][0];
317 gfx::TileInfo t2 = tile16_blockset.tile_info[i][1];
318 gfx::TileInfo t3 = tile16_blockset.tile_info[i][2];
319 gfx::TileInfo t4 = tile16_blockset.tile_info[i][3];
320
321 auto tl = gfx::TileInfoToWord(t1);
322 RETURN_IF_ERROR(rom.WriteWord(addr + (i * 8), tl));
323
324 auto tr = gfx::TileInfoToWord(t2);
325 RETURN_IF_ERROR(rom.WriteWord(addr + 2 + (i * 8), tr));
326
327 auto bl = gfx::TileInfoToWord(t3);
328 RETURN_IF_ERROR(rom.WriteWord(addr + 4 + (i * 8), bl));
329
330 auto br = gfx::TileInfoToWord(t4);
331 RETURN_IF_ERROR(rom.WriteWord(addr + 6 + (i * 8), br));
332 }
333 return absl::OkStatus();
334}
335
336absl::Status LoadDungeonMapGfxFromBinary(Rom& rom, GameData* game_data,
337 gfx::Tilemap& tile16_blockset,
338 std::array<gfx::Bitmap, 4>& sheets,
339 std::vector<uint8_t>& gfx_bin_data) {
340 std::string bin_file = util::FileDialogWrapper::ShowOpenFileDialog();
341 if (bin_file.empty()) {
342 return absl::InternalError("No file selected");
343 }
344
345 std::ifstream file(bin_file, std::ios::binary);
346 if (!file.is_open()) {
347 return absl::InternalError("Failed to open file");
348 }
349
350 // Read the gfx data into a buffer
351 std::vector<uint8_t> bin_data((std::istreambuf_iterator<char>(file)),
352 std::istreambuf_iterator<char>());
353 auto converted_bin = gfx::SnesTo8bppSheet(bin_data, 4, 4);
354 gfx_bin_data = converted_bin;
355 if (LoadDungeonMapTile16(tile16_blockset, rom, game_data, converted_bin, true)
356 .ok()) {
357 std::vector<std::vector<uint8_t>> gfx_sheets;
358 for (int i = 0; i < 4; i++) {
359 gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000),
360 converted_bin.begin() + ((i + 1) * 0x1000));
361 sheets[i] = gfx::Bitmap(128, 32, 8, gfx_sheets[i]);
362 if (game_data) {
363 sheets[i].SetPalette(
365 }
366
367 // Queue texture creation via Arena's deferred system
370 }
371 }
372 file.close();
373
374 return absl::OkStatus();
375}
376
377} // 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
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:785
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:928
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:793
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:873
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
std::vector< uint8_t > SnesTo8bppSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:133
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:379
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:33
Zelda 3 specific classes and functions.
absl::Status LoadDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom, GameData *game_data, const std::vector< uint8_t > &gfx_data, bool bin_mode)
Load the dungeon map tile16 from the ROM.
constexpr int kDungeonMapDataStart
Definition dungeon_map.h:23
int GetDungeonMapTile16Expanded()
Definition dungeon_map.h:33
constexpr int kDungeonMapExpCheck
Definition dungeon_map.h:29
constexpr int kDungeonMapBossFloors
Definition dungeon_map.h:41
constexpr int kDungeonMapDataReservedEnd
Definition dungeon_map.h:25
absl::StatusOr< DungeonMapWritePlan > BuildDungeonMapWritePlan(const std::vector< DungeonMap > &dungeon_maps, size_t rom_size, bool relocated_tile16)
constexpr int kDungeonMapFloors
Definition dungeon_map.h:18
constexpr int kDungeonMapTile16
Definition dungeon_map.h:30
constexpr int kNumRooms
Definition dungeon_map.h:48
absl::Status SaveDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom)
Save the dungeon map tile16 to the ROM.
constexpr int kDungeonMapDataReservedStart
Definition dungeon_map.h:24
absl::Status LoadDungeonMapGfxFromBinary(Rom &rom, GameData *game_data, gfx::Tilemap &tile16_blockset, std::array< gfx::Bitmap, 4 > &sheets, std::vector< uint8_t > &gfx_bin_data)
Load the dungeon map gfx from binary.
constexpr int kDungeonMapDataLimit
Definition dungeon_map.h:26
constexpr int kDungeonMapBossRooms
Definition dungeon_map.h:40
constexpr int kDungeonMapRoomsPtr
Definition dungeon_map.h:17
constexpr int kNumDungeonMapTile16
Definition dungeon_map.h:49
constexpr int kNumDungeons
Definition dungeon_map.h:47
std::array< std::vector< std::array< std::string, kNumRooms > >, kNumDungeons > DungeonMapLabels
Definition dungeon_map.h:72
absl::StatusOr< std::vector< DungeonMap > > LoadDungeonMaps(Rom &rom, DungeonMapLabels &dungeon_map_labels)
Load the dungeon maps from the ROM.
constexpr int kDungeonMapGfxPtr
Definition dungeon_map.h:20
absl::Status SaveDungeonMaps(Rom &rom, std::vector< DungeonMap > &dungeon_maps)
Save the dungeon maps to the ROM.
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Tilemap structure for SNES tile-based graphics management.
Definition tilemap.h:118
Pair tile_size
Size of individual tiles (8x8 or 16x16)
Definition tilemap.h:123
Pair map_size
Size of tilemap in tiles.
Definition tilemap.h:124
Bitmap atlas
Master bitmap containing all tiles.
Definition tilemap.h:119
std::vector< std::array< gfx::TileInfo, 4 > > tile_info
Tile metadata (4 tiles per 16x16)
Definition tilemap.h:122
std::vector< DungeonMapWrite > writes
Definition dungeon_map.h:92
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95