yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_graph_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <ctime>
6#include <iomanip>
7#include <map>
8#include <set>
9#include <sstream>
10#include <string>
11#include <vector>
12
13#include "absl/status/status.h"
14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
17#include "util/macro.h"
18#include "zelda3/common.h"
23
24namespace yaze {
25namespace cli {
26namespace handlers {
27
28namespace {
29
30constexpr int kDarkWorldOffset = 0x40;
31constexpr int kSpecialWorldOffset = 0x80;
32
33// Screen dimensions in pixels
34constexpr int kScreenWidth = 512;
35constexpr int kScreenHeight = 512;
36
37// Maps per row in each world
38constexpr int kMapsPerRow = 8;
39
40std::string GetWorldName(int world) {
41 switch (world) {
42 case 0: return "light";
43 case 1: return "dark";
44 case 2: return "special";
45 default: return "unknown";
46 }
47}
48
50 switch (size) {
51 case zelda3::AreaSizeEnum::SmallArea: return "small";
52 case zelda3::AreaSizeEnum::LargeArea: return "large";
53 case zelda3::AreaSizeEnum::WideArea: return "wide";
54 case zelda3::AreaSizeEnum::TallArea: return "tall";
55 default: return "small";
56 }
57}
58
59int WorldFromMapId(int map_id) {
60 if (map_id >= kSpecialWorldOffset) return 2;
61 if (map_id >= kDarkWorldOffset) return 1;
62 return 0;
63}
64
65int LocalIndexFromMapId(int map_id) {
66 int world = WorldFromMapId(map_id);
67 switch (world) {
68 case 0: return map_id;
69 case 1: return map_id - kDarkWorldOffset;
70 case 2: return map_id - kSpecialWorldOffset;
71 default: return map_id;
72 }
73}
74
75std::string GetDefaultAreaName(int map_id) {
76 int world = WorldFromMapId(map_id);
77 int local = LocalIndexFromMapId(map_id);
78 int grid_x = local % kMapsPerRow;
79 int grid_y = local / kMapsPerRow;
80
81 std::string world_name;
82 switch (world) {
83 case 0: world_name = "Light World"; break;
84 case 1: world_name = "Dark World"; break;
85 case 2: world_name = "Special Area"; break;
86 default: world_name = "Unknown"; break;
87 }
88
89 return absl::StrFormat("%s (%d,%d)", world_name, grid_x, grid_y);
90}
91
92std::string GetCurrentTimestamp() {
93 auto now = std::chrono::system_clock::now();
94 auto time = std::chrono::system_clock::to_time_t(now);
95 std::stringstream ss;
96 ss << std::put_time(std::gmtime(&time), "%Y-%m-%dT%H:%M:%SZ");
97 return ss.str();
98}
99
100} // namespace
101
103 Rom* rom, const resources::ArgumentParser& parser,
104 resources::OutputFormatter& formatter) {
105
106 // Parse world filter
107 std::optional<int> world_filter;
108 if (auto world_str = parser.GetString("world")) {
109 std::string w = *world_str;
110 if (w == "light" || w == "0") {
111 world_filter = 0;
112 } else if (w == "dark" || w == "1") {
113 world_filter = 1;
114 } else if (w == "special" || w == "2") {
115 world_filter = 2;
116 } else if (w != "all") {
117 return absl::InvalidArgumentError(
118 absl::StrCat("Invalid world: ", w,
119 ". Use: light, dark, special, or all"));
120 }
121 }
122
123 // Load overworld data
124 zelda3::Overworld overworld(rom);
125 RETURN_IF_ERROR(overworld.Load(rom));
126
127 // Collect area information
128 std::vector<AreaInfo> areas;
129 std::map<int, int> map_to_parent; // Track parent relationships
130
131 for (int map_id = 0; map_id < zelda3::kNumOverworldMaps; ++map_id) {
132 int world = WorldFromMapId(map_id);
133
134 // Apply world filter
135 if (world_filter.has_value() && world != *world_filter) {
136 continue;
137 }
138
139 const auto* map = overworld.overworld_map(map_id);
140 if (map == nullptr) continue;
141
142 int local = LocalIndexFromMapId(map_id);
143 int grid_x = local % kMapsPerRow;
144 int grid_y = local / kMapsPerRow;
145
146 // Calculate pixel bounds
147 int min_x = grid_x * kScreenWidth;
148 int min_y = grid_y * kScreenHeight;
149 int max_x = min_x + kScreenWidth;
150 int max_y = min_y + kScreenHeight;
151
152 // Adjust for large/wide/tall maps
153 auto area_size = map->area_size();
154 if (area_size == zelda3::AreaSizeEnum::LargeArea) {
155 max_x = min_x + kScreenWidth * 2;
156 max_y = min_y + kScreenHeight * 2;
157 } else if (area_size == zelda3::AreaSizeEnum::WideArea) {
158 max_x = min_x + kScreenWidth * 2;
159 } else if (area_size == zelda3::AreaSizeEnum::TallArea) {
160 max_y = min_y + kScreenHeight * 2;
161 }
162
163 int parent = map->parent();
164 map_to_parent[map_id] = parent;
165
166 AreaInfo area;
167 area.id = map_id;
168 area.name = GetDefaultAreaName(map_id);
169 area.world = world;
170 area.grid_x = grid_x;
171 area.grid_y = grid_y;
172 area.size = GetAreaSizeName(area_size);
173 area.parent_id = parent == map_id ? -1 : parent; // -1 if no parent
174 area.min_x = min_x;
175 area.min_y = min_y;
176 area.max_x = max_x;
177 area.max_y = max_y;
178
179 areas.push_back(area);
180 }
181
182 // Extract connections from transition tables
183 std::vector<AreaConnection> connections;
184 std::set<std::pair<int, int>> seen_connections;
185
186 // Read north transition targets
187 for (int map_id = 0; map_id < zelda3::kNumOverworldMaps; ++map_id) {
188 int world = WorldFromMapId(map_id);
189 if (world_filter.has_value() && world != *world_filter) continue;
190
191 // North transitions
192 ASSIGN_OR_RETURN(auto north_target,
193 rom->ReadWord(zelda3::kTransitionTargetNorth + map_id * 2));
194 int north_area = north_target & 0xFF;
195
196 // Only add if target is different and valid
197 if (north_area != map_id && north_area < zelda3::kNumOverworldMaps) {
198 int target_world = WorldFromMapId(north_area);
199 // Only connect within same world (unless special crossing)
200 if (target_world == world || world == 2 || target_world == 2) {
201 auto key = std::minmax(map_id, north_area);
202 if (seen_connections.find(key) == seen_connections.end()) {
203 seen_connections.insert(key);
204
205 AreaConnection conn;
206 conn.from_area = map_id;
207 conn.to_area = north_area;
208 conn.direction = "north";
209
210 // Calculate edge position
211 int local = LocalIndexFromMapId(map_id);
212 int grid_x = local % kMapsPerRow;
213 int grid_y = local / kMapsPerRow;
214 conn.edge_x = grid_x * kScreenWidth + kScreenWidth / 2;
215 conn.edge_y = grid_y * kScreenHeight;
216 conn.bidirectional = true;
217
218 connections.push_back(conn);
219 }
220 }
221 }
222
223 // West transitions
224 ASSIGN_OR_RETURN(auto west_target,
225 rom->ReadWord(zelda3::kTransitionTargetWest + map_id * 2));
226 int west_area = west_target & 0xFF;
227
228 if (west_area != map_id && west_area < zelda3::kNumOverworldMaps) {
229 int target_world = WorldFromMapId(west_area);
230 if (target_world == world || world == 2 || target_world == 2) {
231 auto key = std::minmax(map_id, west_area);
232 if (seen_connections.find(key) == seen_connections.end()) {
233 seen_connections.insert(key);
234
235 AreaConnection conn;
236 conn.from_area = map_id;
237 conn.to_area = west_area;
238 conn.direction = "west";
239
240 int local = LocalIndexFromMapId(map_id);
241 int grid_x = local % kMapsPerRow;
242 int grid_y = local / kMapsPerRow;
243 conn.edge_x = grid_x * kScreenWidth;
244 conn.edge_y = grid_y * kScreenHeight + kScreenHeight / 2;
245 conn.bidirectional = true;
246
247 connections.push_back(conn);
248 }
249 }
250 }
251 }
252
253 // Also add implicit connections for adjacent areas
254 for (int map_id = 0; map_id < zelda3::kNumOverworldMaps; ++map_id) {
255 int world = WorldFromMapId(map_id);
256 if (world_filter.has_value() && world != *world_filter) continue;
257
258 int local = LocalIndexFromMapId(map_id);
259 int grid_x = local % kMapsPerRow;
260 int grid_y = local / kMapsPerRow;
261
262 // Get world offset for neighbor calculation
263 int world_offset = (world == 0) ? 0 : (world == 1) ? kDarkWorldOffset : kSpecialWorldOffset;
264 int maps_in_world = (world == 2) ? 32 : 64;
265
266 // East neighbor
267 if (grid_x < kMapsPerRow - 1) {
268 int east_local = local + 1;
269 if (east_local < maps_in_world) {
270 int east_area = world_offset + east_local;
271 auto key = std::minmax(map_id, east_area);
272 if (seen_connections.find(key) == seen_connections.end()) {
273 seen_connections.insert(key);
274
275 AreaConnection conn;
276 conn.from_area = map_id;
277 conn.to_area = east_area;
278 conn.direction = "east";
279 conn.edge_x = (grid_x + 1) * kScreenWidth;
280 conn.edge_y = grid_y * kScreenHeight + kScreenHeight / 2;
281 conn.bidirectional = true;
282
283 connections.push_back(conn);
284 }
285 }
286 }
287
288 // South neighbor
289 int rows_in_world = maps_in_world / kMapsPerRow;
290 if (grid_y < rows_in_world - 1) {
291 int south_local = local + kMapsPerRow;
292 if (south_local < maps_in_world) {
293 int south_area = world_offset + south_local;
294 auto key = std::minmax(map_id, south_area);
295 if (seen_connections.find(key) == seen_connections.end()) {
296 seen_connections.insert(key);
297
298 AreaConnection conn;
299 conn.from_area = map_id;
300 conn.to_area = south_area;
301 conn.direction = "south";
302 conn.edge_x = grid_x * kScreenWidth + kScreenWidth / 2;
303 conn.edge_y = (grid_y + 1) * kScreenHeight;
304 conn.bidirectional = true;
305
306 connections.push_back(conn);
307 }
308 }
309 }
310 }
311
312 // Collect entrance information
313 std::vector<EntranceInfo> entrances;
314
315 const auto& ow_entrances = overworld.entrances();
316 for (size_t i = 0; i < ow_entrances.size(); ++i) {
317 const auto& entrance = ow_entrances[i];
318 if (entrance.deleted) continue;
319
320 int area_id = entrance.map_id_ & 0xFF;
321 int world = WorldFromMapId(area_id);
322
323 if (world_filter.has_value() && world != *world_filter) continue;
324
325 EntranceInfo info;
326 info.id = static_cast<int>(i);
327 info.area_id = area_id;
328 info.position_x = entrance.x_;
329 info.position_y = entrance.y_;
330 info.is_hole = entrance.is_hole_;
331
332 // Get entrance name from labels
333 constexpr size_t kNumEntranceLabels =
335 if (entrance.entrance_id_ < kNumEntranceLabels) {
336 info.name = zelda3::kEntranceNames[entrance.entrance_id_];
337 } else {
338 info.name = absl::StrFormat("Entrance %d", entrance.entrance_id_);
339 }
340
341 // Read destination room ID from ROM
342 // kEntranceRoom is 2 bytes per entrance
343 uint8_t entrance_id = entrance.entrance_id_;
344 ASSIGN_OR_RETURN(auto room_id,
345 rom->ReadWord(zelda3::kEntranceRoom + entrance_id * 2));
346 info.dest_room_id = room_id;
347
348 entrances.push_back(info);
349 }
350
351 // Also add holes
352 const auto& holes = overworld.holes();
353 for (size_t i = 0; i < holes.size(); ++i) {
354 const auto& hole = holes[i];
355
356 int area_id = hole.map_id_ & 0xFF;
357 int world = WorldFromMapId(area_id);
358
359 if (world_filter.has_value() && world != *world_filter) continue;
360
361 EntranceInfo info;
362 info.id = static_cast<int>(ow_entrances.size() + i);
363 info.area_id = area_id;
364 info.position_x = hole.x_;
365 info.position_y = hole.y_;
366 info.is_hole = true;
367
368 constexpr size_t kNumEntranceLabels =
370 if (hole.entrance_id_ < kNumEntranceLabels) {
371 info.name = absl::StrFormat("%s (Hole)",
372 zelda3::kEntranceNames[hole.entrance_id_]);
373 } else {
374 info.name = absl::StrFormat("Hole %d", hole.entrance_id_);
375 }
376
377 uint8_t entrance_id = hole.entrance_id_;
378 ASSIGN_OR_RETURN(auto room_id,
379 rom->ReadWord(zelda3::kEntranceRoom + entrance_id * 2));
380 info.dest_room_id = room_id;
381
382 entrances.push_back(info);
383 }
384
385 // Collect exit information
386 std::vector<ExitInfo> exits;
387 const auto* ow_exits = overworld.exits();
388
389 if (ow_exits != nullptr) {
390 for (const auto& exit : *ow_exits) {
391 int area_id = exit.map_id_ & 0xFF;
392 int world = WorldFromMapId(area_id);
393
394 if (world_filter.has_value() && world != *world_filter) continue;
395
396 ExitInfo info;
397 info.room_id = exit.room_id_;
398 info.name = absl::StrFormat("Exit from room 0x%02X", exit.room_id_);
399 info.return_area_id = area_id;
400 info.return_x = exit.x_;
401 info.return_y = exit.y_;
402
403 exits.push_back(info);
404 }
405 }
406
407 // Output the graph
408 formatter.BeginObject("world_graph");
409
410 formatter.AddField("version", "1.0.0");
411 formatter.AddField("generated", GetCurrentTimestamp());
412 formatter.AddField("rom_name", rom->title());
413
414 // Areas array
415 formatter.BeginArray("areas");
416 for (const auto& area : areas) {
417 formatter.BeginObject();
418 formatter.AddField("id", area.id);
419 formatter.AddField("name", area.name);
420 formatter.AddField("world", GetWorldName(area.world));
421
422 formatter.BeginObject("grid");
423 formatter.AddField("x", area.grid_x);
424 formatter.AddField("y", area.grid_y);
425 formatter.EndObject();
426
427 formatter.AddField("size", area.size);
428
429 if (area.parent_id >= 0) {
430 formatter.AddField("parent_id", area.parent_id);
431 }
432
433 formatter.BeginObject("bounds");
434 formatter.AddField("min_x", area.min_x);
435 formatter.AddField("min_y", area.min_y);
436 formatter.AddField("max_x", area.max_x);
437 formatter.AddField("max_y", area.max_y);
438 formatter.EndObject();
439
440 formatter.EndObject();
441 }
442 formatter.EndArray();
443
444 // Connections array
445 formatter.BeginArray("connections");
446 for (const auto& conn : connections) {
447 formatter.BeginObject();
448 formatter.AddField("from_area", conn.from_area);
449 formatter.AddField("to_area", conn.to_area);
450 formatter.AddField("direction", conn.direction);
451
452 formatter.BeginObject("edge");
453 formatter.AddField("x", conn.edge_x);
454 formatter.AddField("y", conn.edge_y);
455 formatter.EndObject();
456
457 formatter.AddField("bidirectional", conn.bidirectional);
458 formatter.EndObject();
459 }
460 formatter.EndArray();
461
462 // Entrances array
463 formatter.BeginArray("entrances");
464 for (const auto& entrance : entrances) {
465 formatter.BeginObject();
466 formatter.AddField("id", entrance.id);
467 formatter.AddField("name", entrance.name);
468 formatter.AddField("area_id", entrance.area_id);
469
470 formatter.BeginObject("position");
471 formatter.AddField("x", entrance.position_x);
472 formatter.AddField("y", entrance.position_y);
473 formatter.EndObject();
474
475 formatter.AddField("dest_room_id", entrance.dest_room_id);
476 formatter.AddField("is_hole", entrance.is_hole);
477 formatter.EndObject();
478 }
479 formatter.EndArray();
480
481 // Exits array
482 formatter.BeginArray("exits");
483 for (const auto& exit : exits) {
484 formatter.BeginObject();
485 formatter.AddField("room_id", exit.room_id);
486 formatter.AddField("name", exit.name);
487 formatter.AddField("return_area_id", exit.return_area_id);
488
489 formatter.BeginObject("return_position");
490 formatter.AddField("x", exit.return_x);
491 formatter.AddField("y", exit.return_y);
492 formatter.EndObject();
493
494 formatter.EndObject();
495 }
496 formatter.EndArray();
497
498 // Summary stats
499 formatter.BeginObject("stats");
500 formatter.AddField("total_areas", static_cast<int>(areas.size()));
501 formatter.AddField("total_connections", static_cast<int>(connections.size()));
502 formatter.AddField("total_entrances", static_cast<int>(entrances.size()));
503 formatter.AddField("total_exits", static_cast<int>(exits.size()));
504 formatter.EndObject();
505
506 formatter.EndObject();
507
508 return absl::OkStatus();
509}
510
511} // namespace handlers
512} // namespace cli
513} // 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::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:416
auto title() const
Definition rom.h:137
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 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.
Represents the full Overworld data, light and dark world.
Definition overworld.h:261
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:36
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:562
auto overworld_map(int i) const
Definition overworld.h:528
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:557
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
constexpr int kEntranceRoom
constexpr int kNumOverworldMaps
Definition common.h:85
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr int kTransitionTargetWest
Definition overworld.h:155
constexpr const char * kEntranceNames[]
Definition common.h:91
constexpr int kTransitionTargetNorth
Definition overworld.h:154
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22