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));
76 const std::string& configured_path, absl::string_view field_name) {
77 namespace fs = std::filesystem;
78 if (configured_path.empty() ||
79 configured_path.find(
'\\') != std::string::npos ||
80 configured_path.find(
':') != std::string::npos ||
81 configured_path.find(
'\0') != std::string::npos) {
82 return absl::InvalidArgumentError(absl::StrFormat(
83 "%s must be a non-empty portable project-relative path", field_name));
86 const fs::path path(configured_path);
87 if (path.is_absolute() || path.has_root_name() || path.has_root_directory() ||
88 !path.has_filename()) {
89 return absl::InvalidArgumentError(
90 absl::StrFormat(
"%s must be a project-relative file path", field_name));
92 for (
const fs::path& component : path) {
93 if (component ==
"." || component ==
"..") {
94 return absl::InvalidArgumentError(absl::StrFormat(
95 "%s may not contain '.' or '..' components", field_name));
98 if (path.lexically_normal().generic_string() != configured_path) {
99 return absl::InvalidArgumentError(absl::StrFormat(
100 "%s must be a normalized project-relative path", field_name));
102 return absl::OkStatus();
124 if (address >= 0x800000 && address <= 0xFFFFFF) {
145 const std::string& field,
148 return absl::InvalidArgumentError(
149 absl::StrFormat(
"%s must be a hexadecimal string", field));
152 const std::string input = value.
get<std::string>();
153 size_t digits_begin = 0;
154 if (!input.empty() && input.front() ==
'$') {
156 }
else if (input.size() >= 2 && input[0] ==
'0' &&
157 (input[1] ==
'x' || input[1] ==
'X')) {
160 if (digits_begin == input.size()) {
161 return absl::InvalidArgumentError(
162 absl::StrFormat(
"%s has no hexadecimal digits", field));
164 for (
size_t index = digits_begin; index < input.size(); ++index) {
165 if (!std::isxdigit(
static_cast<unsigned char>(input[index]))) {
166 return absl::InvalidArgumentError(absl::StrFormat(
167 "%s contains an invalid hexadecimal value '%s'", field, input));
172 const uint64_t parsed =
173 std::stoull(input.substr(digits_begin),
nullptr, 16);
174 if (parsed > maximum) {
175 return absl::InvalidArgumentError(
176 absl::StrFormat(
"%s value '%s' exceeds 0x%X", field, input, maximum));
178 return static_cast<uint32_t
>(parsed);
179 }
catch (
const std::exception& exc) {
180 return absl::InvalidArgumentError(
181 absl::StrFormat(
"Invalid hexadecimal value for %s ('%s'): %s", field,
187 if (!root.
contains(
"manifest_version")) {
191 const Json& value = root[
"manifest_version"];
192 uint64_t version = 0;
193 if (value.is_number_unsigned()) {
194 version = value.
get<uint64_t>();
195 }
else if (value.is_number_integer()) {
196 const int64_t signed_version = value.
get<int64_t>();
197 if (signed_version < 0) {
198 return absl::InvalidArgumentError(
199 "manifest_version must be a non-negative integer");
201 version =
static_cast<uint64_t
>(signed_version);
203 return absl::InvalidArgumentError(
204 "manifest_version must be a non-negative integer");
207 if (version >
static_cast<uint64_t
>(std::numeric_limits<int>::max())) {
208 return absl::InvalidArgumentError(
"manifest_version is too large");
210 return static_cast<int>(version);
214 absl::string_view field,
217 if (value.is_number_unsigned()) {
218 parsed = value.
get<uint64_t>();
219 }
else if (value.is_number_integer()) {
220 const int64_t signed_value = value.
get<int64_t>();
221 if (signed_value < 0) {
222 return absl::InvalidArgumentError(
223 absl::StrFormat(
"%s must be a non-negative integer", field));
225 parsed =
static_cast<uint64_t
>(signed_value);
227 return absl::InvalidArgumentError(
228 absl::StrFormat(
"%s must be a non-negative integer", field));
230 if (parsed > maximum ||
231 parsed >
static_cast<uint64_t
>(std::numeric_limits<int>::max())) {
232 return absl::InvalidArgumentError(
233 absl::StrFormat(
"%s is too large", field));
235 return static_cast<int>(parsed);
239 const std::string& key,
240 const std::string& path) {
241 if (!
object.contains(key)) {
242 return absl::InvalidArgumentError(
243 absl::StrFormat(
"%s.%s is required", path, key));
246 uint32_t address = 0;
250 const uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
251 if (bank == 0x7E || bank == 0x7F) {
252 return absl::InvalidArgumentError(absl::StrFormat(
253 "%s.%s must not use SNES WRAM bank 0x%02X", path, key, bank));
255 if ((address & 0xFFFF) < 0x8000) {
256 return absl::InvalidArgumentError(absl::StrFormat(
257 "%s.%s must be a mapped LoROM address (low word >= 0x8000)", path,
261 return absl::InvalidArgumentError(absl::StrFormat(
262 "%s.%s is not convertible to a canonical LoROM address", path, key));
268 const std::string& path) {
269 constexpr const char* kField =
"pointer_count";
270 constexpr uint32_t kMaxDungeonPointerCount = 296;
271 if (!
object.contains(kField)) {
272 return absl::InvalidArgumentError(
273 absl::StrFormat(
"%s.%s is required", path, kField));
276 const Json& value =
object[kField];
278 if (value.is_number_unsigned()) {
279 count = value.
get<uint64_t>();
280 }
else if (value.is_number_integer()) {
281 const int64_t signed_count = value.
get<int64_t>();
282 if (signed_count <= 0) {
283 return absl::InvalidArgumentError(
284 absl::StrFormat(
"%s.%s must be greater than zero", path, kField));
286 count =
static_cast<uint64_t
>(signed_count);
288 return absl::InvalidArgumentError(
289 absl::StrFormat(
"%s.%s must be an integer", path, kField));
292 if (count == 0 || count > kMaxDungeonPointerCount) {
293 return absl::InvalidArgumentError(absl::StrFormat(
294 "%s.%s must be in [1, %u]", path, kField, kMaxDungeonPointerCount));
296 return static_cast<uint32_t
>(count);
300 const Json&
object,
const std::string& path) {
301 constexpr const char* kField =
"pointer_encoding";
302 if (!
object.contains(kField) || !
object[kField].is_string()) {
303 return absl::InvalidArgumentError(
304 absl::StrFormat(
"%s.%s must be 'long24' or 'bank16'", path, kField));
306 const std::string value =
object[kField].get<std::string>();
307 if (value ==
"long24") {
310 if (value ==
"bank16") {
313 return absl::InvalidArgumentError(
314 absl::StrFormat(
"%s.%s has unknown encoding '%s'", path, kField, value));
318 const Json&
object,
const std::string& path) {
319 constexpr const char* kField =
"strategy";
320 if (!
object.contains(kField) || !
object[kField].is_string()) {
321 return absl::InvalidArgumentError(absl::StrFormat(
322 "%s.%s must be 'copy_on_write' or 'repack_all'", path, kField));
324 const std::string value =
object[kField].get<std::string>();
325 if (value ==
"copy_on_write") {
328 if (value ==
"repack_all") {
331 return absl::InvalidArgumentError(
332 absl::StrFormat(
"%s.%s has unknown strategy '%s'", path, kField, value));
342 const std::string& description) {
343 std::sort(ranges.begin(), ranges.end(),
345 return lhs.start < rhs.start ||
346 (lhs.start == rhs.start && lhs.end < rhs.end);
348 if (ranges.empty()) {
349 return absl::OkStatus();
353 for (
size_t index = 1; index < ranges.size(); ++index) {
356 return absl::InvalidArgumentError(
357 absl::StrFormat(
"%s overlap: %s conflicts with %s", description,
360 if (current.
end > active.
end) {
364 return absl::OkStatus();
368 const Json&
object,
const std::string& key,
const std::string& path) {
369 const std::string field_path = path +
"." + key;
370 if (!
object.contains(key) || !
object[key].is_array() ||
object[key].empty()) {
371 return absl::InvalidArgumentError(
372 absl::StrFormat(
"%s must be a non-empty array", field_path));
375 std::vector<SnesAddressRange> ranges;
376 std::vector<NamedPcRange> pc_ranges;
377 ranges.reserve(
object[key].size());
378 pc_ranges.reserve(
object[key].size());
380 for (
const Json& range_json :
object[key]) {
381 const std::string range_path =
382 absl::StrFormat(
"%s[%zu]", field_path, index);
383 if (!range_json.is_object()) {
384 return absl::InvalidArgumentError(
385 absl::StrFormat(
"%s must be an object", range_path));
395 if (range.
end <= range.
start || pc_end <= pc_start) {
396 return absl::InvalidArgumentError(absl::StrFormat(
397 "%s must be a non-empty half-open range with end > start",
401 ranges.push_back(range);
402 pc_ranges.push_back({pc_start, pc_end, range_path});
407 std::sort(ranges.begin(), ranges.end(),
409 return lhs.start < rhs.start;
415 const Json& root,
int manifest_version) {
416 constexpr const char* kSection =
"editor_managed_regions";
418 return std::vector<SnesAddressRange>{};
420 if (manifest_version < 3) {
421 return absl::InvalidArgumentError(
422 "editor_managed_regions requires manifest_version 3 or newer");
425 const Json& section = root[kSection];
427 return absl::InvalidArgumentError(
428 "editor_managed_regions must be a non-empty object");
439 const std::string& key,
440 const std::string& path,
441 bool require_mapped_lorom) {
442 if (require_mapped_lorom) {
445 if (!
object.contains(key)) {
446 return absl::InvalidArgumentError(
447 absl::StrFormat(
"%s.%s is required", path, key));
450 uint32_t address = 0;
457 const Json& root,
int manifest_version) {
459 constexpr const char* kSection =
"protected_regions";
464 const Json& section = root[kSection];
466 return absl::InvalidArgumentError(
"protected_regions must be an object");
469 return absl::InvalidArgumentError(
470 "protected_regions.regions must be an array");
474 parsed.
regions.reserve(section[
"regions"].size());
479 const bool require_mapped_lorom = manifest_version >= 3;
481 for (
const Json& region_json : section[
"regions"]) {
482 const std::string path =
483 absl::StrFormat(
"protected_regions.regions[%zu]", index);
484 if (!region_json.is_object()) {
485 return absl::InvalidArgumentError(
486 absl::StrFormat(
"%s must be an object", path));
492 require_mapped_lorom));
494 require_mapped_lorom));
497 return absl::InvalidArgumentError(absl::StrFormat(
498 "%s must be a non-empty half-open range with end > start", path));
501 region.
hook_count = region_json.value(
"hook_count", 0);
502 region.module = region_json.value(
"module",
"");
503 parsed.
regions.push_back(std::move(region));
511 return lhs.start < rhs.start;
524 const std::string path = absl::StrFormat(
"dungeon_stream_regions.%s",
526 if (!
json.is_object()) {
527 return absl::InvalidArgumentError(
528 absl::StrFormat(
"%s must be an object", path));
543 if (!
json.contains(
"pointer_bank")) {
544 return absl::InvalidArgumentError(
545 absl::StrFormat(
"%s.pointer_bank is required for bank16", path));
554 if (bank == 0x7E || bank == 0x7F) {
555 return absl::InvalidArgumentError(absl::StrFormat(
556 "%s.pointer_bank must not use SNES WRAM bank 0x%02X", path, bank));
560 const uint32_t bank_start_snes = (bank << 16) | 0x8000;
561 const uint32_t bank_start_pc =
SnesToPc(bank_start_snes);
562 const uint64_t bank_end_pc =
static_cast<uint64_t
>(bank_start_pc) + 0x8000;
564 if (
SnesToPc(range.start) < bank_start_pc ||
565 SnesToPc(range.end) > bank_end_pc) {
566 return absl::InvalidArgumentError(absl::StrFormat(
567 "%s.data_regions must stay within pointer_bank 0x%02X for "
572 }
else if (
json.contains(
"pointer_bank")) {
573 return absl::InvalidArgumentError(
574 absl::StrFormat(
"%s.pointer_bank is not allowed for long24", path));
578 const bool contained =
581 return PcRangeContains(data, allocation);
584 return absl::InvalidArgumentError(absl::StrFormat(
585 "%s allocation range [0x%06X, 0x%06X) is not fully contained in "
587 path, allocation.start, allocation.end));
591 const uint64_t pointer_width =
594 const uint64_t table_size =
596 const uint64_t table_end_pc = table_start_pc + table_size;
599 return absl::InvalidArgumentError(absl::StrFormat(
600 "%s pointer table extends beyond canonical LoROM address space", path));
603 const uint64_t runtime_table_end =
604 static_cast<uint64_t
>(layout.
pointer_table & 0xFFFFu) + table_size;
605 if (runtime_table_end > 0x10000u) {
606 return absl::InvalidArgumentError(absl::StrFormat(
607 "%s bank16 pointer table crosses its runtime CPU bank", path));
614absl::StatusOr<std::unordered_map<DungeonStreamType, DungeonStreamLayout>>
616 std::unordered_map<DungeonStreamType, DungeonStreamLayout> layouts;
617 if (!root.
contains(
"dungeon_stream_regions")) {
621 const Json& section = root[
"dungeon_stream_regions"];
623 return absl::InvalidArgumentError(
624 "dungeon_stream_regions must be a non-empty object");
627 constexpr std::array<std::pair<const char*, DungeonStreamType>, 3> kStreams =
631 for (
const auto& item : section.
items()) {
632 const auto& key = item.key();
634 for (
const auto& entry : kStreams) {
635 if (key == entry.first) {
641 return absl::InvalidArgumentError(absl::StrFormat(
642 "dungeon_stream_regions contains unknown stream '%s'", key));
646 std::vector<NamedPcRange> occupied_ranges;
647 std::vector<NamedPcRange> allocation_ranges;
648 for (
const auto& [key, stream] : kStreams) {
656 const uint64_t pointer_width =
659 const uint64_t table_end_pc =
662 occupied_ranges.push_back({
static_cast<uint32_t
>(table_start_pc),
663 static_cast<uint32_t
>(table_end_pc),
664 absl::StrFormat(
"%s.pointer_table", key)});
666 for (
size_t index = 0; index < layout.
data_regions.size(); ++index) {
668 occupied_ranges.push_back(
670 absl::StrFormat(
"%s.data_regions[%zu]", key, index)});
674 allocation_ranges.push_back(
676 absl::StrFormat(
"%s.allocation_regions[%zu]", key, index)});
678 layouts.emplace(stream, std::move(layout));
682 "dungeon stream pointer/data ranges"));
684 "dungeon stream allocation ranges"));
713 std::ifstream file(filepath);
714 if (!file.is_open()) {
715 return absl::NotFoundError(
"Could not open manifest: " + filepath);
717 std::stringstream buffer;
718 buffer << file.rdbuf();
728 }
catch (
const std::exception& exc) {
729 return absl::InvalidArgumentError(
730 std::string(
"Failed to parse manifest JSON: ") + exc.what());
734 return absl::InvalidArgumentError(
"Hack manifest root must be an object");
744 std::unordered_map<DungeonStreamType, DungeonStreamLayout>
745 dungeon_stream_layouts;
753 if (root.
contains(
"build_pipeline")) {
754 auto& pipeline = root[
"build_pipeline"];
763 if (root.
contains(
"owned_banks") && root[
"owned_banks"].
contains(
"banks")) {
764 for (
auto& bank_json : root[
"owned_banks"][
"banks"]) {
766 uint32_t bank_u32 = 0;
768 ParseHexAddress(bank_json.value(
"bank",
"0x00")));
769 bank.
bank =
static_cast<uint8_t
>(bank_u32 & 0xFF);
770 if (bank.
bank >= 0x80) {
774 "bank_start",
"0x000000")));
776 "bank_end",
"0x000000")));
780 "ownership",
"asm_owned")));
788 for (
auto& tag_json : root[
"room_tags"][
"tags"]) {
790 uint32_t tag_id_u32 = 0;
792 ParseHexAddress(tag_json.value(
"tag_id",
"0x00")));
793 tag.
tag_id =
static_cast<uint8_t
>(tag_id_u32 & 0xFF);
795 ParseHexAddress(tag_json.value(
"address",
"0x000000")));
797 tag.
name = tag_json.value(
"name",
"");
798 tag.
purpose = tag_json.value(
"purpose",
"");
799 tag.
source = tag_json.value(
"source",
"");
801 tag.
enabled = tag_json.value(
"enabled",
true);
808 if (root.
contains(
"feature_flags") &&
809 root[
"feature_flags"].
contains(
"flags")) {
810 for (
auto& flag_json : root[
"feature_flags"][
"flags"]) {
812 flag.
name = flag_json.value(
"name",
"");
813 flag.
value = flag_json.value(
"value", 0);
814 flag.
enabled = flag_json.value(
"enabled",
false);
815 flag.
source = flag_json.value(
"source",
"");
823 for (
auto& var_json : root[
"sram"][
"variables"]) {
825 var.
name = var_json.value(
"name",
"");
827 ParseHexAddress(var_json.value(
"address",
"0x000000")));
828 var.
purpose = var_json.value(
"purpose",
"");
836 auto& msg = root[
"messages"];
837 if (!msg.is_object()) {
838 return absl::InvalidArgumentError(
"messages must be an object");
840 if (msg.contains(
"hook_address") && msg[
"hook_address"].is_string()) {
842 ParseHexAddress(msg[
"hook_address"].get<std::string>()));
846 if (msg.contains(
"data_start")) {
848 ParseHexAddress(msg.value(
"data_start",
"0x000000")));
852 if (msg.contains(
"data_end")) {
854 ParseHexAddress(msg.value(
"data_end",
"0x000000")));
857 if (msg.contains(
"vanilla_count")) {
860 ParseBoundedNonnegativeInteger(
861 msg[
"vanilla_count"],
"messages.vanilla_count",
862 static_cast<uint64_t
>(std::numeric_limits<int>::max())));
866 if (msg.contains(
"expanded_range")) {
867 auto& expanded = msg[
"expanded_range"];
868 if (!expanded.is_object()) {
869 return absl::InvalidArgumentError(
870 "messages.expanded_range must be an object");
872 uint32_t first_id = 0;
873 uint32_t last_id = 0;
875 ParseHexAddress(expanded.value(
"first",
"0x000")));
877 ParseHexAddress(expanded.value(
"last",
"0x000")));
879 static_cast<uint16_t
>(first_id & 0xFFFF);
881 static_cast<uint16_t
>(last_id & 0xFFFF);
882 if (expanded.contains(
"count")) {
883 constexpr uint64_t kMaximumExpandedMessageCount =
884 static_cast<uint64_t
>(std::numeric_limits<uint16_t>::max()) + 1;
886 ParseBoundedNonnegativeInteger(
887 expanded[
"count"],
"messages.expanded_range.count",
888 kMaximumExpandedMessageCount));
893 if (msg.contains(
"source")) {
894 const auto& source = msg[
"source"];
895 if (!source.is_object()) {
896 return absl::InvalidArgumentError(
"messages.source must be an object");
898 constexpr std::array<absl::string_view, 4> kSourceKeys = {
899 "format",
"version",
"canonical_bundle_path",
900 "generated_asm_include_path"};
901 for (
const auto& item : source.items()) {
902 if (std::find(kSourceKeys.begin(), kSourceKeys.end(), item.key()) ==
904 return absl::InvalidArgumentError(absl::StrFormat(
905 "messages.source contains unknown field '%s'", item.key()));
908 for (absl::string_view key : kSourceKeys) {
909 if (!source.contains(key)) {
910 return absl::InvalidArgumentError(
911 absl::StrFormat(
"messages.source.%s is required", key));
914 if (!source[
"format"].is_string() ||
915 source[
"format"].get<std::string>() !=
"yaze-message-bundle") {
916 return absl::InvalidArgumentError(
917 "messages.source.format must be 'yaze-message-bundle'");
919 int source_version = 0;
920 auto source_version_or = ParseBoundedNonnegativeInteger(
921 source[
"version"],
"messages.source.version",
922 static_cast<uint64_t
>(std::numeric_limits<int>::max()));
923 if (!source_version_or.ok() || *source_version_or != 1) {
924 return absl::InvalidArgumentError(
925 "messages.source.version must be integer 1");
927 source_version = *source_version_or;
928 if (!source[
"canonical_bundle_path"].is_string() ||
929 source[
"canonical_bundle_path"].get<std::string>().empty()) {
930 return absl::InvalidArgumentError(
931 "messages.source.canonical_bundle_path must be a non-empty "
934 if (!source[
"generated_asm_include_path"].is_string() ||
935 source[
"generated_asm_include_path"].get<std::string>().empty()) {
936 return absl::InvalidArgumentError(
937 "messages.source.generated_asm_include_path must be a non-empty "
941 .
format = source[
"format"].get<std::string>(),
942 .version = source_version,
943 .canonical_bundle_path =
944 source[
"canonical_bundle_path"].get<std::string>(),
945 .generated_asm_include_path =
946 source[
"generated_asm_include_path"].get<std::string>(),
952 if (root.
contains(
"minecart_tracks")) {
953 const auto& minecart_tracks = root[
"minecart_tracks"];
954 if (!minecart_tracks.is_object()) {
955 return absl::InvalidArgumentError(
"minecart_tracks must be an object");
957 constexpr std::array<absl::string_view, 1> kMinecartTrackKeys = {
"source"};
958 for (
const auto& item : minecart_tracks.items()) {
959 if (std::find(kMinecartTrackKeys.begin(), kMinecartTrackKeys.end(),
960 item.key()) == kMinecartTrackKeys.end()) {
961 return absl::InvalidArgumentError(absl::StrFormat(
962 "minecart_tracks contains unknown field '%s'", item.key()));
965 if (!minecart_tracks.contains(
"source")) {
966 return absl::InvalidArgumentError(
967 "minecart_tracks.source is required when minecart_tracks is present");
970 const auto& source = minecart_tracks[
"source"];
971 if (!source.is_object()) {
972 return absl::InvalidArgumentError(
973 "minecart_tracks.source must be an object");
975 constexpr std::array<absl::string_view, 3> kSourceKeys = {
976 "format",
"version",
"path"};
977 for (
const auto& item : source.items()) {
978 if (std::find(kSourceKeys.begin(), kSourceKeys.end(), item.key()) ==
980 return absl::InvalidArgumentError(absl::StrFormat(
981 "minecart_tracks.source contains unknown field '%s'", item.key()));
984 for (absl::string_view key : kSourceKeys) {
985 if (!source.contains(key)) {
986 return absl::InvalidArgumentError(
987 absl::StrFormat(
"minecart_tracks.source.%s is required", key));
990 if (!source[
"format"].is_string() ||
991 source[
"format"].get<std::string>() !=
"yaze-minecart-track-table") {
992 return absl::InvalidArgumentError(
993 "minecart_tracks.source.format must be "
994 "'yaze-minecart-track-table'");
996 const auto source_version_or = ParseBoundedNonnegativeInteger(
997 source[
"version"],
"minecart_tracks.source.version",
998 static_cast<uint64_t
>(std::numeric_limits<int>::max()));
999 if (!source_version_or.ok() || *source_version_or != 1) {
1000 return absl::InvalidArgumentError(
1001 "minecart_tracks.source.version must be integer 1");
1003 if (!source[
"path"].is_string()) {
1004 return absl::InvalidArgumentError(
1005 "minecart_tracks.source.path must be a string");
1007 const std::string source_path = source[
"path"].get<std::string>();
1009 source_path,
"minecart_tracks.source.path"));
1011 .
format = source[
"format"].get<std::string>(),
1012 .version = *source_version_or,
1013 .path = source_path,
1022 return absl::OkStatus();
1029 address = NormalizeSnesAddress(address);
1041 const uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
1044 return bank_it->second.ownership;
1058 uint32_t address)
const {
1059 address = NormalizeSnesAddress(address);
1063 const auto upper = std::upper_bound(
1066 return addr < region.start;
1069 for (
auto iter = std::make_reverse_iterator(upper);
1071 if (address >= iter->start && address < iter->end) {
1083 address = NormalizeSnesAddress(address);
1084 const auto upper = std::upper_bound(
1087 return addr < region.start;
1093 return region.
start <= address && address < region.
end;
1097 uint8_t bank)
const {
1103 return std::nullopt;
1104 return iter->second.ownership;
1111 return iter->second.name;
1117 return std::nullopt;
1118 return iter->second;
1125 return iter->second.enabled;
1129 const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
const {
1130 std::vector<WriteConflict> conflicts;
1135 for (
const auto& range : ranges) {
1136 const uint32_t start = NormalizeSnesAddress(range.first);
1137 const uint32_t end = NormalizeSnesAddress(range.second);
1145 std::vector<uint32_t> boundaries = {start, end};
1146 const uint64_t first_bank_boundary =
1147 (
static_cast<uint64_t
>(start) & 0xFF0000u) + 0x010000u;
1148 for (uint64_t boundary = first_bank_boundary; boundary < end;
1149 boundary += 0x010000u) {
1150 boundaries.push_back(
static_cast<uint32_t
>(boundary));
1152 auto add_range_boundaries = [start, end, &boundaries](
const auto& regions) {
1153 for (
const auto& region : regions) {
1154 if (region.start < end && start < region.end) {
1155 boundaries.push_back(std::max(start, region.start));
1156 boundaries.push_back(std::min(end, region.end));
1163 std::sort(boundaries.begin(), boundaries.end());
1164 boundaries.erase(std::unique(boundaries.begin(), boundaries.end()),
1167 bool previous_was_conflict =
false;
1169 std::string previous_module;
1170 for (
size_t index = 0; index + 1 < boundaries.size(); ++index) {
1171 const uint32_t segment_start = boundaries[index];
1172 if (segment_start >= boundaries[index + 1]) {
1177 if (!IsAsmOwned(ownership)) {
1178 previous_was_conflict =
false;
1186 protected_region !=
nullptr) {
1187 module = protected_region->module;
1190 const uint8_t bank =
static_cast<uint8_t
>((segment_start >> 16) & 0xFF);
1193 module = bank_it->second.ownership_note;
1197 if (!previous_was_conflict || ownership != previous_ownership ||
1198 module != previous_module) {
1199 conflicts.push_back({segment_start, ownership,
module});
1201 previous_was_conflict =
true;
1202 previous_ownership = ownership;
1203 previous_module = std::move(module);
1211 const std::vector<std::pair<uint32_t, uint32_t>>& pc_ranges)
const {
1216 std::vector<std::pair<uint32_t, uint32_t>> snes_ranges;
1217 snes_ranges.reserve(pc_ranges.size());
1219 for (
const auto& range : pc_ranges) {
1220 uint32_t pc_start = range.first;
1221 const uint32_t pc_end = range.second;
1222 if (pc_end <= pc_start) {
1228 while (pc_start < pc_end) {
1229 const uint32_t next_boundary = (pc_start & ~0x7FFFu) + 0x8000u;
1230 const uint32_t seg_end = std::min(pc_end, next_boundary);
1231 const uint32_t seg_len = seg_end - pc_start;
1232 const uint32_t snes_start =
PcToSnes(pc_start);
1233 const uint32_t snes_end = snes_start + seg_len;
1234 snes_ranges.emplace_back(snes_start, snes_end);
1246 return iter->second.name;
1265 namespace fs = std::filesystem;
1268 fs::path base(code_folder);
1271 fs::path planning = base /
"Docs" /
"Dev" /
"Planning";
1274 fs::path dungeons_path = planning /
"dungeons.json";
1275 if (fs::exists(dungeons_path)) {
1276 std::ifstream file(dungeons_path);
1277 if (file.is_open()) {
1278 std::stringstream buffer;
1279 buffer << file.rdbuf();
1283 for (
const auto& dj : root[
"dungeons"]) {
1285 entry.
id = dj.value(
"id",
"");
1286 entry.
name = dj.value(
"name",
"");
1290 if (dj.contains(
"rooms") && dj[
"rooms"].is_array()) {
1291 for (
const auto& rj : dj[
"rooms"]) {
1293 std::string id_str = rj.value(
"id",
"0x00");
1294 auto parsed = ParseHexAddress(id_str);
1295 room.
id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
1296 room.
name = rj.value(
"name",
"");
1297 room.
floor = rj.value(
"floor",
"");
1299 rj.contains(
"grid_row") && rj.contains(
"grid_col");
1300 room.
grid_row = rj.value(
"grid_row", 0);
1301 room.
grid_col = rj.value(
"grid_col", 0);
1302 room.
type = rj.value(
"type",
"normal");
1303 room.
palette = rj.value(
"palette", 0);
1304 room.
blockset = rj.value(
"blockset", 0);
1305 room.
spriteset = rj.value(
"spriteset", 0);
1306 room.
tag1 =
static_cast<uint8_t
>(rj.value(
"tag1", 0));
1307 room.
tag2 =
static_cast<uint8_t
>(rj.value(
"tag2", 0));
1308 entry.
rooms.push_back(std::move(room));
1313 if (dj.contains(
"stairs") && dj[
"stairs"].is_array()) {
1314 for (
const auto& sj : dj[
"stairs"]) {
1316 std::string from_str = sj.value(
"from",
"0x00");
1317 std::string to_str = sj.value(
"to",
"0x00");
1318 auto from_parsed = ParseHexAddress(from_str);
1319 auto to_parsed = ParseHexAddress(to_str);
1321 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1323 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1324 conn.
label = sj.value(
"label",
"");
1325 entry.
stairs.push_back(std::move(conn));
1330 if (dj.contains(
"holewarps") && dj[
"holewarps"].is_array()) {
1331 for (
const auto& hj : dj[
"holewarps"]) {
1333 std::string from_str = hj.value(
"from",
"0x00");
1334 std::string to_str = hj.value(
"to",
"0x00");
1335 auto from_parsed = ParseHexAddress(from_str);
1336 auto to_parsed = ParseHexAddress(to_str);
1338 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1340 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1341 conn.
label = hj.value(
"label",
"");
1342 entry.
holewarps.push_back(std::move(conn));
1347 if (dj.contains(
"doors") && dj[
"doors"].is_array()) {
1348 for (
const auto& doorj : dj[
"doors"]) {
1350 std::string from_str = doorj.value(
"from",
"0x00");
1351 std::string to_str = doorj.value(
"to",
"0x00");
1352 auto from_parsed = ParseHexAddress(from_str);
1353 auto to_parsed = ParseHexAddress(to_str);
1355 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1357 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1358 conn.
label = doorj.value(
"label",
"");
1359 conn.
direction = doorj.value(
"direction",
"");
1360 entry.
doors.push_back(std::move(conn));
1367 }
catch (
const std::exception& exc) {
1368 LOG_WARN(
"HackManifest",
"Failed to parse dungeons.json: %s",
1375 fs::path overworld_path = planning /
"overworld.json";
1376 if (fs::exists(overworld_path)) {
1377 std::ifstream file(overworld_path);
1378 if (file.is_open()) {
1379 std::stringstream buffer;
1380 buffer << file.rdbuf();
1384 for (
const auto& aj : root[
"areas"]) {
1386 std::string id_str = aj.value(
"area_id",
"0x00");
1387 auto parsed = ParseHexAddress(id_str);
1388 area.
area_id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
1389 area.
name = aj.value(
"name",
"");
1390 area.
world = aj.value(
"world",
"");
1391 area.
grid_row = aj.value(
"grid_row", 0);
1392 area.
grid_col = aj.value(
"grid_col", 0);
1396 }
catch (
const std::exception& exc) {
1397 LOG_WARN(
"HackManifest",
"Failed to parse overworld.json: %s",
1406 fs::path unified_path = planning /
"oracle_resource_labels.json";
1407 fs::path legacy_path = planning /
"oracle_room_labels.json";
1409 auto normalize_label_id = [](
const std::string& raw) -> std::string {
1412 std::string s = raw;
1413 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.front()))) {
1416 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.back()))) {
1424 if (s.size() >= 2 && s[0] ==
'0' && (s[1] ==
'x' || s[1] ==
'X')) {
1434 const unsigned long value = std::stoul(s, &idx, base);
1435 if (idx != s.size()) {
1438 if (value >
static_cast<unsigned long>(std::numeric_limits<int>::max())) {
1441 return std::to_string(
static_cast<int>(value));
1447 if (fs::exists(unified_path)) {
1448 std::ifstream file(unified_path);
1449 if (file.is_open()) {
1450 std::stringstream buffer;
1451 buffer << file.rdbuf();
1455 const std::vector<std::string> label_types = {
1456 "room",
"sprite",
"item",
"entrance",
"overworld_map",
"music"};
1457 for (
const auto& type_key : label_types) {
1459 for (
const auto& [key, value] : root[type_key].
items()) {
1460 if (value.is_string()) {
1461 const std::string normalized_key = normalize_label_id(key);
1464 value.get<std::string>();
1469 }
catch (
const std::exception& exc) {
1471 "Failed to parse oracle_resource_labels.json: %s", exc.what());
1474 }
else if (fs::exists(legacy_path)) {
1476 std::ifstream file(legacy_path);
1477 if (file.is_open()) {
1478 std::stringstream buffer;
1479 buffer << file.rdbuf();
1482 if (root.
contains(
"resource_labels") &&
1483 root[
"resource_labels"].
contains(
"room")) {
1484 for (
const auto& [key, value] :
1485 root[
"resource_labels"][
"room"].
items()) {
1486 const std::string normalized_key = normalize_label_id(key);
1488 value.get<std::string>();
1491 }
catch (
const std::exception& exc) {
1492 LOG_WARN(
"HackManifest",
"Failed to parse oracle_room_labels.json: %s",
1503 size_t dungeon_room_labels = 0;
1505 for (
const auto& room : dungeon.rooms) {
1506 if (room.id >= 0 && !room.name.empty()) {
1509 ++dungeon_room_labels;
1513 if (dungeon_room_labels > 0) {
1515 "Mirrored %zu dungeon room names into project resource labels",
1516 dungeon_room_labels);
1520 fs::path story_events_path = planning /
"story_events.json";
1521 if (fs::exists(story_events_path)) {
1524 if (story_status.ok()) {
1534 LOG_DEBUG(
"HackManifest",
"Loaded story events: %zu nodes, %zu edges",
1538 LOG_WARN(
"HackManifest",
"Failed to load story_events.json: %s",
1539 std::string(story_status.message()).c_str());
1543 size_t total_labels = 0;
1545 total_labels += labels.size();
1551 "Loaded project registry: %zu dungeons, %zu overworld areas, "
1552 "%zu resource labels (%zu types)",
1558 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.
MinecartTrackLayout minecart_track_layout_
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::Status ValidateProjectRelativeSourcePath(const std::string &configured_path, absl::string_view field_name)
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)
absl::StatusOr< int > ParseBoundedNonnegativeInteger(const Json &value, absl::string_view field, uint64_t maximum)
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.
std::optional< Source > source
uint16_t last_expanded_id
uint16_t first_expanded_id
Canonical source metadata for the ASM-owned minecart track table.
std::optional< Source > source
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