13#include "absl/status/statusor.h"
14#include "absl/strings/str_format.h"
25 return "vanilla_safe";
27 return "hook_patched";
33 return "asm_expansion";
44absl::StatusOr<uint32_t> ParseHexAddress(
const std::string& str) {
46 if (str.size() >= 2 && str[0] ==
'0' && (str[1] ==
'x' || str[1] ==
'X')) {
47 return static_cast<uint32_t
>(std::stoul(str.substr(2),
nullptr, 16));
49 return static_cast<uint32_t
>(std::stoul(str,
nullptr, 16));
50 }
catch (
const std::exception& exc) {
51 return absl::InvalidArgumentError(
52 absl::StrFormat(
"Invalid hex address '%s': %s", str, exc.what()));
57 if (str ==
"vanilla_safe")
59 if (str ==
"hook_patched")
61 if (str ==
"asm_owned")
65 if (str ==
"asm_expansion")
71 return absl::InvalidArgumentError(
72 absl::StrFormat(
"Unknown ownership string '%s'", str));
94 if (address >= 0x800000 && address <= 0xFFFFFF) {
115 const std::string& field,
118 return absl::InvalidArgumentError(
119 absl::StrFormat(
"%s must be a hexadecimal string", field));
122 const std::string input = value.
get<std::string>();
123 size_t digits_begin = 0;
124 if (!input.empty() && input.front() ==
'$') {
126 }
else if (input.size() >= 2 && input[0] ==
'0' &&
127 (input[1] ==
'x' || input[1] ==
'X')) {
130 if (digits_begin == input.size()) {
131 return absl::InvalidArgumentError(
132 absl::StrFormat(
"%s has no hexadecimal digits", field));
134 for (
size_t index = digits_begin; index < input.size(); ++index) {
135 if (!std::isxdigit(
static_cast<unsigned char>(input[index]))) {
136 return absl::InvalidArgumentError(absl::StrFormat(
137 "%s contains an invalid hexadecimal value '%s'", field, input));
142 const uint64_t parsed =
143 std::stoull(input.substr(digits_begin),
nullptr, 16);
144 if (parsed > maximum) {
145 return absl::InvalidArgumentError(
146 absl::StrFormat(
"%s value '%s' exceeds 0x%X", field, input, maximum));
148 return static_cast<uint32_t
>(parsed);
149 }
catch (
const std::exception& exc) {
150 return absl::InvalidArgumentError(
151 absl::StrFormat(
"Invalid hexadecimal value for %s ('%s'): %s", field,
157 if (!root.
contains(
"manifest_version")) {
161 const Json& value = root[
"manifest_version"];
162 uint64_t version = 0;
163 if (value.is_number_unsigned()) {
164 version = value.
get<uint64_t>();
165 }
else if (value.is_number_integer()) {
166 const int64_t signed_version = value.
get<int64_t>();
167 if (signed_version < 0) {
168 return absl::InvalidArgumentError(
169 "manifest_version must be a non-negative integer");
171 version =
static_cast<uint64_t
>(signed_version);
173 return absl::InvalidArgumentError(
174 "manifest_version must be a non-negative integer");
177 if (version >
static_cast<uint64_t
>(std::numeric_limits<int>::max())) {
178 return absl::InvalidArgumentError(
"manifest_version is too large");
180 return static_cast<int>(version);
184 const std::string& key,
185 const std::string& path) {
186 if (!
object.contains(key)) {
187 return absl::InvalidArgumentError(
188 absl::StrFormat(
"%s.%s is required", path, key));
191 uint32_t address = 0;
195 const uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
196 if (bank == 0x7E || bank == 0x7F) {
197 return absl::InvalidArgumentError(absl::StrFormat(
198 "%s.%s must not use SNES WRAM bank 0x%02X", path, key, bank));
200 if ((address & 0xFFFF) < 0x8000) {
201 return absl::InvalidArgumentError(absl::StrFormat(
202 "%s.%s must be a mapped LoROM address (low word >= 0x8000)", path,
206 return absl::InvalidArgumentError(absl::StrFormat(
207 "%s.%s is not convertible to a canonical LoROM address", path, key));
213 const std::string& path) {
214 constexpr const char* kField =
"pointer_count";
215 constexpr uint32_t kMaxDungeonPointerCount = 296;
216 if (!
object.contains(kField)) {
217 return absl::InvalidArgumentError(
218 absl::StrFormat(
"%s.%s is required", path, kField));
221 const Json& value =
object[kField];
223 if (value.is_number_unsigned()) {
224 count = value.
get<uint64_t>();
225 }
else if (value.is_number_integer()) {
226 const int64_t signed_count = value.
get<int64_t>();
227 if (signed_count <= 0) {
228 return absl::InvalidArgumentError(
229 absl::StrFormat(
"%s.%s must be greater than zero", path, kField));
231 count =
static_cast<uint64_t
>(signed_count);
233 return absl::InvalidArgumentError(
234 absl::StrFormat(
"%s.%s must be an integer", path, kField));
237 if (count == 0 || count > kMaxDungeonPointerCount) {
238 return absl::InvalidArgumentError(absl::StrFormat(
239 "%s.%s must be in [1, %u]", path, kField, kMaxDungeonPointerCount));
241 return static_cast<uint32_t
>(count);
245 const Json&
object,
const std::string& path) {
246 constexpr const char* kField =
"pointer_encoding";
247 if (!
object.contains(kField) || !
object[kField].is_string()) {
248 return absl::InvalidArgumentError(
249 absl::StrFormat(
"%s.%s must be 'long24' or 'bank16'", path, kField));
251 const std::string value =
object[kField].get<std::string>();
252 if (value ==
"long24") {
255 if (value ==
"bank16") {
258 return absl::InvalidArgumentError(
259 absl::StrFormat(
"%s.%s has unknown encoding '%s'", path, kField, value));
263 const Json&
object,
const std::string& path) {
264 constexpr const char* kField =
"strategy";
265 if (!
object.contains(kField) || !
object[kField].is_string()) {
266 return absl::InvalidArgumentError(absl::StrFormat(
267 "%s.%s must be 'copy_on_write' or 'repack_all'", path, kField));
269 const std::string value =
object[kField].get<std::string>();
270 if (value ==
"copy_on_write") {
273 if (value ==
"repack_all") {
276 return absl::InvalidArgumentError(
277 absl::StrFormat(
"%s.%s has unknown strategy '%s'", path, kField, value));
287 const std::string& description) {
288 std::sort(ranges.begin(), ranges.end(),
290 return lhs.start < rhs.start ||
291 (lhs.start == rhs.start && lhs.end < rhs.end);
293 if (ranges.empty()) {
294 return absl::OkStatus();
298 for (
size_t index = 1; index < ranges.size(); ++index) {
301 return absl::InvalidArgumentError(
302 absl::StrFormat(
"%s overlap: %s conflicts with %s", description,
305 if (current.
end > active.
end) {
309 return absl::OkStatus();
313 const Json&
object,
const std::string& key,
const std::string& path) {
314 const std::string field_path = path +
"." + key;
315 if (!
object.contains(key) || !
object[key].is_array() ||
object[key].empty()) {
316 return absl::InvalidArgumentError(
317 absl::StrFormat(
"%s must be a non-empty array", field_path));
320 std::vector<SnesAddressRange> ranges;
321 std::vector<NamedPcRange> pc_ranges;
322 ranges.reserve(
object[key].size());
323 pc_ranges.reserve(
object[key].size());
325 for (
const Json& range_json :
object[key]) {
326 const std::string range_path =
327 absl::StrFormat(
"%s[%zu]", field_path, index);
328 if (!range_json.is_object()) {
329 return absl::InvalidArgumentError(
330 absl::StrFormat(
"%s must be an object", range_path));
340 if (range.
end <= range.
start || pc_end <= pc_start) {
341 return absl::InvalidArgumentError(absl::StrFormat(
342 "%s must be a non-empty half-open range with end > start",
346 ranges.push_back(range);
347 pc_ranges.push_back({pc_start, pc_end, range_path});
352 std::sort(ranges.begin(), ranges.end(),
354 return lhs.start < rhs.start;
360 const Json& root,
int manifest_version) {
361 constexpr const char* kSection =
"editor_managed_regions";
363 return std::vector<SnesAddressRange>{};
365 if (manifest_version < 3) {
366 return absl::InvalidArgumentError(
367 "editor_managed_regions requires manifest_version 3 or newer");
370 const Json& section = root[kSection];
372 return absl::InvalidArgumentError(
373 "editor_managed_regions must be a non-empty object");
384 const std::string& key,
385 const std::string& path,
386 bool require_mapped_lorom) {
387 if (require_mapped_lorom) {
390 if (!
object.contains(key)) {
391 return absl::InvalidArgumentError(
392 absl::StrFormat(
"%s.%s is required", path, key));
395 uint32_t address = 0;
402 const Json& root,
int manifest_version) {
404 constexpr const char* kSection =
"protected_regions";
409 const Json& section = root[kSection];
411 return absl::InvalidArgumentError(
"protected_regions must be an object");
414 return absl::InvalidArgumentError(
415 "protected_regions.regions must be an array");
419 parsed.
regions.reserve(section[
"regions"].size());
424 const bool require_mapped_lorom = manifest_version >= 3;
426 for (
const Json& region_json : section[
"regions"]) {
427 const std::string path =
428 absl::StrFormat(
"protected_regions.regions[%zu]", index);
429 if (!region_json.is_object()) {
430 return absl::InvalidArgumentError(
431 absl::StrFormat(
"%s must be an object", path));
437 require_mapped_lorom));
439 require_mapped_lorom));
442 return absl::InvalidArgumentError(absl::StrFormat(
443 "%s must be a non-empty half-open range with end > start", path));
446 region.
hook_count = region_json.value(
"hook_count", 0);
447 region.module = region_json.value(
"module",
"");
448 parsed.
regions.push_back(std::move(region));
456 return lhs.start < rhs.start;
469 const std::string path = absl::StrFormat(
"dungeon_stream_regions.%s",
471 if (!
json.is_object()) {
472 return absl::InvalidArgumentError(
473 absl::StrFormat(
"%s must be an object", path));
488 if (!
json.contains(
"pointer_bank")) {
489 return absl::InvalidArgumentError(
490 absl::StrFormat(
"%s.pointer_bank is required for bank16", path));
499 if (bank == 0x7E || bank == 0x7F) {
500 return absl::InvalidArgumentError(absl::StrFormat(
501 "%s.pointer_bank must not use SNES WRAM bank 0x%02X", path, bank));
505 const uint32_t bank_start_snes = (bank << 16) | 0x8000;
506 const uint32_t bank_start_pc =
SnesToPc(bank_start_snes);
507 const uint64_t bank_end_pc =
static_cast<uint64_t
>(bank_start_pc) + 0x8000;
509 if (
SnesToPc(range.start) < bank_start_pc ||
510 SnesToPc(range.end) > bank_end_pc) {
511 return absl::InvalidArgumentError(absl::StrFormat(
512 "%s.data_regions must stay within pointer_bank 0x%02X for "
517 }
else if (
json.contains(
"pointer_bank")) {
518 return absl::InvalidArgumentError(
519 absl::StrFormat(
"%s.pointer_bank is not allowed for long24", path));
523 const bool contained =
526 return PcRangeContains(data, allocation);
529 return absl::InvalidArgumentError(absl::StrFormat(
530 "%s allocation range [0x%06X, 0x%06X) is not fully contained in "
532 path, allocation.start, allocation.end));
536 const uint64_t pointer_width =
539 const uint64_t table_size =
541 const uint64_t table_end_pc = table_start_pc + table_size;
544 return absl::InvalidArgumentError(absl::StrFormat(
545 "%s pointer table extends beyond canonical LoROM address space", path));
548 const uint64_t runtime_table_end =
549 static_cast<uint64_t
>(layout.
pointer_table & 0xFFFFu) + table_size;
550 if (runtime_table_end > 0x10000u) {
551 return absl::InvalidArgumentError(absl::StrFormat(
552 "%s bank16 pointer table crosses its runtime CPU bank", path));
559absl::StatusOr<std::unordered_map<DungeonStreamType, DungeonStreamLayout>>
561 std::unordered_map<DungeonStreamType, DungeonStreamLayout> layouts;
562 if (!root.
contains(
"dungeon_stream_regions")) {
566 const Json& section = root[
"dungeon_stream_regions"];
568 return absl::InvalidArgumentError(
569 "dungeon_stream_regions must be a non-empty object");
572 constexpr std::array<std::pair<const char*, DungeonStreamType>, 3> kStreams =
576 for (
const auto& item : section.
items()) {
577 const auto& key = item.key();
579 for (
const auto& entry : kStreams) {
580 if (key == entry.first) {
586 return absl::InvalidArgumentError(absl::StrFormat(
587 "dungeon_stream_regions contains unknown stream '%s'", key));
591 std::vector<NamedPcRange> occupied_ranges;
592 std::vector<NamedPcRange> allocation_ranges;
593 for (
const auto& [key, stream] : kStreams) {
601 const uint64_t pointer_width =
604 const uint64_t table_end_pc =
607 occupied_ranges.push_back({
static_cast<uint32_t
>(table_start_pc),
608 static_cast<uint32_t
>(table_end_pc),
609 absl::StrFormat(
"%s.pointer_table", key)});
611 for (
size_t index = 0; index < layout.
data_regions.size(); ++index) {
613 occupied_ranges.push_back(
615 absl::StrFormat(
"%s.data_regions[%zu]", key, index)});
619 allocation_ranges.push_back(
621 absl::StrFormat(
"%s.allocation_regions[%zu]", key, index)});
623 layouts.emplace(stream, std::move(layout));
627 "dungeon stream pointer/data ranges"));
629 "dungeon stream allocation ranges"));
657 std::ifstream file(filepath);
658 if (!file.is_open()) {
659 return absl::NotFoundError(
"Could not open manifest: " + filepath);
661 std::stringstream buffer;
662 buffer << file.rdbuf();
672 }
catch (
const std::exception& exc) {
673 return absl::InvalidArgumentError(
674 std::string(
"Failed to parse manifest JSON: ") + exc.what());
678 return absl::InvalidArgumentError(
"Hack manifest root must be an object");
688 std::unordered_map<DungeonStreamType, DungeonStreamLayout>
689 dungeon_stream_layouts;
697 if (root.
contains(
"build_pipeline")) {
698 auto& pipeline = root[
"build_pipeline"];
707 if (root.
contains(
"owned_banks") && root[
"owned_banks"].
contains(
"banks")) {
708 for (
auto& bank_json : root[
"owned_banks"][
"banks"]) {
710 uint32_t bank_u32 = 0;
712 ParseHexAddress(bank_json.value(
"bank",
"0x00")));
713 bank.
bank =
static_cast<uint8_t
>(bank_u32 & 0xFF);
714 if (bank.
bank >= 0x80) {
718 "bank_start",
"0x000000")));
720 "bank_end",
"0x000000")));
724 "ownership",
"asm_owned")));
732 for (
auto& tag_json : root[
"room_tags"][
"tags"]) {
734 uint32_t tag_id_u32 = 0;
736 ParseHexAddress(tag_json.value(
"tag_id",
"0x00")));
737 tag.
tag_id =
static_cast<uint8_t
>(tag_id_u32 & 0xFF);
739 ParseHexAddress(tag_json.value(
"address",
"0x000000")));
741 tag.
name = tag_json.value(
"name",
"");
742 tag.
purpose = tag_json.value(
"purpose",
"");
743 tag.
source = tag_json.value(
"source",
"");
745 tag.
enabled = tag_json.value(
"enabled",
true);
752 if (root.
contains(
"feature_flags") &&
753 root[
"feature_flags"].
contains(
"flags")) {
754 for (
auto& flag_json : root[
"feature_flags"][
"flags"]) {
756 flag.
name = flag_json.value(
"name",
"");
757 flag.
value = flag_json.value(
"value", 0);
758 flag.
enabled = flag_json.value(
"enabled",
false);
759 flag.
source = flag_json.value(
"source",
"");
767 for (
auto& var_json : root[
"sram"][
"variables"]) {
769 var.
name = var_json.value(
"name",
"");
771 ParseHexAddress(var_json.value(
"address",
"0x000000")));
772 var.
purpose = var_json.value(
"purpose",
"");
780 auto& msg = root[
"messages"];
781 if (msg.contains(
"hook_address") && msg[
"hook_address"].is_string()) {
783 ParseHexAddress(msg[
"hook_address"].get<std::string>()));
787 if (msg.contains(
"data_start")) {
789 ParseHexAddress(msg.value(
"data_start",
"0x000000")));
793 if (msg.contains(
"data_end")) {
795 ParseHexAddress(msg.value(
"data_end",
"0x000000")));
799 if (msg.contains(
"expanded_range")) {
800 auto& expanded = msg[
"expanded_range"];
801 uint32_t first_id = 0;
802 uint32_t last_id = 0;
804 ParseHexAddress(expanded.value(
"first",
"0x000")));
806 ParseHexAddress(expanded.value(
"last",
"0x000")));
808 static_cast<uint16_t
>(first_id & 0xFFFF);
810 static_cast<uint16_t
>(last_id & 0xFFFF);
820 return absl::OkStatus();
827 address = NormalizeSnesAddress(address);
839 const uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
842 return bank_it->second.ownership;
856 uint32_t address)
const {
857 address = NormalizeSnesAddress(address);
861 const auto upper = std::upper_bound(
864 return addr < region.start;
867 for (
auto iter = std::make_reverse_iterator(upper);
869 if (address >= iter->start && address < iter->end) {
881 address = NormalizeSnesAddress(address);
882 const auto upper = std::upper_bound(
885 return addr < region.start;
891 return region.
start <= address && address < region.
end;
895 uint8_t bank)
const {
902 return iter->second.ownership;
909 return iter->second.name;
923 return iter->second.enabled;
927 const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
const {
928 std::vector<WriteConflict> conflicts;
933 for (
const auto& range : ranges) {
934 const uint32_t start = NormalizeSnesAddress(range.first);
935 const uint32_t end = NormalizeSnesAddress(range.second);
943 std::vector<uint32_t> boundaries = {start, end};
944 const uint64_t first_bank_boundary =
945 (
static_cast<uint64_t
>(start) & 0xFF0000u) + 0x010000u;
946 for (uint64_t boundary = first_bank_boundary; boundary < end;
947 boundary += 0x010000u) {
948 boundaries.push_back(
static_cast<uint32_t
>(boundary));
950 auto add_range_boundaries = [start, end, &boundaries](
const auto& regions) {
951 for (
const auto& region : regions) {
952 if (region.start < end && start < region.end) {
953 boundaries.push_back(std::max(start, region.start));
954 boundaries.push_back(std::min(end, region.end));
961 std::sort(boundaries.begin(), boundaries.end());
962 boundaries.erase(std::unique(boundaries.begin(), boundaries.end()),
965 bool previous_was_conflict =
false;
967 std::string previous_module;
968 for (
size_t index = 0; index + 1 < boundaries.size(); ++index) {
969 const uint32_t segment_start = boundaries[index];
970 if (segment_start >= boundaries[index + 1]) {
975 if (!IsAsmOwned(ownership)) {
976 previous_was_conflict =
false;
984 protected_region !=
nullptr) {
985 module = protected_region->module;
988 const uint8_t bank =
static_cast<uint8_t
>((segment_start >> 16) & 0xFF);
991 module = bank_it->second.ownership_note;
995 if (!previous_was_conflict || ownership != previous_ownership ||
996 module != previous_module) {
997 conflicts.push_back({segment_start, ownership,
module});
999 previous_was_conflict =
true;
1000 previous_ownership = ownership;
1001 previous_module = std::move(module);
1009 const std::vector<std::pair<uint32_t, uint32_t>>& pc_ranges)
const {
1014 std::vector<std::pair<uint32_t, uint32_t>> snes_ranges;
1015 snes_ranges.reserve(pc_ranges.size());
1017 for (
const auto& range : pc_ranges) {
1018 uint32_t pc_start = range.first;
1019 const uint32_t pc_end = range.second;
1020 if (pc_end <= pc_start) {
1026 while (pc_start < pc_end) {
1027 const uint32_t next_boundary = (pc_start & ~0x7FFFu) + 0x8000u;
1028 const uint32_t seg_end = std::min(pc_end, next_boundary);
1029 const uint32_t seg_len = seg_end - pc_start;
1030 const uint32_t snes_start =
PcToSnes(pc_start);
1031 const uint32_t snes_end = snes_start + seg_len;
1032 snes_ranges.emplace_back(snes_start, snes_end);
1044 return iter->second.name;
1063 namespace fs = std::filesystem;
1066 fs::path base(code_folder);
1069 fs::path planning = base /
"Docs" /
"Dev" /
"Planning";
1072 fs::path dungeons_path = planning /
"dungeons.json";
1073 if (fs::exists(dungeons_path)) {
1074 std::ifstream file(dungeons_path);
1075 if (file.is_open()) {
1076 std::stringstream buffer;
1077 buffer << file.rdbuf();
1081 for (
const auto& dj : root[
"dungeons"]) {
1083 entry.
id = dj.value(
"id",
"");
1084 entry.
name = dj.value(
"name",
"");
1088 if (dj.contains(
"rooms") && dj[
"rooms"].is_array()) {
1089 for (
const auto& rj : dj[
"rooms"]) {
1091 std::string id_str = rj.value(
"id",
"0x00");
1092 auto parsed = ParseHexAddress(id_str);
1093 room.
id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
1094 room.
name = rj.value(
"name",
"");
1095 room.
floor = rj.value(
"floor",
"");
1097 rj.contains(
"grid_row") && rj.contains(
"grid_col");
1098 room.
grid_row = rj.value(
"grid_row", 0);
1099 room.
grid_col = rj.value(
"grid_col", 0);
1100 room.
type = rj.value(
"type",
"normal");
1101 room.
palette = rj.value(
"palette", 0);
1102 room.
blockset = rj.value(
"blockset", 0);
1103 room.
spriteset = rj.value(
"spriteset", 0);
1104 room.
tag1 =
static_cast<uint8_t
>(rj.value(
"tag1", 0));
1105 room.
tag2 =
static_cast<uint8_t
>(rj.value(
"tag2", 0));
1106 entry.
rooms.push_back(std::move(room));
1111 if (dj.contains(
"stairs") && dj[
"stairs"].is_array()) {
1112 for (
const auto& sj : dj[
"stairs"]) {
1114 std::string from_str = sj.value(
"from",
"0x00");
1115 std::string to_str = sj.value(
"to",
"0x00");
1116 auto from_parsed = ParseHexAddress(from_str);
1117 auto to_parsed = ParseHexAddress(to_str);
1119 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1121 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1122 conn.
label = sj.value(
"label",
"");
1123 entry.
stairs.push_back(std::move(conn));
1128 if (dj.contains(
"holewarps") && dj[
"holewarps"].is_array()) {
1129 for (
const auto& hj : dj[
"holewarps"]) {
1131 std::string from_str = hj.value(
"from",
"0x00");
1132 std::string to_str = hj.value(
"to",
"0x00");
1133 auto from_parsed = ParseHexAddress(from_str);
1134 auto to_parsed = ParseHexAddress(to_str);
1136 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1138 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1139 conn.
label = hj.value(
"label",
"");
1140 entry.
holewarps.push_back(std::move(conn));
1145 if (dj.contains(
"doors") && dj[
"doors"].is_array()) {
1146 for (
const auto& doorj : dj[
"doors"]) {
1148 std::string from_str = doorj.value(
"from",
"0x00");
1149 std::string to_str = doorj.value(
"to",
"0x00");
1150 auto from_parsed = ParseHexAddress(from_str);
1151 auto to_parsed = ParseHexAddress(to_str);
1153 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1155 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1156 conn.
label = doorj.value(
"label",
"");
1157 conn.
direction = doorj.value(
"direction",
"");
1158 entry.
doors.push_back(std::move(conn));
1165 }
catch (
const std::exception& exc) {
1166 LOG_WARN(
"HackManifest",
"Failed to parse dungeons.json: %s",
1173 fs::path overworld_path = planning /
"overworld.json";
1174 if (fs::exists(overworld_path)) {
1175 std::ifstream file(overworld_path);
1176 if (file.is_open()) {
1177 std::stringstream buffer;
1178 buffer << file.rdbuf();
1182 for (
const auto& aj : root[
"areas"]) {
1184 std::string id_str = aj.value(
"area_id",
"0x00");
1185 auto parsed = ParseHexAddress(id_str);
1186 area.
area_id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
1187 area.
name = aj.value(
"name",
"");
1188 area.
world = aj.value(
"world",
"");
1189 area.
grid_row = aj.value(
"grid_row", 0);
1190 area.
grid_col = aj.value(
"grid_col", 0);
1194 }
catch (
const std::exception& exc) {
1195 LOG_WARN(
"HackManifest",
"Failed to parse overworld.json: %s",
1204 fs::path unified_path = planning /
"oracle_resource_labels.json";
1205 fs::path legacy_path = planning /
"oracle_room_labels.json";
1207 auto normalize_label_id = [](
const std::string& raw) -> std::string {
1210 std::string s = raw;
1211 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.front()))) {
1214 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.back()))) {
1222 if (s.size() >= 2 && s[0] ==
'0' && (s[1] ==
'x' || s[1] ==
'X')) {
1232 const unsigned long value = std::stoul(s, &idx, base);
1233 if (idx != s.size()) {
1236 if (value >
static_cast<unsigned long>(std::numeric_limits<int>::max())) {
1239 return std::to_string(
static_cast<int>(value));
1245 if (fs::exists(unified_path)) {
1246 std::ifstream file(unified_path);
1247 if (file.is_open()) {
1248 std::stringstream buffer;
1249 buffer << file.rdbuf();
1253 const std::vector<std::string> label_types = {
1254 "room",
"sprite",
"item",
"entrance",
"overworld_map",
"music"};
1255 for (
const auto& type_key : label_types) {
1257 for (
const auto& [key, value] : root[type_key].
items()) {
1258 if (value.is_string()) {
1259 const std::string normalized_key = normalize_label_id(key);
1262 value.get<std::string>();
1267 }
catch (
const std::exception& exc) {
1269 "Failed to parse oracle_resource_labels.json: %s", exc.what());
1272 }
else if (fs::exists(legacy_path)) {
1274 std::ifstream file(legacy_path);
1275 if (file.is_open()) {
1276 std::stringstream buffer;
1277 buffer << file.rdbuf();
1280 if (root.
contains(
"resource_labels") &&
1281 root[
"resource_labels"].
contains(
"room")) {
1282 for (
const auto& [key, value] :
1283 root[
"resource_labels"][
"room"].
items()) {
1284 const std::string normalized_key = normalize_label_id(key);
1286 value.get<std::string>();
1289 }
catch (
const std::exception& exc) {
1290 LOG_WARN(
"HackManifest",
"Failed to parse oracle_room_labels.json: %s",
1301 size_t dungeon_room_labels = 0;
1303 for (
const auto& room : dungeon.rooms) {
1304 if (room.id >= 0 && !room.name.empty()) {
1307 ++dungeon_room_labels;
1311 if (dungeon_room_labels > 0) {
1313 "Mirrored %zu dungeon room names into project resource labels",
1314 dungeon_room_labels);
1318 fs::path story_events_path = planning /
"story_events.json";
1319 if (fs::exists(story_events_path)) {
1322 if (story_status.ok()) {
1332 LOG_DEBUG(
"HackManifest",
"Loaded story events: %zu nodes, %zu edges",
1336 LOG_WARN(
"HackManifest",
"Failed to load story_events.json: %s",
1337 std::string(story_status.message()).c_str());
1341 size_t total_labels = 0;
1343 total_labels += labels.size();
1349 "Loaded project registry: %zu dungeons, %zu overworld areas, "
1350 "%zu resource labels (%zu types)",
1356 return absl::OkStatus();
static Json parse(const std::string &)
bool contains(const std::string &) const
T value(const std::string &, const T &def) const
std::vector< WriteConflict > AnalyzeWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &ranges) const
Analyze a set of address ranges for write conflicts.
std::unordered_map< std::string, FeatureFlag > feature_flag_map_
void ClearOracleProgressionState()
std::optional< RoomTagEntry > GetRoomTag(uint8_t tag_id) const
Get the full room tag entry for a tag ID.
std::unordered_map< uint8_t, OwnedBank > owned_banks_
AddressOwnership ClassifyAddress(uint32_t address) const
Classify a ROM address by ownership.
bool IsFeatureEnabled(const std::string &flag_name) const
std::vector< SramVariable > sram_variables_
std::vector< ProtectedRegion > protected_regions_
bool IsWriteOverwritten(uint32_t address) const
Check if a ROM write at this address would be overwritten by asar.
ProjectRegistry project_registry_
bool IsExpandedMessage(uint16_t message_id) const
bool IsEditorManaged(uint32_t address) const
Check if an address is explicitly managed by yaze.
BuildPipeline build_pipeline_
std::optional< AddressOwnership > GetBankOwnership(uint8_t bank) const
Get the bank ownership for a given bank number.
std::optional< OracleProgressionState > oracle_progression_state_
std::unordered_map< uint32_t, SramVariable > sram_map_
std::string GetSramVariableName(uint32_t address) const
std::unordered_map< uint8_t, RoomTagEntry > room_tag_map_
std::vector< SnesAddressRange > editor_managed_regions_
std::unordered_map< DungeonStreamType, DungeonStreamLayout > dungeon_stream_layouts_
const std::vector< ProtectedRegion > & protected_regions() const
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
bool IsProtected(uint32_t address) const
Check if an address is in a protected region.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
absl::Status LoadFromString(const std::string &json_content)
Load manifest from a JSON string.
MessageLayout message_layout_
std::string GetRoomTagLabel(uint8_t tag_id) const
Get the human-readable label for a room tag ID.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
void SetOracleProgressionState(const OracleProgressionState &state)
const ProtectedRegion * FindProtectedRegion(uint32_t address) const
const std::vector< SnesAddressRange > & editor_managed_regions() const
std::vector< RoomTagEntry > room_tags_
std::vector< FeatureFlag > feature_flags_
const std::vector< StoryEventNode > & nodes() const
void AutoLayout()
Compute layout positions using topological sort + layered positioning.
void UpdateStatus(uint8_t crystal_bitfield, uint8_t game_state)
Update node completion status based on SRAM state.
bool loaded() const
Check if the graph has been loaded.
const std::vector< StoryEdge > & edges() const
absl::Status LoadFromJson(const std::string &path)
Load the graph from a JSON file.
#define LOG_DEBUG(category, format,...)
#define LOG_WARN(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::StatusOr< AddressOwnership > ParseOwnership(const std::string &str)
constexpr uint64_t kCanonicalLoRomSize
absl::StatusOr< ParsedProtectedRegions > ParseProtectedRegions(const Json &root, int manifest_version)
bool IsAsmOwned(AddressOwnership ownership)
absl::StatusOr< DungeonWriteStrategy > ParseWriteStrategy(const Json &object, const std::string &path)
absl::Status ValidateDisjointRanges(std::vector< NamedPcRange > ranges, const std::string &description)
absl::StatusOr< uint32_t > ParseProtectedAddress(const Json &object, const std::string &key, const std::string &path, bool require_mapped_lorom)
absl::StatusOr< uint32_t > ParseStrictHexValue(const Json &value, const std::string &field, uint32_t maximum)
uint32_t NormalizeSnesAddress(uint32_t address)
absl::StatusOr< uint32_t > ParsePointerCount(const Json &object, const std::string &path)
const char * DungeonStreamTypeToString(DungeonStreamType stream)
absl::StatusOr< uint32_t > ParseLoRomAddress(const Json &object, const std::string &key, const std::string &path)
absl::StatusOr< int > ParseManifestVersion(const Json &root)
absl::StatusOr< std::unordered_map< DungeonStreamType, DungeonStreamLayout > > ParseDungeonStreamLayouts(const Json &root)
bool PcRangeContains(const SnesAddressRange &outer, const SnesAddressRange &inner)
absl::StatusOr< DungeonStreamLayout > ParseDungeonStreamLayout(DungeonStreamType stream, const Json &json)
absl::StatusOr< std::vector< SnesAddressRange > > ParseAddressRanges(const Json &object, const std::string &key, const std::string &path)
absl::StatusOr< std::vector< SnesAddressRange > > ParseEditorManagedRegions(const Json &root, int manifest_version)
absl::StatusOr< DungeonPointerEncoding > ParsePointerEncoding(const Json &object, const std::string &path)
std::string AddressOwnershipToString(AddressOwnership ownership)
DungeonStreamType
Dungeon stream kinds with allocator layouts in the hack manifest.
AddressOwnership
Ownership classification for ROM addresses and banks.
uint32_t PcToSnes(uint32_t addr)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
Build pipeline information.
A connection between two rooms (stair, holewarp, or door).
A complete dungeon entry with rooms and connections.
std::vector< DungeonConnection > doors
std::vector< DungeonConnection > holewarps
std::vector< DungeonRoom > rooms
std::vector< DungeonConnection > stairs
A room within a dungeon, with spatial and metadata info.
Explicit pointer and storage layout for one dungeon stream.
DungeonWriteStrategy strategy
std::vector< SnesAddressRange > allocation_regions
std::optional< uint8_t > pointer_bank
std::vector< SnesAddressRange > data_regions
DungeonPointerEncoding pointer_encoding
A compile-time feature flag.
Message range information for the expanded message system.
uint16_t last_expanded_id
uint16_t first_expanded_id
Oracle of Secrets game progression state parsed from SRAM.
An overworld area from the overworld registry.
An expanded bank with ownership classification.
AddressOwnership ownership
std::string ownership_note
Project-level registry data loaded from the Oracle planning outputs.
std::vector< DungeonEntry > dungeons
std::vector< OverworldArea > overworld_areas
StoryEventGraph story_events
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > all_resource_labels
A contiguous protected ROM region owned by the ASM hack.
A room tag entry from the dispatch table.
A half-open [start, end) range of canonical LoROM SNES addresses.
A custom SRAM variable definition.
std::vector< ProtectedRegion > regions