yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_map_commands.cc
Go to the documentation of this file.
2
3#include <cstdint>
4#include <string>
5#include <vector>
6
7#include "absl/strings/str_format.h"
8#include "cli/util/hex_util.h"
9#include "rom/rom.h"
11#include "zelda3/dungeon/room.h"
13
14namespace yaze {
15namespace cli {
16namespace handlers {
17
19
20namespace {
21
22// ASCII character mapping for room tiles
23constexpr char kCharWall = '#';
24constexpr char kCharFloor = '.';
25constexpr char kCharWater = '~';
26constexpr char kCharPit = 'v';
27constexpr char kCharStair = '>';
28constexpr char kCharDoor = 'D';
29constexpr char kCharChest = 'C';
30constexpr char kCharSpike = 'X';
31constexpr char kCharBlock = 'B';
32constexpr char kCharTorch = 'T';
33constexpr char kCharSwitch = 'S';
34constexpr char kCharTrackH = '-';
35constexpr char kCharTrackV = '|';
36constexpr char kCharTrackX = '+';
37constexpr char kCharStopN = 'N';
38constexpr char kCharStopS = 's';
39constexpr char kCharStopE = 'E';
40constexpr char kCharStopW = 'W';
41constexpr char kCharUnknown = '?';
42
43// Room dimensions
44constexpr int kRoomWidth = 64;
45constexpr int kRoomHeight = 64;
46
47// Classify an object ID to an ASCII character
48char ClassifyObject(uint16_t object_id) {
49 // Common object types from ALTTP (approximate ranges)
50 // These are based on typical object ID patterns
51
52 // Walls and borders (0x000-0x0FF often includes wall/floor variants)
53 if (object_id >= 0x000 && object_id <= 0x00F) {
54 return kCharFloor;
55 }
56
57 // Pits and holes
58 if (object_id == 0x020 || object_id == 0x021 || object_id == 0x022) {
59 return kCharPit;
60 }
61
62 // Water tiles
63 if (object_id >= 0x030 && object_id <= 0x03F) {
64 return kCharWater;
65 }
66 if (object_id >= 0x040 && object_id <= 0x04F) {
67 return kCharWater;
68 }
69
70 // Stairs (0x138, 0x139, 0x13A, 0x13B)
71 if (object_id >= 0x138 && object_id <= 0x13B) {
72 return kCharStair;
73 }
74 if (object_id == 0x12D || object_id == 0x12E) {
75 return kCharStair;
76 }
77
78 // Chests
79 if (object_id >= 0x0F0 && object_id <= 0x0FF) {
80 return kCharChest;
81 }
82
83 // Blocks/pushable (exclude $D0-$D3 switch tracks)
84 if (object_id >= 0x0D4 && object_id <= 0x0DF) {
85 return kCharBlock;
86 }
87
88 // Torches
89 if (object_id >= 0x0E0 && object_id <= 0x0EF) {
90 return kCharTorch;
91 }
92
93 // Spikes
94 if (object_id >= 0x100 && object_id <= 0x10F) {
95 return kCharSpike;
96 }
97
98 // Switches/crystals
99 if (object_id >= 0x110 && object_id <= 0x11F) {
100 return kCharSwitch;
101 }
102
103 // Walls (common high-range objects)
104 if (object_id >= 0x200 && object_id <= 0x2FF) {
105 return kCharWall;
106 }
107
108 return kCharUnknown;
109}
110
111// Map room object to grid position and character
112void PlaceObject(std::vector<std::string>& grid, const zelda3::RoomObject& obj,
113 int layer_filter) {
114 // Skip if wrong layer
115 if (layer_filter >= 0 && static_cast<int>(obj.layer_) != layer_filter) {
116 return;
117 }
118
119 int tile_x = obj.x_;
120 int tile_y = obj.y_;
121 int width = obj.size_ > 0 ? obj.size_ : 1;
122
123 char ch = ClassifyObject(obj.id_);
124
125 // Place the object character(s) on the grid
126 for (int dx = 0; dx < width; ++dx) {
127 int gx = tile_x + dx;
128 int gy = tile_y;
129
130 if (gx >= 0 && gx < kRoomWidth && gy >= 0 && gy < kRoomHeight) {
131 // Don't overwrite more important markers with floor
132 if (ch == kCharFloor && grid[gy][gx] != kCharFloor) {
133 continue;
134 }
135 grid[gy][gx] = ch;
136 }
137 }
138}
139
140// Place doors on the grid
141void PlaceDoors(std::vector<std::string>& grid,
142 const std::vector<zelda3::Room::Door>& doors) {
143 for (const auto& door : doors) {
144 auto [tile_x, tile_y] = door.GetTileCoords();
145
146 // Doors are typically 2-3 tiles wide
147 for (int dx = 0; dx < 2; ++dx) {
148 int gx = tile_x + dx;
149 int gy = tile_y;
150
151 if (gx >= 0 && gx < kRoomWidth && gy >= 0 && gy < kRoomHeight) {
152 grid[gy][gx] = kCharDoor;
153 }
154 }
155 }
156}
157
158// Place stairs on the grid
159// Note: staircase is a C struct from zelda.h with id (x) and room (y) fields
160void PlaceStairs(std::vector<std::string>& grid,
161 const std::vector<staircase>& stairs) {
162 for (const auto& stair : stairs) {
163 // staircase struct: id = x position index, room = y position index
164 // These are tile indices
165 int tile_x = stair.id;
166 int tile_y = stair.room;
167
168 if (tile_x >= 0 && tile_x < kRoomWidth && tile_y >= 0 &&
169 tile_y < kRoomHeight) {
170 grid[tile_y][tile_x] = kCharStair;
171 }
172 }
173}
174
175// Place chests on the grid
176// Note: chest_data is a C struct from zelda.h with id and size fields
177void PlaceChests(std::vector<std::string>& grid,
178 const std::vector<chest_data>& chests) {
179 // Chest positions need to be decoded from ROM format
180 // For now we just mark them if we have location data
181 // (Full chest position decoding would require additional ROM reads)
182 (void)grid; // Suppress unused warning
183 (void)chests; // Suppress unused warning
184}
185
186char ClassifyCustomCollisionTile(uint8_t tile) {
187 // Oracle-of-Secrets minecart tracks/stops are stored in the custom collision
188 // map (ZScream format), not as tile objects.
189 switch (tile) {
190 case 0xB0: // horizontal
191 return kCharTrackH;
192 case 0xB1: // vertical
193 return kCharTrackV;
194 case 0xB7:
195 return kCharStopN;
196 case 0xB8:
197 return kCharStopS;
198 case 0xB9:
199 return kCharStopW;
200 case 0xBA:
201 return kCharStopE;
202 default:
203 break;
204 }
205
206 if ((tile >= 0xB2 && tile <= 0xB6) || (tile >= 0xBB && tile <= 0xBE)) {
207 return kCharTrackX;
208 }
209 if (tile >= 0xD0 && tile <= 0xD3) {
210 return kCharSwitch;
211 }
212 return 0;
213}
214
215void OverlayCustomCollision(std::vector<std::string>& grid, Rom* rom,
216 int room_id) {
217 auto map_or = zelda3::LoadCustomCollisionMap(rom, room_id);
218 if (!map_or.ok() || !map_or.value().has_data) {
219 return;
220 }
221
222 const auto& map = map_or.value().tiles;
223 for (int y = 0; y < kRoomHeight; ++y) {
224 for (int x = 0; x < kRoomWidth; ++x) {
225 uint8_t tile = map[static_cast<size_t>(y * kRoomWidth + x)];
226 char ch = ClassifyCustomCollisionTile(tile);
227 if (ch == 0) {
228 continue;
229 }
230
231 // Overlay tracks/stops on top of floor/unknown. Preserve higher-signal
232 // markers like doors/walls.
233 if (grid[y][x] == kCharFloor || grid[y][x] == kCharUnknown) {
234 grid[y][x] = ch;
235 }
236 }
237 }
238}
239
240} // namespace
241
243 Rom* rom, const resources::ArgumentParser& parser,
244 resources::OutputFormatter& formatter) {
245 auto room_id_str = parser.GetString("room").value();
246 auto layer_opt = parser.GetString("layer");
247
248 int room_id;
249 if (!ParseHexString(room_id_str, &room_id)) {
250 return absl::InvalidArgumentError(
251 "Invalid room ID format. Must be hex (e.g., 0x07).");
252 }
253
254 int layer_filter = -1; // -1 means all layers
255 if (layer_opt.has_value()) {
256 if (!ParseHexString(layer_opt.value(), &layer_filter)) {
257 return absl::InvalidArgumentError(
258 "Invalid layer format. Must be 0, 1, or 2.");
259 }
260 if (layer_filter < 0 || layer_filter > 2) {
261 return absl::InvalidArgumentError("Layer must be 0, 1, or 2.");
262 }
263 }
264
265 // Load room with full objects
266 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
267
268 // Initialize grid with floor tiles
269 std::vector<std::string> grid(kRoomHeight, std::string(kRoomWidth, kCharFloor));
270
271 // Add border walls
272 for (int x = 0; x < kRoomWidth; ++x) {
273 grid[0][x] = kCharWall;
274 grid[kRoomHeight - 1][x] = kCharWall;
275 }
276 for (int y = 0; y < kRoomHeight; ++y) {
277 grid[y][0] = kCharWall;
278 grid[y][kRoomWidth - 1] = kCharWall;
279 }
280
281 // Place objects on the grid
282 const auto& objects = room.GetTileObjects();
283 for (const auto& obj : objects) {
284 PlaceObject(grid, obj, layer_filter);
285 }
286
287 // Place doors
288 PlaceDoors(grid, room.GetDoors());
289
290 // Place stairs
291 PlaceStairs(grid, room.GetStairs());
292
293 // Place chests (if position data available)
294 PlaceChests(grid, room.GetChests());
295
296 // Overlay Oracle custom collision tiles (minecart tracks/stops/switches).
297 OverlayCustomCollision(grid, rom, room_id);
298
299 // Output
300 formatter.BeginObject("dungeon_map");
301 formatter.AddField("room_id", absl::StrFormat("0x%02X", room_id));
302 formatter.AddField("room_name", std::string(zelda3::kRoomNames[room_id]));
303 formatter.AddField("width", kRoomWidth);
304 formatter.AddField("height", kRoomHeight);
305 formatter.AddField("layer_filter",
306 layer_filter >= 0 ? absl::StrFormat("%d", layer_filter)
307 : "all");
308 formatter.AddField("object_count", static_cast<int>(objects.size()));
309 formatter.AddField("door_count", static_cast<int>(room.GetDoors().size()));
310
311 // Legend
312 formatter.BeginObject("legend");
313 formatter.AddField("#", "wall");
314 formatter.AddField(".", "floor");
315 formatter.AddField("~", "water");
316 formatter.AddField("v", "pit");
317 formatter.AddField(">", "stair");
318 formatter.AddField("D", "door");
319 formatter.AddField("C", "chest");
320 formatter.AddField("X", "spike");
321 formatter.AddField("B", "block");
322 formatter.AddField("T", "torch");
323 formatter.AddField("S", "switch");
324 formatter.AddField("-", "track_h");
325 formatter.AddField("|", "track_v");
326 formatter.AddField("+", "track_x");
327 formatter.AddField("N/s/E/W", "stop_tiles");
328 formatter.EndObject();
329
330 // ASCII map as array of strings
331 formatter.BeginArray("map");
332 // Header row with column numbers
333 std::string header = " ";
334 for (int x = 0; x < kRoomWidth; x += 10) {
335 header += absl::StrFormat("%-10d", x);
336 }
337 formatter.AddArrayItem(header);
338
339 // Grid rows with row numbers
340 for (int y = 0; y < kRoomHeight; ++y) {
341 std::string row = absl::StrFormat("%2d %s", y, grid[y]);
342 formatter.AddArrayItem(row);
343 }
344 formatter.EndArray();
345
346 formatter.EndObject();
347
348 return absl::OkStatus();
349}
350
351} // namespace handlers
352} // namespace cli
353} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
const std::vector< chest_data > & GetChests() const
Definition room.h:218
const std::vector< Door > & GetDoors() const
Definition room.h:294
const std::vector< staircase > & GetStairs() const
Definition room.h:222
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:314
void PlaceObject(std::vector< std::string > &grid, const zelda3::RoomObject &obj, int layer_filter)
void PlaceDoors(std::vector< std::string > &grid, const std::vector< zelda3::Room::Door > &doors)
void PlaceStairs(std::vector< std::string > &grid, const std::vector< staircase > &stairs)
void OverlayCustomCollision(std::vector< std::string > &grid, Rom *rom, int room_id)
void PlaceChests(std::vector< std::string > &grid, const std::vector< chest_data > &chests)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:253
constexpr std::array< std::string_view, 297 > kRoomNames
Definition room.h:791