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