7#include <initializer_list>
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
13#include "nlohmann/json.hpp"
21using Json = nlohmann::ordered_json;
29 for (
char c : token) {
32 }
else if (c ==
'/') {
41std::string
Child(
const std::string& ptr,
const std::string& key) {
45std::string
Child(
const std::string& ptr,
size_t index) {
46 return ptr +
"/" + std::to_string(index);
50 std::string text = value.dump();
51 if (text.size() > 40) {
52 text = text.substr(0, 37) +
"...";
57bool IsHexDigit(
char c) {
58 return std::isxdigit(
static_cast<unsigned char>(c)) != 0;
65 explicit DocReader(std::string file) : file_(std::move(file)) {}
67 const std::string&
file()
const {
return file_; }
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));
78 std::initializer_list<const char*> allowed)
const {
79 if (!value.is_object()) {
80 return Error(ptr,
"expected an object, got " +
Describe(value));
82 for (
const auto& [key, unused] : value.items()) {
83 if (!key.empty() && key.front() ==
'_') {
87 std::any_of(allowed.begin(), allowed.end(),
88 [&key = key](
const char*
name) { return key == name; });
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,
", ")));
97 return absl::OkStatus();
101 const char* key)
const {
102 if (!
object.contains(key)) {
103 return Error(ptr, absl::StrFormat(
"missing required key \"%s\"", key));
105 return absl::OkStatus();
109 const std::string& ptr)
const {
110 if (!value.is_string()) {
111 return Error(ptr,
"expected a string, got " +
Describe(value));
113 return value.get<std::string>();
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);
121 return text.status();
123 for (
const char* choice : choices) {
124 if (*text == choice) {
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,
", ")));
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 "
144 absl::StatusOr<int>
Integer(
const Json& value,
const std::string& ptr,
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",
151 return static_cast<int>(value.get<int64_t>());
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));
158 return value.get<
bool>();
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()) {
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)",
177 return Error(ptr, absl::StrFormat(
"%s 0x%X is out of range (0x0-0x%X)",
178 what, *
id, limit - 1));
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);
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));
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);
205 return first.status();
207 auto last = Id(text.substr(dash + 1), item_ptr, limit, what);
209 return last.status();
211 if (*last < *first) {
214 absl::StrFormat(
"range \"%s\" ends before it starts", text));
216 for (
int id = *first;
id <= *last; ++id) {
222 auto id = Id(item, item_ptr, limit, what);
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)};
243 return Error(ptr, absl::StrFormat(
"%s is not a color (\"#RRGGBB\")",
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));
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));
256 return text.status();
258 out.push_back(*text);
267#define ATLAS_ASSIGN(lhs, expr) \
269 auto atlas_value_ = (expr); \
270 if (!atlas_value_.ok()) { \
271 return atlas_value_.status(); \
273 lhs = std::move(atlas_value_).value(); \
276#define ATLAS_CHECK(expr) \
278 absl::Status atlas_status_ = (expr); \
279 if (!atlas_status_.ok()) { \
280 return atlas_status_; \
287 for (
auto& existing : out->
dungeons) {
290 ? existing.owner == entry.
owner
291 : existing.owner.empty() && existing.dungeon_id == entry.
dungeon_id;
295 if (!entry.
code.empty()) {
296 existing.code = entry.
code;
298 if (!entry.
name.empty()) {
299 existing.name = entry.
name;
301 if (entry.
color.has_value()) {
302 existing.color = entry.
color;
308 existing.origin = entry.
origin;
311 out->
dungeons.push_back(std::move(entry));
315 for (
auto& existing : out->
areas) {
316 if (existing.key != entry.
key) {
319 if (!entry.
code.empty()) {
320 existing.code = entry.
code;
322 if (!entry.
name.empty()) {
323 existing.name = entry.
name;
325 if (entry.
color.has_value()) {
326 existing.color = entry.
color;
328 existing.rooms = entry.
rooms;
329 if (!entry.
note.empty()) {
330 existing.note = entry.
note;
332 existing.origin = entry.
origin;
335 out->
areas.push_back(std::move(entry));
339 for (
auto& existing : out->
plans) {
340 if (existing.key != entry.
key) {
343 if (!entry.
label.empty()) {
344 existing.label = entry.
label;
346 if (!entry.
name.empty()) {
347 existing.name = entry.
name;
349 if (!entry.
status.empty()) {
350 existing.status = entry.
status;
352 if (entry.
color.has_value()) {
353 existing.color = entry.
color;
355 existing.rooms = entry.
rooms;
356 if (!entry.
note.empty()) {
357 existing.note = entry.
note;
359 existing.origin = entry.
origin;
362 out->
plans.push_back(std::move(entry));
366 if (!entry.
id.empty()) {
367 for (
auto& existing : out->
links) {
368 if (existing.id == entry.
id) {
369 existing = std::move(entry);
374 out->
links.push_back(std::move(entry));
378 auto& existing = out->
rooms[room];
379 if (!entry.
name.empty()) {
380 existing.name = entry.
name;
382 existing.reserved = existing.reserved || entry.
reserved;
383 if (!entry.
note.empty()) {
384 existing.note = entry.
note;
386 existing.origin = entry.
origin;
392 if (!entry.
name.empty()) {
393 existing.name = entry.
name;
395 if (!entry.
note.empty()) {
396 existing.note = entry.
note;
398 existing.origin = entry.
origin;
403 if (existing.map == entry.
map && existing.gate == entry.
gate) {
404 if (!entry.
note.empty()) {
405 existing.note = entry.
note;
407 if (!entry.
role.empty()) {
408 existing.role = entry.
role;
411 existing.origin = entry.
origin;
420 if (existing.name == entry.
name && existing.world == entry.
world) {
421 existing = std::move(entry);
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()) {
440 const std::string& ptr) {
442 r.
CheckObject(value, ptr, {
"symbol",
"count",
"width",
"kind",
"label"}));
452 absl::StrFormat(
"count %d is more than %d rooms", link.
count,
455 if (value.contains(
"width")) {
458 return r.
Error(
Child(ptr,
"width"),
"width must be 1 or 2");
462 {
"pit",
"warp",
"stair",
"pad",
"whirlpool",
463 "special",
"dream",
"warn"}));
464 if (value.contains(
"label")) {
471 const std::string& ptr) {
474 {
"key",
"label",
"name",
"status",
"color",
"rooms",
"source"}));
480 if (value.contains(
"label")) {
483 if (value.contains(
"name")) {
486 if (value.contains(
"status")) {
488 {
"decided",
"provisional"}));
490 if (value.contains(
"color")) {
495 if (value.contains(
"source")) {
502 const std::string& ptr) {
504 value, ptr, {
"key",
"code",
"name",
"color",
"rooms",
"source"}));
510 if (area.
key.empty()) {
511 return r.
Error(
Child(ptr,
"key"),
"key must not be empty");
513 if (value.contains(
"code")) {
516 if (value.contains(
"name")) {
519 if (value.contains(
"color")) {
524 if (value.contains(
"source")) {
533 if (value.contains(
"dungeon")) {
534 std::vector<std::string> notes;
537 notes.begin(), notes.end());
539 if (value.contains(
"overworld")) {
540 std::vector<std::string> notes;
544 notes.begin(), notes.end());
546 return absl::OkStatus();
551 const std::string& ptr) {
553 r.
CheckObject(value, ptr, {
"title",
"subtitle",
"maps",
"origin"}));
558 if (value.contains(
"title")) {
561 if (value.contains(
"subtitle")) {
563 r.
String(value[
"subtitle"],
Child(ptr,
"subtitle")));
567 const Json& origin = value[
"origin"];
568 if (!origin.is_array() || origin.size() != 2 || !origin[0].is_number() ||
569 !origin[1].is_number()) {
571 "origin must be [column, row] (two numbers)");
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>\"",
589 const std::string gate = key.substr(colon + 1);
590 if (gate !=
"Glove" && gate !=
"Mitt" && gate !=
"Hammer" &&
592 return r.
Error(ptr, absl::StrFormat(
"gate \"%s\" is not Glove, Mitt, "
596 return std::make_pair(map, gate);
603 {
"sections",
"regions",
"maps",
"gates",
"edges",
604 "closed_border_maps",
"isolated_maps",
"tile_classes",
605 "gate_role_thresholds",
"warp_tile_rule"}));
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");
613 for (
size_t i = 0; i < list.size(); ++i) {
616 ow.sections.push_back(std::move(section));
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");
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];
629 {
"name",
"code",
"world",
"color",
"maps"}));
636 if (item.contains(
"code")) {
640 if (item.contains(
"world")) {
642 r.
Enum(item[
"world"],
Child(item_ptr,
"world"),
643 {
"LW",
"DW",
"SW"}));
645 if (item.contains(
"color")) {
647 r.
Color(item[
"color"],
Child(item_ptr,
"color")));
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");
660 for (
const auto& [key, item] : maps.items()) {
661 const std::string item_ptr =
Child(maps_ptr, key);
667 if (item.contains(
"name")) {
671 if (item.contains(
"note")) {
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>\"");
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;
691 gate.
map = gate_key.first;
692 gate.
gate = gate_key.second;
693 if (item.contains(
"note")) {
697 if (item.contains(
"role")) {
699 {
"ROUTE",
"POCKET",
"DECOR"}));
701 if (item.contains(
"suppress")) {
703 r.
Bool(item[
"suppress"],
Child(item_ptr,
"suppress")));
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");
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];
728 r.
Enum(item[
"status"],
Child(item_ptr,
"status"),
729 {
"open",
"swim",
"blocked",
"special"}));
730 if (item.contains(
"note")) {
734 ow.edges.push_back(std::move(edge));
737 for (
const char* key : {
"closed_border_maps",
"isolated_maps"}) {
738 if (!value.contains(key)) {
741 std::vector<int> maps;
744 AppendUnique(std::string(key) ==
"closed_border_maps"
745 ? &ow.closed_border_maps
749 if (value.contains(
"tile_classes")) {
750 const Json& classes = value[
"tile_classes"];
751 const std::string classes_ptr =
Child(ptr,
"tile_classes");
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() ==
'_') {
759 std::vector<int> ids;
760 const bool tile16 = key ==
"hammer_tile16";
763 tile16 ?
"tile16" :
"tile type"));
764 ow.tile_classes[key] = std::move(ids);
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");
771 if (thresholds.contains(
"big")) {
775 if (thresholds.contains(
"small")) {
777 r.
Integer(thresholds[
"small"],
Child(t_ptr,
"small"), 1));
780 if (value.contains(
"warp_tile_rule")) {
782 r.
Enum(value[
"warp_tile_rule"],
Child(ptr,
"warp_tile_rule"),
783 {
"toggle_world",
"none"}));
785 return absl::OkStatus();
794 {
"schema",
"title",
"about",
"dungeons",
"areas",
"rooms",
"links",
795 "symbol_links",
"plans",
"legend_notes",
"items",
"overworld"}));
796 if (root.contains(
"title")) {
800 if (root.contains(
"about")) {
803 if (root.contains(
"dungeons")) {
804 const Json& list = root[
"dungeons"];
805 if (!list.is_array()) {
806 return r.
Error(
"/dungeons",
"expected a list");
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")) {
815 "needs \"owner\" (registry dungeon ID) or "
816 "\"dungeon_id\" (entrance dungeon ID)");
821 if (item.contains(
"owner")) {
825 if (item.contains(
"dungeon_id")) {
827 r.
Id(item[
"dungeon_id"],
Child(ptr,
"dungeon_id"), 0x100,
830 if (item.contains(
"code")) {
833 if (item.contains(
"name")) {
836 if (item.contains(
"color")) {
843 if (root.contains(
"areas")) {
844 const Json& list = root[
"areas"];
845 if (!list.is_array()) {
846 return r.
Error(
"/areas",
"expected a list");
848 for (
size_t i = 0; i < list.size(); ++i) {
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");
859 for (
const auto& [key, item] : rooms.items()) {
860 const std::string ptr =
Child(
"/rooms", key);
861 if (!key.empty() && key.front() ==
'_') {
869 if (item.contains(
"name")) {
872 if (item.contains(
"status")) {
877 if (item.contains(
"note")) {
883 if (root.contains(
"links")) {
884 const Json& list = root[
"links"];
885 if (!list.is_array()) {
886 return r.
Error(
"/links",
"expected a list");
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"}));
898 if (item.contains(
"id")) {
903 {
"dungeon",
"overworld"}));
911 {
"pit",
"warp",
"stair",
"pad",
"whirlpool",
912 "special",
"dream",
"warn"}));
914 r.
Id(item[
"from"],
Child(ptr,
"from"), limit, what));
915 if (item.contains(
"to")) {
918 if (item.contains(
"label")) {
921 if (item.contains(
"source")) {
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");
932 for (
size_t i = 0; i < list.size(); ++i) {
938 if (root.contains(
"plans")) {
939 const Json& list = root[
"plans"];
940 if (!list.is_array()) {
941 return r.
Error(
"/plans",
"expected a list");
943 for (
size_t i = 0; i < list.size(); ++i) {
949 if (root.contains(
"legend_notes")) {
952 if (root.contains(
"items")) {
953 const Json& items = root[
"items"];
956 {
"small_key",
"compass",
"big_key",
"map",
"pot_small_key"}));
957 const std::pair<const char*, int*> fields[] = {
963 for (
const auto& [key, target] : fields) {
964 if (items.contains(key)) {
970 if (root.contains(
"overworld")) {
973 return absl::OkStatus();
986 {
"dungeon_codes",
"areas",
"room_names",
"runtime_pits",
"plans",
"title",
987 "dungeon_colors",
"symbol_links",
"legend_notes"}));
988 if (root.contains(
"title")) {
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}");
998 for (
const auto& [owner, code] : codes.items()) {
999 if (!owner.empty() && owner.front() ==
'_') {
1004 dungeon.
owner = owner;
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\"}");
1015 for (
const auto& [owner, color] : colors.items()) {
1016 if (!owner.empty() && owner.front() ==
'_') {
1022 dungeon.
owner = owner;
1024 r.
Color(color,
Child(
"/dungeon_colors", owner)));
1028 if (root.contains(
"areas")) {
1029 const Json& list = root[
"areas"];
1030 if (!list.is_array()) {
1031 return r.
Error(
"/areas",
"expected a list");
1033 for (
size_t i = 0; i < list.size(); ++i) {
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}");
1044 for (
const auto& [key,
name] : names.items()) {
1045 if (!key.empty() && key.front() ==
'_') {
1048 const std::string ptr =
Child(
"/room_names", key);
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");
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];
1076 if (item.contains(
"source")) {
1082 if (root.contains(
"plans")) {
1083 const Json& list = root[
"plans"];
1084 if (!list.is_array()) {
1085 return r.
Error(
"/plans",
"expected a list");
1087 for (
size_t i = 0; i < list.size(); ++i) {
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");
1098 for (
size_t i = 0; i < list.size(); ++i) {
1104 if (root.contains(
"legend_notes")) {
1105 std::vector<std::string> notes;
1108 notes.begin(), notes.end());
1110 return absl::OkStatus();
1119 {
"regions",
"names",
"gate_notes",
"special_edge_block",
1120 "sw_isolated_maps",
"links",
"sections",
"legend_notes",
"title"}));
1122 if (root.contains(
"title")) {
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]}");
1131 for (
const auto& [key, maps] : regions.items()) {
1132 if (!key.empty() && key.front() ==
'_') {
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)) {
1140 "region names start with the world: \"LW \", "
1141 "\"DW \" or \"SW \"");
1145 region.
world = key.substr(0, 2);
1146 region.
name = key.substr(3);
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}");
1156 for (
const auto& [key,
name] : names.items()) {
1157 if (!key.empty() && key.front() ==
'_') {
1160 const std::string ptr =
Child(
"/names", key);
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}");
1175 for (
const auto& [key, note] : notes.items()) {
1176 if (!key.empty() && key.front() ==
'_') {
1179 const std::string ptr =
Child(
"/gate_notes", key);
1180 std::pair<int, std::string> gate_key;
1184 gate.
map = gate_key.first;
1185 gate.
gate = gate_key.second;
1190 if (root.contains(
"special_edge_block")) {
1191 std::vector<int> maps;
1194 AppendUnique(&ow.closed_border_maps, maps);
1196 if (root.contains(
"sw_isolated_maps")) {
1197 std::vector<int> maps;
1200 AppendUnique(&ow.isolated_maps, maps);
1202 if (root.contains(
"links")) {
1203 const Json& list = root[
"links"];
1204 if (!list.is_array()) {
1205 return r.
Error(
"/links",
"expected a list");
1207 for (
size_t i = 0; i < list.size(); ++i) {
1208 const std::string ptr =
Child(
"/links", i);
1209 const Json& item = list[i];
1221 r.
Enum(item[
"kind"],
Child(ptr,
"kind"),
1222 {
"pit",
"warp",
"stair",
"pad",
"whirlpool",
1223 "special",
"dream",
"warn"}));
1227 if (root.contains(
"sections")) {
1228 const Json& list = root[
"sections"];
1229 if (!list.is_array()) {
1230 return r.
Error(
"/sections",
"expected a list");
1232 for (
size_t i = 0; i < list.size(); ++i) {
1235 ow.sections.push_back(std::move(section));
1238 if (root.contains(
"legend_notes")) {
1239 std::vector<std::string> notes;
1242 notes.begin(), notes.end());
1244 return absl::OkStatus();
1249 if (!root.is_object()) {
1250 return r.
Error(
"",
"an annotation file is a JSON object");
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\")",
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"};
1267 bool overworld =
false;
1268 for (
const char* key : kRoomKeys) {
1269 room |= root.contains(key);
1271 for (
const char* key : kOverworldKeys) {
1272 overworld |= root.contains(key);
1274 if (room && overworld) {
1276 "mixes Oracle room keys (dungeon_codes, areas, ...) "
1277 "and overworld keys (regions, names, ...); split the "
1278 "file or add \"schema\": \"yaze.atlas/1\"");
1287 "cannot tell the format; add \"schema\": "
1288 "\"yaze.atlas/1\"");
1297 return absl::StrFormat(
"#%02X%02X%02X",
r,
g,
b);
1307 return "annotation:" +
ref;
1309 return "symbol:" +
ref;
1319 return "yaze.atlas/1";
1321 return "oracle-room-v0";
1323 return "oracle-overworld-v0";
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);
1334 if (digits.empty() || digits.size() > 3 ||
1335 !std::all_of(digits.begin(), digits.end(), IsHexDigit)) {
1336 return std::nullopt;
1338 return std::stoi(digits,
nullptr, 16);
1342 const std::string& file_label,
1345 const DocReader reader(file_label);
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()));
1354 auto detected = DetectFormat(reader, root);
1355 if (!detected.ok()) {
1356 return detected.status();
1358 if (format !=
nullptr) {
1359 *format = *detected;
1363 absl::Status status;
1364 switch (*detected) {
1366 status = ReadAtlasV1(reader, root, &merged);
1369 status = ReadOracleRoomV0(reader, root, &merged);
1372 status = ReadOracleOverworldV0(reader, root, &merged);
1378 merged.
files.push_back(file_label);
1379 *out = std::move(merged);
1380 return absl::OkStatus();
1386 std::ifstream file(path, std::ios::binary);
1388 return absl::NotFoundError(
1389 absl::StrFormat(
"Cannot read Atlas annotation file %s", path));
1391 std::stringstream buffer;
1392 buffer << file.rdbuf();
1394 buffer.str(), std::filesystem::path(path).filename().string(), out,
1402 dungeon.
code = entry.code;
1403 dungeon.
name = entry.name;
1404 dungeon.
color = entry.color;
1407 MergeDungeon(out, std::move(dungeon));
1413 const std::vector<std::string>& files,
const std::string& defaults) {
1415 if (defaults ==
"vanilla" || defaults.empty()) {
1417 }
else if (defaults !=
"none") {
1418 return absl::InvalidArgumentError(absl::StrFormat(
1419 "[atlas] defaults must be \"vanilla\" or \"none\", got \"%s\"",
1422 for (
const auto& file : files) {
#define ATLAS_ASSIGN(lhs, expr)
#define ATLAS_CHECK(expr)
static Json parse(const std::string &)
const std::string & file() const
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
DocReader(std::string file)
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)
nlohmann::ordered_json Json
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)
constexpr int kTile16Limit
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)
std::string Describe(const Json &value)
void MergePlan(AtlasAnnotations *out, PlanAnnotation entry)
absl::Status ReadOracleOverworldV0(const DocReader &r, const Json &root, AtlasAnnotations *out)
constexpr int kItemIdLimit
constexpr int kTileTypeLimit
std::string EscapePointerToken(const std::string &token)
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< std::string > files
std::vector< SymbolLinkAnnotation > symbol_links
std::vector< PlanAnnotation > plans
std::vector< DungeonAnnotation > dungeons
std::vector< AreaAnnotation > areas
OverworldAnnotations overworld
std::vector< std::string > legend_notes_overworld
std::map< int, RoomAnnotation > rooms
std::optional< AtlasColor > color
std::string ToString() const
std::vector< OverworldGateAnnotation > gates
std::vector< OverworldRegionAnnotation > regions
std::map< int, OverworldMapAnnotation > maps
std::optional< AtlasColor > color
std::optional< AtlasColor > color