yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
atlas_annotations.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <initializer_list>
8#include <sstream>
9#include <utility>
10
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
13#include "nlohmann/json.hpp"
15
17
18namespace {
19
20// ordered_json keeps the file's key order (region and legend order matter).
21using Json = nlohmann::ordered_json;
22
23constexpr int kItemIdLimit = 0x100;
24constexpr int kTileTypeLimit = 0x100;
25constexpr int kTile16Limit = 0x1000;
26
27std::string EscapePointerToken(const std::string& token) {
28 std::string out;
29 for (char c : token) {
30 if (c == '~') {
31 out += "~0";
32 } else if (c == '/') {
33 out += "~1";
34 } else {
35 out += c;
36 }
37 }
38 return out;
39}
40
41std::string Child(const std::string& ptr, const std::string& key) {
42 return ptr + "/" + EscapePointerToken(key);
43}
44
45std::string Child(const std::string& ptr, size_t index) {
46 return ptr + "/" + std::to_string(index);
47}
48
49std::string Describe(const Json& value) {
50 std::string text = value.dump();
51 if (text.size() > 40) {
52 text = text.substr(0, 37) + "...";
53 }
54 return text;
55}
56
57bool IsHexDigit(char c) {
58 return std::isxdigit(static_cast<unsigned char>(c)) != 0;
59}
60
61// Reads one annotation document. Every error names the file and the JSON
62// pointer of the offending value.
63class DocReader {
64 public:
65 explicit DocReader(std::string file) : file_(std::move(file)) {}
66
67 const std::string& file() const { return file_; }
68 Origin origin() const { return Origin::Annotation(file_); }
69
70 absl::Status Error(const std::string& ptr, const std::string& message) const {
71 return absl::InvalidArgumentError(
72 absl::StrFormat("%s: %s: %s", file_, ptr.empty() ? "/" : ptr, message));
73 }
74
75 // `value` must be an object whose keys are all in `allowed`; keys starting
76 // with '_' are comments.
77 absl::Status CheckObject(const Json& value, const std::string& ptr,
78 std::initializer_list<const char*> allowed) const {
79 if (!value.is_object()) {
80 return Error(ptr, "expected an object, got " + Describe(value));
81 }
82 for (const auto& [key, unused] : value.items()) {
83 if (!key.empty() && key.front() == '_') {
84 continue;
85 }
86 const bool known =
87 std::any_of(allowed.begin(), allowed.end(),
88 [&key = key](const char* name) { return key == name; });
89 if (!known) {
90 std::vector<std::string> names(allowed.begin(), allowed.end());
91 return Error(Child(ptr, key),
92 absl::StrFormat("unknown key (allowed: %s; keys starting "
93 "with '_' are comments)",
94 absl::StrJoin(names, ", ")));
95 }
96 }
97 return absl::OkStatus();
98 }
99
100 absl::Status Require(const Json& object, const std::string& ptr,
101 const char* key) const {
102 if (!object.contains(key)) {
103 return Error(ptr, absl::StrFormat("missing required key \"%s\"", key));
104 }
105 return absl::OkStatus();
106 }
107
108 absl::StatusOr<std::string> String(const Json& value,
109 const std::string& ptr) const {
110 if (!value.is_string()) {
111 return Error(ptr, "expected a string, got " + Describe(value));
112 }
113 return value.get<std::string>();
114 }
115
116 absl::StatusOr<std::string> Enum(
117 const Json& value, const std::string& ptr,
118 std::initializer_list<const char*> choices) const {
119 auto text = String(value, ptr);
120 if (!text.ok()) {
121 return text.status();
122 }
123 for (const char* choice : choices) {
124 if (*text == choice) {
125 return *text;
126 }
127 }
128 std::vector<std::string> names(choices.begin(), choices.end());
129 return Error(ptr, absl::StrFormat("\"%s\" is not one of: %s", *text,
130 absl::StrJoin(names, ", ")));
131 }
132
133 absl::StatusOr<std::string> Code(const Json& value,
134 const std::string& ptr) const {
135 auto text = String(value, ptr);
136 if (text.ok() && text->size() > 4) {
137 return Error(ptr, absl::StrFormat("code \"%s\" is longer than 4 "
138 "characters",
139 *text));
140 }
141 return text;
142 }
143
144 absl::StatusOr<int> Integer(const Json& value, const std::string& ptr,
145 int min) const {
146 if (!value.is_number_integer() || value.get<int64_t>() < min ||
147 value.get<int64_t>() > 0x7FFFFFFF) {
148 return Error(ptr, absl::StrFormat("expected an integer >= %d, got %s",
149 min, Describe(value)));
150 }
151 return static_cast<int>(value.get<int64_t>());
152 }
153
154 absl::StatusOr<bool> Bool(const Json& value, const std::string& ptr) const {
155 if (!value.is_boolean()) {
156 return Error(ptr, "expected true or false, got " + Describe(value));
157 }
158 return value.get<bool>();
159 }
160
161 // A room, map or item ID: "0x81", "81" (hex) or an integer, below `limit`.
162 absl::StatusOr<int> Id(const Json& value, const std::string& ptr, int limit,
163 const char* what) const {
164 std::optional<int> id;
165 if (value.is_number_integer() && value.get<int64_t>() >= 0 &&
166 value.get<int64_t>() < 0x1000) {
167 id = static_cast<int>(value.get<int64_t>());
168 } else if (value.is_string()) {
169 id = ParseAtlasId(value.get<std::string>());
170 }
171 if (!id.has_value()) {
172 return Error(ptr, absl::StrFormat("%s is not a %s ID (hex like \"0x1F\" "
173 "or \"1F\", or a number)",
174 Describe(value), what));
175 }
176 if (*id >= limit) {
177 return Error(ptr, absl::StrFormat("%s 0x%X is out of range (0x0-0x%X)",
178 what, *id, limit - 1));
179 }
180 return *id;
181 }
182
183 absl::StatusOr<int> Id(const std::string& text, const std::string& ptr,
184 int limit, const char* what) const {
185 return Id(Json(text), ptr, limit, what);
186 }
187
188 // IDs and ranges ("0x90-0x96").
189 absl::StatusOr<std::vector<int>> IdList(const Json& value,
190 const std::string& ptr, int limit,
191 const char* what) const {
192 if (!value.is_array()) {
193 return Error(ptr, "expected a list of IDs, got " + Describe(value));
194 }
195 std::vector<int> out;
196 for (size_t i = 0; i < value.size(); ++i) {
197 const Json& item = value[i];
198 const std::string item_ptr = Child(ptr, i);
199 if (item.is_string()) {
200 const std::string text = item.get<std::string>();
201 const size_t dash = text.find('-');
202 if (dash != std::string::npos) {
203 auto first = Id(text.substr(0, dash), item_ptr, limit, what);
204 if (!first.ok()) {
205 return first.status();
206 }
207 auto last = Id(text.substr(dash + 1), item_ptr, limit, what);
208 if (!last.ok()) {
209 return last.status();
210 }
211 if (*last < *first) {
212 return Error(
213 item_ptr,
214 absl::StrFormat("range \"%s\" ends before it starts", text));
215 }
216 for (int id = *first; id <= *last; ++id) {
217 out.push_back(id);
218 }
219 continue;
220 }
221 }
222 auto id = Id(item, item_ptr, limit, what);
223 if (!id.ok()) {
224 return id.status();
225 }
226 out.push_back(*id);
227 }
228 return out;
229 }
230
231 absl::StatusOr<AtlasColor> Color(const Json& value,
232 const std::string& ptr) const {
233 if (value.is_string()) {
234 const std::string text = value.get<std::string>();
235 if (text.size() == 7 && text[0] == '#' &&
236 std::all_of(text.begin() + 1, text.end(), IsHexDigit)) {
237 const int rgb = std::stoi(text.substr(1), nullptr, 16);
238 return AtlasColor{static_cast<uint8_t>(rgb >> 16),
239 static_cast<uint8_t>((rgb >> 8) & 0xFF),
240 static_cast<uint8_t>(rgb & 0xFF)};
241 }
242 }
243 return Error(ptr, absl::StrFormat("%s is not a color (\"#RRGGBB\")",
244 Describe(value)));
245 }
246
247 absl::StatusOr<std::vector<std::string>> StringList(
248 const Json& value, const std::string& ptr) const {
249 if (!value.is_array()) {
250 return Error(ptr, "expected a list of strings, got " + Describe(value));
251 }
252 std::vector<std::string> out;
253 for (size_t i = 0; i < value.size(); ++i) {
254 auto text = String(value[i], Child(ptr, i));
255 if (!text.ok()) {
256 return text.status();
257 }
258 out.push_back(*text);
259 }
260 return out;
261 }
262
263 private:
264 std::string file_;
265};
266
267#define ATLAS_ASSIGN(lhs, expr) \
268 do { \
269 auto atlas_value_ = (expr); \
270 if (!atlas_value_.ok()) { \
271 return atlas_value_.status(); \
272 } \
273 lhs = std::move(atlas_value_).value(); \
274 } while (false)
275
276#define ATLAS_CHECK(expr) \
277 do { \
278 absl::Status atlas_status_ = (expr); \
279 if (!atlas_status_.ok()) { \
280 return atlas_status_; \
281 } \
282 } while (false)
283
284// ---------------------------------------------------------------- merging
285
287 for (auto& existing : out->dungeons) {
288 const bool same =
289 !entry.owner.empty()
290 ? existing.owner == entry.owner
291 : existing.owner.empty() && existing.dungeon_id == entry.dungeon_id;
292 if (!same) {
293 continue;
294 }
295 if (!entry.code.empty()) {
296 existing.code = entry.code;
297 }
298 if (!entry.name.empty()) {
299 existing.name = entry.name;
300 }
301 if (entry.color.has_value()) {
302 existing.color = entry.color;
303 existing.color_origin = entry.color_origin;
304 }
305 if (entry.dungeon_id >= 0) {
306 existing.dungeon_id = entry.dungeon_id;
307 }
308 existing.origin = entry.origin;
309 return;
310 }
311 out->dungeons.push_back(std::move(entry));
312}
313
315 for (auto& existing : out->areas) {
316 if (existing.key != entry.key) {
317 continue;
318 }
319 if (!entry.code.empty()) {
320 existing.code = entry.code;
321 }
322 if (!entry.name.empty()) {
323 existing.name = entry.name;
324 }
325 if (entry.color.has_value()) {
326 existing.color = entry.color;
327 }
328 existing.rooms = entry.rooms;
329 if (!entry.note.empty()) {
330 existing.note = entry.note;
331 }
332 existing.origin = entry.origin;
333 return;
334 }
335 out->areas.push_back(std::move(entry));
336}
337
339 for (auto& existing : out->plans) {
340 if (existing.key != entry.key) {
341 continue;
342 }
343 if (!entry.label.empty()) {
344 existing.label = entry.label;
345 }
346 if (!entry.name.empty()) {
347 existing.name = entry.name;
348 }
349 if (!entry.status.empty()) {
350 existing.status = entry.status;
351 }
352 if (entry.color.has_value()) {
353 existing.color = entry.color;
354 }
355 existing.rooms = entry.rooms;
356 if (!entry.note.empty()) {
357 existing.note = entry.note;
358 }
359 existing.origin = entry.origin;
360 return;
361 }
362 out->plans.push_back(std::move(entry));
363}
364
366 if (!entry.id.empty()) {
367 for (auto& existing : out->links) {
368 if (existing.id == entry.id) {
369 existing = std::move(entry);
370 return;
371 }
372 }
373 }
374 out->links.push_back(std::move(entry));
375}
376
377void MergeRoom(AtlasAnnotations* out, int room, RoomAnnotation entry) {
378 auto& existing = out->rooms[room];
379 if (!entry.name.empty()) {
380 existing.name = entry.name;
381 }
382 existing.reserved = existing.reserved || entry.reserved;
383 if (!entry.note.empty()) {
384 existing.note = entry.note;
385 }
386 existing.origin = entry.origin;
387}
388
391 auto& existing = out->overworld.maps[map];
392 if (!entry.name.empty()) {
393 existing.name = entry.name;
394 }
395 if (!entry.note.empty()) {
396 existing.note = entry.note;
397 }
398 existing.origin = entry.origin;
399}
400
402 for (auto& existing : out->overworld.gates) {
403 if (existing.map == entry.map && existing.gate == entry.gate) {
404 if (!entry.note.empty()) {
405 existing.note = entry.note;
406 }
407 if (!entry.role.empty()) {
408 existing.role = entry.role;
409 }
410 existing.suppress = entry.suppress;
411 existing.origin = entry.origin;
412 return;
413 }
414 }
415 out->overworld.gates.push_back(std::move(entry));
416}
417
419 for (auto& existing : out->overworld.regions) {
420 if (existing.name == entry.name && existing.world == entry.world) {
421 existing = std::move(entry);
422 return;
423 }
424 }
425 out->overworld.regions.push_back(std::move(entry));
426}
427
428void AppendUnique(std::vector<int>* list, const std::vector<int>& more) {
429 for (int id : more) {
430 if (std::find(list->begin(), list->end(), id) == list->end()) {
431 list->push_back(id);
432 }
433 }
434}
435
436// ---------------------------------------------------------------- shared
437
438absl::StatusOr<SymbolLinkAnnotation> ReadSymbolLink(const DocReader& r,
439 const Json& value,
440 const std::string& ptr) {
442 r.CheckObject(value, ptr, {"symbol", "count", "width", "kind", "label"}));
443 ATLAS_CHECK(r.Require(value, ptr, "symbol"));
444 ATLAS_CHECK(r.Require(value, ptr, "count"));
445 ATLAS_CHECK(r.Require(value, ptr, "kind"));
447 link.origin = r.origin();
448 ATLAS_ASSIGN(link.symbol, r.String(value["symbol"], Child(ptr, "symbol")));
449 ATLAS_ASSIGN(link.count, r.Integer(value["count"], Child(ptr, "count"), 1));
450 if (link.count > kAtlasRoomCount) {
451 return r.Error(Child(ptr, "count"),
452 absl::StrFormat("count %d is more than %d rooms", link.count,
454 }
455 if (value.contains("width")) {
456 ATLAS_ASSIGN(link.width, r.Integer(value["width"], Child(ptr, "width"), 1));
457 if (link.width != 1 && link.width != 2) {
458 return r.Error(Child(ptr, "width"), "width must be 1 or 2");
459 }
460 }
461 ATLAS_ASSIGN(link.kind, r.Enum(value["kind"], Child(ptr, "kind"),
462 {"pit", "warp", "stair", "pad", "whirlpool",
463 "special", "dream", "warn"}));
464 if (value.contains("label")) {
465 ATLAS_ASSIGN(link.label, r.String(value["label"], Child(ptr, "label")));
466 }
467 return link;
468}
469
470absl::StatusOr<PlanAnnotation> ReadPlan(const DocReader& r, const Json& value,
471 const std::string& ptr) {
473 value, ptr,
474 {"key", "label", "name", "status", "color", "rooms", "source"}));
475 ATLAS_CHECK(r.Require(value, ptr, "key"));
476 ATLAS_CHECK(r.Require(value, ptr, "rooms"));
477 PlanAnnotation plan;
478 plan.origin = r.origin();
479 ATLAS_ASSIGN(plan.key, r.String(value["key"], Child(ptr, "key")));
480 if (value.contains("label")) {
481 ATLAS_ASSIGN(plan.label, r.String(value["label"], Child(ptr, "label")));
482 }
483 if (value.contains("name")) {
484 ATLAS_ASSIGN(plan.name, r.String(value["name"], Child(ptr, "name")));
485 }
486 if (value.contains("status")) {
487 ATLAS_ASSIGN(plan.status, r.Enum(value["status"], Child(ptr, "status"),
488 {"decided", "provisional"}));
489 }
490 if (value.contains("color")) {
491 ATLAS_ASSIGN(plan.color, r.Color(value["color"], Child(ptr, "color")));
492 }
493 ATLAS_ASSIGN(plan.rooms, r.IdList(value["rooms"], Child(ptr, "rooms"),
494 kAtlasRoomCount, "room"));
495 if (value.contains("source")) {
496 ATLAS_ASSIGN(plan.note, r.String(value["source"], Child(ptr, "source")));
497 }
498 return plan;
499}
500
501absl::StatusOr<AreaAnnotation> ReadArea(const DocReader& r, const Json& value,
502 const std::string& ptr) {
504 value, ptr, {"key", "code", "name", "color", "rooms", "source"}));
505 ATLAS_CHECK(r.Require(value, ptr, "key"));
506 ATLAS_CHECK(r.Require(value, ptr, "rooms"));
507 AreaAnnotation area;
508 area.origin = r.origin();
509 ATLAS_ASSIGN(area.key, r.String(value["key"], Child(ptr, "key")));
510 if (area.key.empty()) {
511 return r.Error(Child(ptr, "key"), "key must not be empty");
512 }
513 if (value.contains("code")) {
514 ATLAS_ASSIGN(area.code, r.Code(value["code"], Child(ptr, "code")));
515 }
516 if (value.contains("name")) {
517 ATLAS_ASSIGN(area.name, r.String(value["name"], Child(ptr, "name")));
518 }
519 if (value.contains("color")) {
520 ATLAS_ASSIGN(area.color, r.Color(value["color"], Child(ptr, "color")));
521 }
522 ATLAS_ASSIGN(area.rooms, r.IdList(value["rooms"], Child(ptr, "rooms"),
523 kAtlasRoomCount, "room"));
524 if (value.contains("source")) {
525 ATLAS_ASSIGN(area.note, r.String(value["source"], Child(ptr, "source")));
526 }
527 return area;
528}
529
530absl::Status ReadLegendNotes(const DocReader& r, const Json& value,
531 const std::string& ptr, AtlasAnnotations* out) {
532 ATLAS_CHECK(r.CheckObject(value, ptr, {"dungeon", "overworld"}));
533 if (value.contains("dungeon")) {
534 std::vector<std::string> notes;
535 ATLAS_ASSIGN(notes, r.StringList(value["dungeon"], Child(ptr, "dungeon")));
536 out->legend_notes_dungeon.insert(out->legend_notes_dungeon.end(),
537 notes.begin(), notes.end());
538 }
539 if (value.contains("overworld")) {
540 std::vector<std::string> notes;
541 ATLAS_ASSIGN(notes,
542 r.StringList(value["overworld"], Child(ptr, "overworld")));
543 out->legend_notes_overworld.insert(out->legend_notes_overworld.end(),
544 notes.begin(), notes.end());
545 }
546 return absl::OkStatus();
547}
548
549absl::StatusOr<OverworldSectionAnnotation> ReadSection(const DocReader& r,
550 const Json& value,
551 const std::string& ptr) {
553 r.CheckObject(value, ptr, {"title", "subtitle", "maps", "origin"}));
554 ATLAS_CHECK(r.Require(value, ptr, "maps"));
555 ATLAS_CHECK(r.Require(value, ptr, "origin"));
557 section.origin = r.origin();
558 if (value.contains("title")) {
559 ATLAS_ASSIGN(section.title, r.String(value["title"], Child(ptr, "title")));
560 }
561 if (value.contains("subtitle")) {
562 ATLAS_ASSIGN(section.subtitle,
563 r.String(value["subtitle"], Child(ptr, "subtitle")));
564 }
565 ATLAS_ASSIGN(section.maps, r.IdList(value["maps"], Child(ptr, "maps"),
566 kAtlasMapCount, "map"));
567 const Json& origin = value["origin"];
568 if (!origin.is_array() || origin.size() != 2 || !origin[0].is_number() ||
569 !origin[1].is_number()) {
570 return r.Error(Child(ptr, "origin"),
571 "origin must be [column, row] (two numbers)");
572 }
573 section.origin_col = origin[0].get<double>();
574 section.origin_row = origin[1].get<double>();
575 return section;
576}
577
578// "0x0F:Hammer" -> {0x0F, "Hammer"}
579absl::StatusOr<std::pair<int, std::string>> ReadGateKey(
580 const DocReader& r, const std::string& key, const std::string& ptr) {
581 const size_t colon = key.find(':');
582 if (colon == std::string::npos) {
583 return r.Error(ptr, absl::StrFormat("gate key \"%s\" must be "
584 "\"<map>:<Glove|Mitt|Hammer|Boots>\"",
585 key));
586 }
587 int map = 0;
588 ATLAS_ASSIGN(map, r.Id(key.substr(0, colon), ptr, kAtlasMapCount, "map"));
589 const std::string gate = key.substr(colon + 1);
590 if (gate != "Glove" && gate != "Mitt" && gate != "Hammer" &&
591 gate != "Boots") {
592 return r.Error(ptr, absl::StrFormat("gate \"%s\" is not Glove, Mitt, "
593 "Hammer or Boots",
594 gate));
595 }
596 return std::make_pair(map, gate);
597}
598
599absl::Status ReadOverworldV1(const DocReader& r, const Json& value,
600 const std::string& ptr, AtlasAnnotations* out) {
602 r.CheckObject(value, ptr,
603 {"sections", "regions", "maps", "gates", "edges",
604 "closed_border_maps", "isolated_maps", "tile_classes",
605 "gate_role_thresholds", "warp_tile_rule"}));
606 auto& ow = out->overworld;
607 if (value.contains("sections")) {
608 const Json& list = value["sections"];
609 const std::string list_ptr = Child(ptr, "sections");
610 if (!list.is_array()) {
611 return r.Error(list_ptr, "expected a list");
612 }
613 for (size_t i = 0; i < list.size(); ++i) {
615 ATLAS_ASSIGN(section, ReadSection(r, list[i], Child(list_ptr, i)));
616 ow.sections.push_back(std::move(section));
617 }
618 }
619 if (value.contains("regions")) {
620 const Json& list = value["regions"];
621 const std::string list_ptr = Child(ptr, "regions");
622 if (!list.is_array()) {
623 return r.Error(list_ptr, "expected a list");
624 }
625 for (size_t i = 0; i < list.size(); ++i) {
626 const std::string item_ptr = Child(list_ptr, i);
627 const Json& item = list[i];
628 ATLAS_CHECK(r.CheckObject(item, item_ptr,
629 {"name", "code", "world", "color", "maps"}));
630 ATLAS_CHECK(r.Require(item, item_ptr, "name"));
631 ATLAS_CHECK(r.Require(item, item_ptr, "maps"));
633 region.origin = r.origin();
634 ATLAS_ASSIGN(region.name,
635 r.String(item["name"], Child(item_ptr, "name")));
636 if (item.contains("code")) {
637 ATLAS_ASSIGN(region.code,
638 r.String(item["code"], Child(item_ptr, "code")));
639 }
640 if (item.contains("world")) {
641 ATLAS_ASSIGN(region.world,
642 r.Enum(item["world"], Child(item_ptr, "world"),
643 {"LW", "DW", "SW"}));
644 }
645 if (item.contains("color")) {
646 ATLAS_ASSIGN(region.color,
647 r.Color(item["color"], Child(item_ptr, "color")));
648 }
649 ATLAS_ASSIGN(region.maps, r.IdList(item["maps"], Child(item_ptr, "maps"),
650 kAtlasMapCount, "map"));
651 MergeRegion(out, std::move(region));
652 }
653 }
654 if (value.contains("maps")) {
655 const Json& maps = value["maps"];
656 const std::string maps_ptr = Child(ptr, "maps");
657 if (!maps.is_object()) {
658 return r.Error(maps_ptr, "expected an object keyed by map ID");
659 }
660 for (const auto& [key, item] : maps.items()) {
661 const std::string item_ptr = Child(maps_ptr, key);
662 int map = 0;
663 ATLAS_ASSIGN(map, r.Id(key, item_ptr, kAtlasMapCount, "map"));
664 ATLAS_CHECK(r.CheckObject(item, item_ptr, {"name", "note"}));
666 entry.origin = r.origin();
667 if (item.contains("name")) {
668 ATLAS_ASSIGN(entry.name,
669 r.String(item["name"], Child(item_ptr, "name")));
670 }
671 if (item.contains("note")) {
672 ATLAS_ASSIGN(entry.note,
673 r.String(item["note"], Child(item_ptr, "note")));
674 }
675 MergeOverworldMap(out, map, std::move(entry));
676 }
677 }
678 if (value.contains("gates")) {
679 const Json& gates = value["gates"];
680 const std::string gates_ptr = Child(ptr, "gates");
681 if (!gates.is_object()) {
682 return r.Error(gates_ptr, "expected an object keyed by \"<map>:<Gate>\"");
683 }
684 for (const auto& [key, item] : gates.items()) {
685 const std::string item_ptr = Child(gates_ptr, key);
686 std::pair<int, std::string> gate_key;
687 ATLAS_ASSIGN(gate_key, ReadGateKey(r, key, item_ptr));
688 ATLAS_CHECK(r.CheckObject(item, item_ptr, {"note", "role", "suppress"}));
690 gate.origin = r.origin();
691 gate.map = gate_key.first;
692 gate.gate = gate_key.second;
693 if (item.contains("note")) {
694 ATLAS_ASSIGN(gate.note,
695 r.String(item["note"], Child(item_ptr, "note")));
696 }
697 if (item.contains("role")) {
698 ATLAS_ASSIGN(gate.role, r.Enum(item["role"], Child(item_ptr, "role"),
699 {"ROUTE", "POCKET", "DECOR"}));
700 }
701 if (item.contains("suppress")) {
703 r.Bool(item["suppress"], Child(item_ptr, "suppress")));
704 }
705 MergeGate(out, std::move(gate));
706 }
707 }
708 if (value.contains("edges")) {
709 const Json& list = value["edges"];
710 const std::string list_ptr = Child(ptr, "edges");
711 if (!list.is_array()) {
712 return r.Error(list_ptr, "expected a list");
713 }
714 for (size_t i = 0; i < list.size(); ++i) {
715 const std::string item_ptr = Child(list_ptr, i);
716 const Json& item = list[i];
717 ATLAS_CHECK(r.CheckObject(item, item_ptr, {"a", "b", "status", "note"}));
718 ATLAS_CHECK(r.Require(item, item_ptr, "a"));
719 ATLAS_CHECK(r.Require(item, item_ptr, "b"));
720 ATLAS_CHECK(r.Require(item, item_ptr, "status"));
722 edge.origin = r.origin();
724 edge.a, r.Id(item["a"], Child(item_ptr, "a"), kAtlasMapCount, "map"));
726 edge.b, r.Id(item["b"], Child(item_ptr, "b"), kAtlasMapCount, "map"));
727 ATLAS_ASSIGN(edge.status,
728 r.Enum(item["status"], Child(item_ptr, "status"),
729 {"open", "swim", "blocked", "special"}));
730 if (item.contains("note")) {
731 ATLAS_ASSIGN(edge.note,
732 r.String(item["note"], Child(item_ptr, "note")));
733 }
734 ow.edges.push_back(std::move(edge));
735 }
736 }
737 for (const char* key : {"closed_border_maps", "isolated_maps"}) {
738 if (!value.contains(key)) {
739 continue;
740 }
741 std::vector<int> maps;
742 ATLAS_ASSIGN(maps,
743 r.IdList(value[key], Child(ptr, key), kAtlasMapCount, "map"));
744 AppendUnique(std::string(key) == "closed_border_maps"
745 ? &ow.closed_border_maps
746 : &ow.isolated_maps,
747 maps);
748 }
749 if (value.contains("tile_classes")) {
750 const Json& classes = value["tile_classes"];
751 const std::string classes_ptr = Child(ptr, "tile_classes");
752 ATLAS_CHECK(r.CheckObject(classes, classes_ptr,
753 {"free", "water", "ledge", "glove", "mitt",
754 "boots", "pit", "hammer_tile16"}));
755 for (const auto& [key, item] : classes.items()) {
756 if (!key.empty() && key.front() == '_') {
757 continue;
758 }
759 std::vector<int> ids;
760 const bool tile16 = key == "hammer_tile16";
761 ATLAS_ASSIGN(ids, r.IdList(item, Child(classes_ptr, key),
762 tile16 ? kTile16Limit : kTileTypeLimit,
763 tile16 ? "tile16" : "tile type"));
764 ow.tile_classes[key] = std::move(ids);
765 }
766 }
767 if (value.contains("gate_role_thresholds")) {
768 const Json& thresholds = value["gate_role_thresholds"];
769 const std::string t_ptr = Child(ptr, "gate_role_thresholds");
770 ATLAS_CHECK(r.CheckObject(thresholds, t_ptr, {"big", "small"}));
771 if (thresholds.contains("big")) {
772 ATLAS_ASSIGN(ow.gate_role_big,
773 r.Integer(thresholds["big"], Child(t_ptr, "big"), 1));
774 }
775 if (thresholds.contains("small")) {
776 ATLAS_ASSIGN(ow.gate_role_small,
777 r.Integer(thresholds["small"], Child(t_ptr, "small"), 1));
778 }
779 }
780 if (value.contains("warp_tile_rule")) {
781 ATLAS_ASSIGN(ow.warp_tile_rule,
782 r.Enum(value["warp_tile_rule"], Child(ptr, "warp_tile_rule"),
783 {"toggle_world", "none"}));
784 }
785 return absl::OkStatus();
786}
787
788// ---------------------------------------------------------------- v1
789
790absl::Status ReadAtlasV1(const DocReader& r, const Json& root,
791 AtlasAnnotations* out) {
793 root, "",
794 {"schema", "title", "about", "dungeons", "areas", "rooms", "links",
795 "symbol_links", "plans", "legend_notes", "items", "overworld"}));
796 if (root.contains("title")) {
797 ATLAS_ASSIGN(out->title, r.String(root["title"], "/title"));
798 out->title_origin = r.origin();
799 }
800 if (root.contains("about")) {
801 ATLAS_CHECK(r.String(root["about"], "/about").status());
802 }
803 if (root.contains("dungeons")) {
804 const Json& list = root["dungeons"];
805 if (!list.is_array()) {
806 return r.Error("/dungeons", "expected a list");
807 }
808 for (size_t i = 0; i < list.size(); ++i) {
809 const std::string ptr = Child("/dungeons", i);
810 const Json& item = list[i];
812 item, ptr, {"owner", "dungeon_id", "code", "name", "color"}));
813 if (!item.contains("owner") && !item.contains("dungeon_id")) {
814 return r.Error(ptr,
815 "needs \"owner\" (registry dungeon ID) or "
816 "\"dungeon_id\" (entrance dungeon ID)");
817 }
818 DungeonAnnotation dungeon;
819 dungeon.origin = r.origin();
820 dungeon.color_origin = r.origin();
821 if (item.contains("owner")) {
822 ATLAS_ASSIGN(dungeon.owner,
823 r.String(item["owner"], Child(ptr, "owner")));
824 }
825 if (item.contains("dungeon_id")) {
826 ATLAS_ASSIGN(dungeon.dungeon_id,
827 r.Id(item["dungeon_id"], Child(ptr, "dungeon_id"), 0x100,
828 "dungeon"));
829 }
830 if (item.contains("code")) {
831 ATLAS_ASSIGN(dungeon.code, r.Code(item["code"], Child(ptr, "code")));
832 }
833 if (item.contains("name")) {
834 ATLAS_ASSIGN(dungeon.name, r.String(item["name"], Child(ptr, "name")));
835 }
836 if (item.contains("color")) {
837 ATLAS_ASSIGN(dungeon.color,
838 r.Color(item["color"], Child(ptr, "color")));
839 }
840 MergeDungeon(out, std::move(dungeon));
841 }
842 }
843 if (root.contains("areas")) {
844 const Json& list = root["areas"];
845 if (!list.is_array()) {
846 return r.Error("/areas", "expected a list");
847 }
848 for (size_t i = 0; i < list.size(); ++i) {
849 AreaAnnotation area;
850 ATLAS_ASSIGN(area, ReadArea(r, list[i], Child("/areas", i)));
851 MergeArea(out, std::move(area));
852 }
853 }
854 if (root.contains("rooms")) {
855 const Json& rooms = root["rooms"];
856 if (!rooms.is_object()) {
857 return r.Error("/rooms", "expected an object keyed by room ID");
858 }
859 for (const auto& [key, item] : rooms.items()) {
860 const std::string ptr = Child("/rooms", key);
861 if (!key.empty() && key.front() == '_') {
862 continue;
863 }
864 int room = 0;
865 ATLAS_ASSIGN(room, r.Id(key, ptr, kAtlasRoomCount, "room"));
866 ATLAS_CHECK(r.CheckObject(item, ptr, {"name", "status", "note"}));
867 RoomAnnotation entry;
868 entry.origin = r.origin();
869 if (item.contains("name")) {
870 ATLAS_ASSIGN(entry.name, r.String(item["name"], Child(ptr, "name")));
871 }
872 if (item.contains("status")) {
873 ATLAS_CHECK(r.Enum(item["status"], Child(ptr, "status"), {"reserved"})
874 .status());
875 entry.reserved = true;
876 }
877 if (item.contains("note")) {
878 ATLAS_ASSIGN(entry.note, r.String(item["note"], Child(ptr, "note")));
879 }
880 MergeRoom(out, room, std::move(entry));
881 }
882 }
883 if (root.contains("links")) {
884 const Json& list = root["links"];
885 if (!list.is_array()) {
886 return r.Error("/links", "expected a list");
887 }
888 for (size_t i = 0; i < list.size(); ++i) {
889 const std::string ptr = Child("/links", i);
890 const Json& item = list[i];
892 item, ptr, {"id", "view", "kind", "from", "to", "label", "source"}));
893 ATLAS_CHECK(r.Require(item, ptr, "view"));
894 ATLAS_CHECK(r.Require(item, ptr, "kind"));
895 ATLAS_CHECK(r.Require(item, ptr, "from"));
896 LinkAnnotation link;
897 link.origin = r.origin();
898 if (item.contains("id")) {
899 ATLAS_ASSIGN(link.id, r.String(item["id"], Child(ptr, "id")));
900 }
901 std::string view;
902 ATLAS_ASSIGN(view, r.Enum(item["view"], Child(ptr, "view"),
903 {"dungeon", "overworld"}));
904 link.view =
905 view == "dungeon" ? AtlasView::kDungeon : AtlasView::kOverworld;
906 const int limit =
908 const char* what = link.view == AtlasView::kDungeon ? "room" : "map";
909 ATLAS_ASSIGN(link.kind,
910 r.Enum(item["kind"], Child(ptr, "kind"),
911 {"pit", "warp", "stair", "pad", "whirlpool",
912 "special", "dream", "warn"}));
913 ATLAS_ASSIGN(link.from,
914 r.Id(item["from"], Child(ptr, "from"), limit, what));
915 if (item.contains("to")) {
916 ATLAS_ASSIGN(link.to, r.Id(item["to"], Child(ptr, "to"), limit, what));
917 }
918 if (item.contains("label")) {
919 ATLAS_ASSIGN(link.label, r.String(item["label"], Child(ptr, "label")));
920 }
921 if (item.contains("source")) {
922 ATLAS_ASSIGN(link.note, r.String(item["source"], Child(ptr, "source")));
923 }
924 MergeLink(out, std::move(link));
925 }
926 }
927 if (root.contains("symbol_links")) {
928 const Json& list = root["symbol_links"];
929 if (!list.is_array()) {
930 return r.Error("/symbol_links", "expected a list");
931 }
932 for (size_t i = 0; i < list.size(); ++i) {
934 ATLAS_ASSIGN(link, ReadSymbolLink(r, list[i], Child("/symbol_links", i)));
935 out->symbol_links.push_back(std::move(link));
936 }
937 }
938 if (root.contains("plans")) {
939 const Json& list = root["plans"];
940 if (!list.is_array()) {
941 return r.Error("/plans", "expected a list");
942 }
943 for (size_t i = 0; i < list.size(); ++i) {
944 PlanAnnotation plan;
945 ATLAS_ASSIGN(plan, ReadPlan(r, list[i], Child("/plans", i)));
946 MergePlan(out, std::move(plan));
947 }
948 }
949 if (root.contains("legend_notes")) {
950 ATLAS_CHECK(ReadLegendNotes(r, root["legend_notes"], "/legend_notes", out));
951 }
952 if (root.contains("items")) {
953 const Json& items = root["items"];
955 items, "/items",
956 {"small_key", "compass", "big_key", "map", "pot_small_key"}));
957 const std::pair<const char*, int*> fields[] = {
958 {"small_key", &out->items.small_key},
959 {"compass", &out->items.compass},
960 {"big_key", &out->items.big_key},
961 {"map", &out->items.map},
962 {"pot_small_key", &out->items.pot_small_key}};
963 for (const auto& [key, target] : fields) {
964 if (items.contains(key)) {
965 ATLAS_ASSIGN(*target, r.Id(items[key], Child("/items", key),
966 kItemIdLimit, "item"));
967 }
968 }
969 }
970 if (root.contains("overworld")) {
971 ATLAS_CHECK(ReadOverworldV1(r, root["overworld"], "/overworld", out));
972 }
973 return absl::OkStatus();
974}
975
976// ---------------------------------------------------------------- Oracle v0
977
978// Scripts/Maps/room_annotations.json. Besides the keys the Oracle scripts
979// read, the optional keys the design doc (section 6.5) lets Oracle add are
980// accepted: title, dungeon_colors, areas[].code/color, plans[].color,
981// symbol_links, legend_notes.
982absl::Status ReadOracleRoomV0(const DocReader& r, const Json& root,
983 AtlasAnnotations* out) {
985 root, "",
986 {"dungeon_codes", "areas", "room_names", "runtime_pits", "plans", "title",
987 "dungeon_colors", "symbol_links", "legend_notes"}));
988 if (root.contains("title")) {
989 ATLAS_ASSIGN(out->title, r.String(root["title"], "/title"));
990 out->title_origin = r.origin();
991 }
992 if (root.contains("dungeon_codes")) {
993 const Json& codes = root["dungeon_codes"];
994 if (!codes.is_object()) {
995 return r.Error("/dungeon_codes",
996 "expected an object {registry dungeon ID: code}");
997 }
998 for (const auto& [owner, code] : codes.items()) {
999 if (!owner.empty() && owner.front() == '_') {
1000 continue;
1001 }
1002 DungeonAnnotation dungeon;
1003 dungeon.origin = r.origin();
1004 dungeon.owner = owner;
1005 ATLAS_ASSIGN(dungeon.code, r.Code(code, Child("/dungeon_codes", owner)));
1006 MergeDungeon(out, std::move(dungeon));
1007 }
1008 }
1009 if (root.contains("dungeon_colors")) {
1010 const Json& colors = root["dungeon_colors"];
1011 if (!colors.is_object()) {
1012 return r.Error("/dungeon_colors",
1013 "expected an object {registry dungeon ID: \"#RRGGBB\"}");
1014 }
1015 for (const auto& [owner, color] : colors.items()) {
1016 if (!owner.empty() && owner.front() == '_') {
1017 continue;
1018 }
1019 DungeonAnnotation dungeon;
1020 dungeon.origin = r.origin();
1021 dungeon.color_origin = r.origin();
1022 dungeon.owner = owner;
1023 ATLAS_ASSIGN(dungeon.color,
1024 r.Color(color, Child("/dungeon_colors", owner)));
1025 MergeDungeon(out, std::move(dungeon));
1026 }
1027 }
1028 if (root.contains("areas")) {
1029 const Json& list = root["areas"];
1030 if (!list.is_array()) {
1031 return r.Error("/areas", "expected a list");
1032 }
1033 for (size_t i = 0; i < list.size(); ++i) {
1034 AreaAnnotation area;
1035 ATLAS_ASSIGN(area, ReadArea(r, list[i], Child("/areas", i)));
1036 MergeArea(out, std::move(area));
1037 }
1038 }
1039 if (root.contains("room_names")) {
1040 const Json& names = root["room_names"];
1041 if (!names.is_object()) {
1042 return r.Error("/room_names", "expected an object {room ID: name}");
1043 }
1044 for (const auto& [key, name] : names.items()) {
1045 if (!key.empty() && key.front() == '_') {
1046 continue;
1047 }
1048 const std::string ptr = Child("/room_names", key);
1049 int room = 0;
1050 ATLAS_ASSIGN(room, r.Id(key, ptr, kAtlasRoomCount, "room"));
1051 RoomAnnotation entry;
1052 entry.origin = r.origin();
1053 ATLAS_ASSIGN(entry.name, r.String(name, ptr));
1054 MergeRoom(out, room, std::move(entry));
1055 }
1056 }
1057 if (root.contains("runtime_pits")) {
1058 const Json& list = root["runtime_pits"];
1059 if (!list.is_array()) {
1060 return r.Error("/runtime_pits", "expected a list");
1061 }
1062 for (size_t i = 0; i < list.size(); ++i) {
1063 const std::string ptr = Child("/runtime_pits", i);
1064 const Json& item = list[i];
1065 ATLAS_CHECK(r.CheckObject(item, ptr, {"room", "to", "source"}));
1066 ATLAS_CHECK(r.Require(item, ptr, "room"));
1067 ATLAS_CHECK(r.Require(item, ptr, "to"));
1068 LinkAnnotation link;
1069 link.origin = r.origin();
1071 link.kind = "pit";
1072 ATLAS_ASSIGN(link.from, r.Id(item["room"], Child(ptr, "room"),
1073 kAtlasRoomCount, "room"));
1074 ATLAS_ASSIGN(link.to,
1075 r.Id(item["to"], Child(ptr, "to"), kAtlasRoomCount, "room"));
1076 if (item.contains("source")) {
1077 ATLAS_ASSIGN(link.note, r.String(item["source"], Child(ptr, "source")));
1078 }
1079 MergeLink(out, std::move(link));
1080 }
1081 }
1082 if (root.contains("plans")) {
1083 const Json& list = root["plans"];
1084 if (!list.is_array()) {
1085 return r.Error("/plans", "expected a list");
1086 }
1087 for (size_t i = 0; i < list.size(); ++i) {
1088 PlanAnnotation plan;
1089 ATLAS_ASSIGN(plan, ReadPlan(r, list[i], Child("/plans", i)));
1090 MergePlan(out, std::move(plan));
1091 }
1092 }
1093 if (root.contains("symbol_links")) {
1094 const Json& list = root["symbol_links"];
1095 if (!list.is_array()) {
1096 return r.Error("/symbol_links", "expected a list");
1097 }
1098 for (size_t i = 0; i < list.size(); ++i) {
1100 ATLAS_ASSIGN(link, ReadSymbolLink(r, list[i], Child("/symbol_links", i)));
1101 out->symbol_links.push_back(std::move(link));
1102 }
1103 }
1104 if (root.contains("legend_notes")) {
1105 std::vector<std::string> notes;
1106 ATLAS_ASSIGN(notes, r.StringList(root["legend_notes"], "/legend_notes"));
1107 out->legend_notes_dungeon.insert(out->legend_notes_dungeon.end(),
1108 notes.begin(), notes.end());
1109 }
1110 return absl::OkStatus();
1111}
1112
1113// Scripts/Maps/overworld_annotations.json, plus the optional sections,
1114// legend_notes and title keys (design doc section 6.5).
1115absl::Status ReadOracleOverworldV0(const DocReader& r, const Json& root,
1116 AtlasAnnotations* out) {
1118 root, "",
1119 {"regions", "names", "gate_notes", "special_edge_block",
1120 "sw_isolated_maps", "links", "sections", "legend_notes", "title"}));
1121 auto& ow = out->overworld;
1122 if (root.contains("title")) {
1123 ATLAS_ASSIGN(out->title, r.String(root["title"], "/title"));
1124 out->title_origin = r.origin();
1125 }
1126 if (root.contains("regions")) {
1127 const Json& regions = root["regions"];
1128 if (!regions.is_object()) {
1129 return r.Error("/regions", "expected an object {\"LW Name\": [map IDs]}");
1130 }
1131 for (const auto& [key, maps] : regions.items()) {
1132 if (!key.empty() && key.front() == '_') {
1133 continue;
1134 }
1135 const std::string ptr = Child("/regions", key);
1136 if (key.size() < 4 || key[2] != ' ' ||
1137 (key.compare(0, 2, "LW") != 0 && key.compare(0, 2, "DW") != 0 &&
1138 key.compare(0, 2, "SW") != 0)) {
1139 return r.Error(ptr,
1140 "region names start with the world: \"LW \", "
1141 "\"DW \" or \"SW \"");
1142 }
1144 region.origin = r.origin();
1145 region.world = key.substr(0, 2);
1146 region.name = key.substr(3);
1147 ATLAS_ASSIGN(region.maps, r.IdList(maps, ptr, kAtlasMapCount, "map"));
1148 MergeRegion(out, std::move(region));
1149 }
1150 }
1151 if (root.contains("names")) {
1152 const Json& names = root["names"];
1153 if (!names.is_object()) {
1154 return r.Error("/names", "expected an object {map ID: name}");
1155 }
1156 for (const auto& [key, name] : names.items()) {
1157 if (!key.empty() && key.front() == '_') {
1158 continue;
1159 }
1160 const std::string ptr = Child("/names", key);
1161 int map = 0;
1162 ATLAS_ASSIGN(map, r.Id(key, ptr, kAtlasMapCount, "map"));
1164 entry.origin = r.origin();
1165 ATLAS_ASSIGN(entry.name, r.String(name, ptr));
1166 MergeOverworldMap(out, map, std::move(entry));
1167 }
1168 }
1169 if (root.contains("gate_notes")) {
1170 const Json& notes = root["gate_notes"];
1171 if (!notes.is_object()) {
1172 return r.Error("/gate_notes",
1173 "expected an object {\"<map>:<Gate>\": note}");
1174 }
1175 for (const auto& [key, note] : notes.items()) {
1176 if (!key.empty() && key.front() == '_') {
1177 continue;
1178 }
1179 const std::string ptr = Child("/gate_notes", key);
1180 std::pair<int, std::string> gate_key;
1181 ATLAS_ASSIGN(gate_key, ReadGateKey(r, key, ptr));
1183 gate.origin = r.origin();
1184 gate.map = gate_key.first;
1185 gate.gate = gate_key.second;
1186 ATLAS_ASSIGN(gate.note, r.String(note, ptr));
1187 MergeGate(out, std::move(gate));
1188 }
1189 }
1190 if (root.contains("special_edge_block")) {
1191 std::vector<int> maps;
1192 ATLAS_ASSIGN(maps, r.IdList(root["special_edge_block"],
1193 "/special_edge_block", kAtlasMapCount, "map"));
1194 AppendUnique(&ow.closed_border_maps, maps);
1195 }
1196 if (root.contains("sw_isolated_maps")) {
1197 std::vector<int> maps;
1198 ATLAS_ASSIGN(maps, r.IdList(root["sw_isolated_maps"], "/sw_isolated_maps",
1199 kAtlasMapCount, "map"));
1200 AppendUnique(&ow.isolated_maps, maps);
1201 }
1202 if (root.contains("links")) {
1203 const Json& list = root["links"];
1204 if (!list.is_array()) {
1205 return r.Error("/links", "expected a list");
1206 }
1207 for (size_t i = 0; i < list.size(); ++i) {
1208 const std::string ptr = Child("/links", i);
1209 const Json& item = list[i];
1210 ATLAS_CHECK(r.CheckObject(item, ptr, {"map", "text", "kind"}));
1211 ATLAS_CHECK(r.Require(item, ptr, "map"));
1212 ATLAS_CHECK(r.Require(item, ptr, "text"));
1213 ATLAS_CHECK(r.Require(item, ptr, "kind"));
1214 LinkAnnotation link;
1215 link.origin = r.origin();
1217 ATLAS_ASSIGN(link.from,
1218 r.Id(item["map"], Child(ptr, "map"), kAtlasMapCount, "map"));
1219 ATLAS_ASSIGN(link.label, r.String(item["text"], Child(ptr, "text")));
1220 ATLAS_ASSIGN(link.kind,
1221 r.Enum(item["kind"], Child(ptr, "kind"),
1222 {"pit", "warp", "stair", "pad", "whirlpool",
1223 "special", "dream", "warn"}));
1224 MergeLink(out, std::move(link));
1225 }
1226 }
1227 if (root.contains("sections")) {
1228 const Json& list = root["sections"];
1229 if (!list.is_array()) {
1230 return r.Error("/sections", "expected a list");
1231 }
1232 for (size_t i = 0; i < list.size(); ++i) {
1234 ATLAS_ASSIGN(section, ReadSection(r, list[i], Child("/sections", i)));
1235 ow.sections.push_back(std::move(section));
1236 }
1237 }
1238 if (root.contains("legend_notes")) {
1239 std::vector<std::string> notes;
1240 ATLAS_ASSIGN(notes, r.StringList(root["legend_notes"], "/legend_notes"));
1241 out->legend_notes_overworld.insert(out->legend_notes_overworld.end(),
1242 notes.begin(), notes.end());
1243 }
1244 return absl::OkStatus();
1245}
1246
1247absl::StatusOr<AnnotationFormat> DetectFormat(const DocReader& r,
1248 const Json& root) {
1249 if (!root.is_object()) {
1250 return r.Error("", "an annotation file is a JSON object");
1251 }
1252 if (root.contains("schema")) {
1253 if (root["schema"] != "yaze.atlas/1") {
1254 return r.Error("/schema",
1255 absl::StrFormat("unknown schema %s (this yaze reads "
1256 "\"yaze.atlas/1\")",
1257 Describe(root["schema"])));
1258 }
1260 }
1261 static constexpr const char* kRoomKeys[] = {
1262 "dungeon_codes", "areas", "room_names", "runtime_pits", "plans"};
1263 static constexpr const char* kOverworldKeys[] = {
1264 "regions", "names", "gate_notes", "special_edge_block",
1265 "sw_isolated_maps", "links"};
1266 bool room = false;
1267 bool overworld = false;
1268 for (const char* key : kRoomKeys) {
1269 room |= root.contains(key);
1270 }
1271 for (const char* key : kOverworldKeys) {
1272 overworld |= root.contains(key);
1273 }
1274 if (room && overworld) {
1275 return r.Error("",
1276 "mixes Oracle room keys (dungeon_codes, areas, ...) "
1277 "and overworld keys (regions, names, ...); split the "
1278 "file or add \"schema\": \"yaze.atlas/1\"");
1279 }
1280 if (room) {
1282 }
1283 if (overworld) {
1285 }
1286 return r.Error("",
1287 "cannot tell the format; add \"schema\": "
1288 "\"yaze.atlas/1\"");
1289}
1290
1291#undef ATLAS_ASSIGN
1292#undef ATLAS_CHECK
1293
1294} // namespace
1295
1296std::string AtlasColor::Hex() const {
1297 return absl::StrFormat("#%02X%02X%02X", r, g, b);
1298}
1299
1300std::string Origin::ToString() const {
1301 switch (kind) {
1302 case Provenance::kRom:
1303 return "rom";
1305 return "registry";
1307 return "annotation:" + ref;
1309 return "symbol:" + ref;
1311 return "default";
1312 }
1313 return "rom";
1314}
1315
1317 switch (format) {
1319 return "yaze.atlas/1";
1321 return "oracle-room-v0";
1323 return "oracle-overworld-v0";
1324 }
1325 return "?";
1326}
1327
1328std::optional<int> ParseAtlasId(const std::string& text) {
1329 std::string digits = text;
1330 if (digits.size() > 2 && digits[0] == '0' &&
1331 (digits[1] == 'x' || digits[1] == 'X')) {
1332 digits = digits.substr(2);
1333 }
1334 if (digits.empty() || digits.size() > 3 ||
1335 !std::all_of(digits.begin(), digits.end(), IsHexDigit)) {
1336 return std::nullopt;
1337 }
1338 return std::stoi(digits, nullptr, 16);
1339}
1340
1341absl::Status MergeAtlasAnnotationText(const std::string& text,
1342 const std::string& file_label,
1343 AtlasAnnotations* out,
1344 AnnotationFormat* format) {
1345 const DocReader reader(file_label);
1346 Json root;
1347 try {
1348 root = Json::parse(text);
1349 } catch (const Json::parse_error& error) {
1350 return reader.Error(
1351 "", absl::StrFormat("not valid JSON (byte %d): %s",
1352 static_cast<int>(error.byte), error.what()));
1353 }
1354 auto detected = DetectFormat(reader, root);
1355 if (!detected.ok()) {
1356 return detected.status();
1357 }
1358 if (format != nullptr) {
1359 *format = *detected;
1360 }
1361 // Merge into a copy so a failing file leaves `out` unchanged.
1362 AtlasAnnotations merged = *out;
1363 absl::Status status;
1364 switch (*detected) {
1366 status = ReadAtlasV1(reader, root, &merged);
1367 break;
1369 status = ReadOracleRoomV0(reader, root, &merged);
1370 break;
1372 status = ReadOracleOverworldV0(reader, root, &merged);
1373 break;
1374 }
1375 if (!status.ok()) {
1376 return status;
1377 }
1378 merged.files.push_back(file_label);
1379 *out = std::move(merged);
1380 return absl::OkStatus();
1381}
1382
1383absl::Status MergeAtlasAnnotationFile(const std::string& path,
1384 AtlasAnnotations* out,
1385 AnnotationFormat* format) {
1386 std::ifstream file(path, std::ios::binary);
1387 if (!file) {
1388 return absl::NotFoundError(
1389 absl::StrFormat("Cannot read Atlas annotation file %s", path));
1390 }
1391 std::stringstream buffer;
1392 buffer << file.rdbuf();
1394 buffer.str(), std::filesystem::path(path).filename().string(), out,
1395 format);
1396}
1397
1399 for (const auto& entry : VanillaDungeonDefaults()) {
1400 DungeonAnnotation dungeon;
1401 dungeon.dungeon_id = entry.dungeon_id;
1402 dungeon.code = entry.code;
1403 dungeon.name = entry.name;
1404 dungeon.color = entry.color;
1405 dungeon.origin = Origin::Default();
1406 dungeon.color_origin = Origin::Default();
1407 MergeDungeon(out, std::move(dungeon));
1408 }
1409 out->vanilla_defaults = true;
1410}
1411
1412absl::StatusOr<AtlasAnnotations> LoadAtlasAnnotations(
1413 const std::vector<std::string>& files, const std::string& defaults) {
1414 AtlasAnnotations annotations;
1415 if (defaults == "vanilla" || defaults.empty()) {
1416 ApplyVanillaAtlasDefaults(&annotations);
1417 } else if (defaults != "none") {
1418 return absl::InvalidArgumentError(absl::StrFormat(
1419 "[atlas] defaults must be \"vanilla\" or \"none\", got \"%s\"",
1420 defaults));
1421 }
1422 for (const auto& file : files) {
1423 auto status = MergeAtlasAnnotationFile(file, &annotations);
1424 if (!status.ok()) {
1425 return status;
1426 }
1427 }
1428 return annotations;
1429}
1430
1431} // namespace yaze::zelda3::atlas
#define ATLAS_ASSIGN(lhs, expr)
#define ATLAS_CHECK(expr)
static Json parse(const std::string &)
Definition json.h:36
absl::StatusOr< std::string > String(const Json &value, const std::string &ptr) const
absl::Status Error(const std::string &ptr, const std::string &message) const
absl::StatusOr< std::vector< int > > IdList(const Json &value, const std::string &ptr, int limit, const char *what) const
absl::StatusOr< int > Id(const std::string &text, const std::string &ptr, int limit, const char *what) const
absl::StatusOr< int > Integer(const Json &value, const std::string &ptr, int min) const
absl::StatusOr< std::vector< std::string > > StringList(const Json &value, const std::string &ptr) const
absl::StatusOr< std::string > Enum(const Json &value, const std::string &ptr, std::initializer_list< const char * > choices) const
absl::StatusOr< bool > Bool(const Json &value, const std::string &ptr) const
absl::Status Require(const Json &object, const std::string &ptr, const char *key) const
absl::StatusOr< AtlasColor > Color(const Json &value, const std::string &ptr) const
absl::Status CheckObject(const Json &value, const std::string &ptr, std::initializer_list< const char * > allowed) const
absl::StatusOr< std::string > Code(const Json &value, const std::string &ptr) const
absl::StatusOr< int > Id(const Json &value, const std::string &ptr, int limit, const char *what) const
absl::StatusOr< AnnotationFormat > DetectFormat(const DocReader &r, const Json &root)
absl::StatusOr< AreaAnnotation > ReadArea(const DocReader &r, const Json &value, const std::string &ptr)
absl::StatusOr< PlanAnnotation > ReadPlan(const DocReader &r, const Json &value, const std::string &ptr)
absl::Status ReadOverworldV1(const DocReader &r, const Json &value, const std::string &ptr, AtlasAnnotations *out)
absl::Status ReadLegendNotes(const DocReader &r, const Json &value, const std::string &ptr, AtlasAnnotations *out)
void MergeArea(AtlasAnnotations *out, AreaAnnotation entry)
void MergeRegion(AtlasAnnotations *out, OverworldRegionAnnotation entry)
void MergeLink(AtlasAnnotations *out, LinkAnnotation entry)
absl::StatusOr< std::pair< int, std::string > > ReadGateKey(const DocReader &r, const std::string &key, const std::string &ptr)
void MergeGate(AtlasAnnotations *out, OverworldGateAnnotation entry)
void MergeDungeon(AtlasAnnotations *out, DungeonAnnotation entry)
absl::StatusOr< SymbolLinkAnnotation > ReadSymbolLink(const DocReader &r, const Json &value, const std::string &ptr)
absl::StatusOr< OverworldSectionAnnotation > ReadSection(const DocReader &r, const Json &value, const std::string &ptr)
void MergePlan(AtlasAnnotations *out, PlanAnnotation entry)
absl::Status ReadOracleOverworldV0(const DocReader &r, const Json &root, AtlasAnnotations *out)
void MergeOverworldMap(AtlasAnnotations *out, int map, OverworldMapAnnotation entry)
std::string Child(const std::string &ptr, const std::string &key)
absl::Status ReadOracleRoomV0(const DocReader &r, const Json &root, AtlasAnnotations *out)
absl::Status ReadAtlasV1(const DocReader &r, const Json &root, AtlasAnnotations *out)
void MergeRoom(AtlasAnnotations *out, int room, RoomAnnotation entry)
constexpr int kAtlasMapCount
absl::Status MergeAtlasAnnotationFile(const std::string &path, AtlasAnnotations *out, AnnotationFormat *format)
void ApplyVanillaAtlasDefaults(AtlasAnnotations *out)
const std::vector< VanillaDungeonDefault > & VanillaDungeonDefaults()
absl::Status MergeAtlasAnnotationText(const std::string &text, const std::string &file_label, AtlasAnnotations *out, AnnotationFormat *format)
constexpr int kAtlasRoomCount
absl::StatusOr< AtlasAnnotations > LoadAtlasAnnotations(const std::vector< std::string > &files, const std::string &defaults)
std::optional< int > ParseAtlasId(const std::string &text)
const char * AnnotationFormatName(AnnotationFormat format)
std::optional< AtlasColor > color
std::vector< std::string > legend_notes_dungeon
std::vector< LinkAnnotation > links
std::vector< SymbolLinkAnnotation > symbol_links
std::vector< PlanAnnotation > plans
std::vector< DungeonAnnotation > dungeons
std::vector< AreaAnnotation > areas
std::vector< std::string > legend_notes_overworld
std::map< int, RoomAnnotation > rooms
std::vector< OverworldGateAnnotation > gates
std::vector< OverworldRegionAnnotation > regions
std::map< int, OverworldMapAnnotation > maps
std::optional< AtlasColor > color