13#include "absl/status/statusor.h"
14#include "absl/strings/str_format.h"
26 "44f46bb06f589a39275ff40d35e40a3a5652502a981205eaebb54f4c447a3ee2";
30 for (
size_t i = 0; i < 4; ++i) {
33 "a81832a1890ee3808d233633a5cc4f90cd77edd8ef68781c8c1701dea4c09175"};
37 "412194777d0a23bcff78ea17b498da632466066462a6f0f2dc28b2758fced844"};
44 return "vanilla_safe";
46 return "hook_patched";
52 return "asm_expansion";
63absl::StatusOr<uint32_t> ParseHexAddress(absl::string_view str) {
70 return absl::InvalidArgumentError(
71 absl::StrFormat(
"Invalid hex address '%s'", str));
77 if (str ==
"vanilla_safe")
79 if (str ==
"hook_patched")
81 if (str ==
"asm_owned")
85 if (str ==
"asm_expansion")
91 return absl::InvalidArgumentError(
92 absl::StrFormat(
"Unknown ownership string '%s'", str));
96 const std::string& configured_path, absl::string_view field_name) {
97 namespace fs = std::filesystem;
98 if (configured_path.empty() ||
99 configured_path.find(
'\\') != std::string::npos ||
100 configured_path.find(
':') != std::string::npos ||
101 configured_path.find(
'\0') != std::string::npos) {
102 return absl::InvalidArgumentError(absl::StrFormat(
103 "%s must be a non-empty portable project-relative path", field_name));
106 const fs::path path(configured_path);
107 if (path.is_absolute() || path.has_root_name() || path.has_root_directory() ||
108 !path.has_filename()) {
109 return absl::InvalidArgumentError(
110 absl::StrFormat(
"%s must be a project-relative file path", field_name));
112 for (
const fs::path& component : path) {
113 if (component ==
"." || component ==
"..") {
114 return absl::InvalidArgumentError(absl::StrFormat(
115 "%s may not contain '.' or '..' components", field_name));
118 if (path.lexically_normal().generic_string() != configured_path) {
119 return absl::InvalidArgumentError(absl::StrFormat(
120 "%s must be a normalized project-relative path", field_name));
122 return absl::OkStatus();
144 if (address >= 0x800000 && address <= 0xFFFFFF) {
165 const std::string& field,
168 return absl::InvalidArgumentError(
169 absl::StrFormat(
"%s must be a hexadecimal string", field));
172 const std::string input = value.
get<std::string>();
173 size_t digits_begin = 0;
174 if (!input.empty() && input.front() ==
'$') {
176 }
else if (input.size() >= 2 && input[0] ==
'0' &&
177 (input[1] ==
'x' || input[1] ==
'X')) {
180 if (digits_begin == input.size()) {
181 return absl::InvalidArgumentError(
182 absl::StrFormat(
"%s has no hexadecimal digits", field));
184 for (
size_t index = digits_begin; index < input.size(); ++index) {
185 if (!std::isxdigit(
static_cast<unsigned char>(input[index]))) {
186 return absl::InvalidArgumentError(absl::StrFormat(
187 "%s contains an invalid hexadecimal value '%s'", field, input));
193 return absl::InvalidArgumentError(absl::StrFormat(
194 "%s contains an invalid hexadecimal value '%s'", field, input));
196 if (parsed > maximum) {
197 return absl::InvalidArgumentError(
198 absl::StrFormat(
"%s value '%s' exceeds 0x%X", field, input, maximum));
200 return static_cast<uint32_t
>(parsed);
204 if (!root.
contains(
"manifest_version")) {
208 const Json& value = root[
"manifest_version"];
209 uint64_t version = 0;
210 if (value.is_number_unsigned()) {
211 version = value.
get<uint64_t>();
212 }
else if (value.is_number_integer()) {
213 const int64_t signed_version = value.
get<int64_t>();
214 if (signed_version < 0) {
215 return absl::InvalidArgumentError(
216 "manifest_version must be a non-negative integer");
218 version =
static_cast<uint64_t
>(signed_version);
220 return absl::InvalidArgumentError(
221 "manifest_version must be a non-negative integer");
224 if (version >
static_cast<uint64_t
>(std::numeric_limits<int>::max())) {
225 return absl::InvalidArgumentError(
"manifest_version is too large");
227 return static_cast<int>(version);
231 absl::string_view field,
234 if (value.is_number_unsigned()) {
235 parsed = value.
get<uint64_t>();
236 }
else if (value.is_number_integer()) {
237 const int64_t signed_value = value.
get<int64_t>();
238 if (signed_value < 0) {
239 return absl::InvalidArgumentError(
240 absl::StrFormat(
"%s must be a non-negative integer", field));
242 parsed =
static_cast<uint64_t
>(signed_value);
244 return absl::InvalidArgumentError(
245 absl::StrFormat(
"%s must be a non-negative integer", field));
247 if (parsed > maximum ||
248 parsed >
static_cast<uint64_t
>(std::numeric_limits<int>::max())) {
249 return absl::InvalidArgumentError(
250 absl::StrFormat(
"%s is too large", field));
252 return static_cast<int>(parsed);
256 const std::string& key,
257 const std::string& path) {
258 if (!
object.contains(key)) {
259 return absl::InvalidArgumentError(
260 absl::StrFormat(
"%s.%s is required", path, key));
263 uint32_t address = 0;
267 const uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
268 if (bank == 0x7E || bank == 0x7F) {
269 return absl::InvalidArgumentError(absl::StrFormat(
270 "%s.%s must not use SNES WRAM bank 0x%02X", path, key, bank));
272 if ((address & 0xFFFF) < 0x8000) {
273 return absl::InvalidArgumentError(absl::StrFormat(
274 "%s.%s must be a mapped LoROM address (low word >= 0x8000)", path,
278 return absl::InvalidArgumentError(absl::StrFormat(
279 "%s.%s is not convertible to a canonical LoROM address", path, key));
285 const std::string& path) {
286 constexpr const char* kField =
"pointer_count";
287 constexpr uint32_t kMaxDungeonPointerCount = 296;
288 if (!
object.contains(kField)) {
289 return absl::InvalidArgumentError(
290 absl::StrFormat(
"%s.%s is required", path, kField));
293 const Json& value =
object[kField];
295 if (value.is_number_unsigned()) {
296 count = value.
get<uint64_t>();
297 }
else if (value.is_number_integer()) {
298 const int64_t signed_count = value.
get<int64_t>();
299 if (signed_count <= 0) {
300 return absl::InvalidArgumentError(
301 absl::StrFormat(
"%s.%s must be greater than zero", path, kField));
303 count =
static_cast<uint64_t
>(signed_count);
305 return absl::InvalidArgumentError(
306 absl::StrFormat(
"%s.%s must be an integer", path, kField));
309 if (count == 0 || count > kMaxDungeonPointerCount) {
310 return absl::InvalidArgumentError(absl::StrFormat(
311 "%s.%s must be in [1, %u]", path, kField, kMaxDungeonPointerCount));
313 return static_cast<uint32_t
>(count);
317 const Json&
object,
const std::string& path) {
318 constexpr const char* kField =
"pointer_encoding";
319 if (!
object.contains(kField) || !
object[kField].is_string()) {
320 return absl::InvalidArgumentError(
321 absl::StrFormat(
"%s.%s must be 'long24' or 'bank16'", path, kField));
323 const std::string value =
object[kField].get<std::string>();
324 if (value ==
"long24") {
327 if (value ==
"bank16") {
330 return absl::InvalidArgumentError(
331 absl::StrFormat(
"%s.%s has unknown encoding '%s'", path, kField, value));
335 const Json&
object,
const std::string& path) {
336 constexpr const char* kField =
"strategy";
337 if (!
object.contains(kField) || !
object[kField].is_string()) {
338 return absl::InvalidArgumentError(absl::StrFormat(
339 "%s.%s must be 'copy_on_write' or 'repack_all'", path, kField));
341 const std::string value =
object[kField].get<std::string>();
342 if (value ==
"copy_on_write") {
345 if (value ==
"repack_all") {
348 return absl::InvalidArgumentError(
349 absl::StrFormat(
"%s.%s has unknown strategy '%s'", path, kField, value));
359 const std::string& description) {
360 std::sort(ranges.begin(), ranges.end(),
362 return lhs.start < rhs.start ||
363 (lhs.start == rhs.start && lhs.end < rhs.end);
365 if (ranges.empty()) {
366 return absl::OkStatus();
370 for (
size_t index = 1; index < ranges.size(); ++index) {
373 return absl::InvalidArgumentError(
374 absl::StrFormat(
"%s overlap: %s conflicts with %s", description,
377 if (current.
end > active.
end) {
381 return absl::OkStatus();
385 const Json&
object,
const std::string& key,
const std::string& path) {
386 const std::string field_path = path +
"." + key;
387 if (!
object.contains(key) || !
object[key].is_array() ||
object[key].empty()) {
388 return absl::InvalidArgumentError(
389 absl::StrFormat(
"%s must be a non-empty array", field_path));
392 std::vector<SnesAddressRange> ranges;
393 std::vector<NamedPcRange> pc_ranges;
394 ranges.reserve(
object[key].size());
395 pc_ranges.reserve(
object[key].size());
397 for (
const Json& range_json :
object[key]) {
398 const std::string range_path =
399 absl::StrFormat(
"%s[%zu]", field_path, index);
400 if (!range_json.is_object()) {
401 return absl::InvalidArgumentError(
402 absl::StrFormat(
"%s must be an object", range_path));
412 if (range.
end <= range.
start || pc_end <= pc_start) {
413 return absl::InvalidArgumentError(absl::StrFormat(
414 "%s must be a non-empty half-open range with end > start",
418 ranges.push_back(range);
419 pc_ranges.push_back({pc_start, pc_end, range_path});
424 std::sort(ranges.begin(), ranges.end(),
426 return lhs.start < rhs.start;
432 const Json& root,
int manifest_version) {
433 constexpr const char* kSection =
"editor_managed_regions";
435 return std::vector<SnesAddressRange>{};
437 if (manifest_version < 3) {
438 return absl::InvalidArgumentError(
439 "editor_managed_regions requires manifest_version 3 or newer");
442 const Json& section = root[kSection];
444 return absl::InvalidArgumentError(
445 "editor_managed_regions must be a non-empty object");
456 const std::string& key,
457 const std::string& path,
458 bool require_mapped_lorom) {
459 if (require_mapped_lorom) {
462 if (!
object.contains(key)) {
463 return absl::InvalidArgumentError(
464 absl::StrFormat(
"%s.%s is required", path, key));
467 uint32_t address = 0;
474 const Json& root,
int manifest_version) {
476 constexpr const char* kSection =
"protected_regions";
481 const Json& section = root[kSection];
483 return absl::InvalidArgumentError(
"protected_regions must be an object");
486 return absl::InvalidArgumentError(
487 "protected_regions.regions must be an array");
491 parsed.
regions.reserve(section[
"regions"].size());
496 const bool require_mapped_lorom = manifest_version >= 3;
498 for (
const Json& region_json : section[
"regions"]) {
499 const std::string path =
500 absl::StrFormat(
"protected_regions.regions[%zu]", index);
501 if (!region_json.is_object()) {
502 return absl::InvalidArgumentError(
503 absl::StrFormat(
"%s must be an object", path));
509 require_mapped_lorom));
511 require_mapped_lorom));
514 return absl::InvalidArgumentError(absl::StrFormat(
515 "%s must be a non-empty half-open range with end > start", path));
518 region.
hook_count = region_json.value(
"hook_count", 0);
519 region.module = region_json.value(
"module",
"");
520 parsed.
regions.push_back(std::move(region));
528 return lhs.start < rhs.start;
541 const std::string path = absl::StrFormat(
"dungeon_stream_regions.%s",
543 if (!
json.is_object()) {
544 return absl::InvalidArgumentError(
545 absl::StrFormat(
"%s must be an object", path));
560 if (!
json.contains(
"pointer_bank")) {
561 return absl::InvalidArgumentError(
562 absl::StrFormat(
"%s.pointer_bank is required for bank16", path));
571 if (bank == 0x7E || bank == 0x7F) {
572 return absl::InvalidArgumentError(absl::StrFormat(
573 "%s.pointer_bank must not use SNES WRAM bank 0x%02X", path, bank));
577 const uint32_t bank_start_snes = (bank << 16) | 0x8000;
578 const uint32_t bank_start_pc =
SnesToPc(bank_start_snes);
579 const uint64_t bank_end_pc =
static_cast<uint64_t
>(bank_start_pc) + 0x8000;
581 if (
SnesToPc(range.start) < bank_start_pc ||
582 SnesToPc(range.end) > bank_end_pc) {
583 return absl::InvalidArgumentError(absl::StrFormat(
584 "%s.data_regions must stay within pointer_bank 0x%02X for "
589 }
else if (
json.contains(
"pointer_bank")) {
590 return absl::InvalidArgumentError(
591 absl::StrFormat(
"%s.pointer_bank is not allowed for long24", path));
595 const bool contained =
598 return PcRangeContains(data, allocation);
601 return absl::InvalidArgumentError(absl::StrFormat(
602 "%s allocation range [0x%06X, 0x%06X) is not fully contained in "
604 path, allocation.start, allocation.end));
608 const uint64_t pointer_width =
611 const uint64_t table_size =
613 const uint64_t table_end_pc = table_start_pc + table_size;
616 return absl::InvalidArgumentError(absl::StrFormat(
617 "%s pointer table extends beyond canonical LoROM address space", path));
620 const uint64_t runtime_table_end =
621 static_cast<uint64_t
>(layout.
pointer_table & 0xFFFFu) + table_size;
622 if (runtime_table_end > 0x10000u) {
623 return absl::InvalidArgumentError(absl::StrFormat(
624 "%s bank16 pointer table crosses its runtime CPU bank", path));
631absl::StatusOr<std::unordered_map<DungeonStreamType, DungeonStreamLayout>>
633 std::unordered_map<DungeonStreamType, DungeonStreamLayout> layouts;
634 if (!root.
contains(
"dungeon_stream_regions")) {
638 const Json& section = root[
"dungeon_stream_regions"];
640 return absl::InvalidArgumentError(
641 "dungeon_stream_regions must be a non-empty object");
644 constexpr std::array<std::pair<const char*, DungeonStreamType>, 3> kStreams =
648 for (
const auto& item : section.
items()) {
649 const auto& key = item.key();
651 for (
const auto& entry : kStreams) {
652 if (key == entry.first) {
658 return absl::InvalidArgumentError(absl::StrFormat(
659 "dungeon_stream_regions contains unknown stream '%s'", key));
663 std::vector<NamedPcRange> occupied_ranges;
664 std::vector<NamedPcRange> allocation_ranges;
665 for (
const auto& [key, stream] : kStreams) {
673 const uint64_t pointer_width =
676 const uint64_t table_end_pc =
679 occupied_ranges.push_back({
static_cast<uint32_t
>(table_start_pc),
680 static_cast<uint32_t
>(table_end_pc),
681 absl::StrFormat(
"%s.pointer_table", key)});
683 for (
size_t index = 0; index < layout.
data_regions.size(); ++index) {
685 occupied_ranges.push_back(
687 absl::StrFormat(
"%s.data_regions[%zu]", key, index)});
691 allocation_ranges.push_back(
693 absl::StrFormat(
"%s.allocation_regions[%zu]", key, index)});
695 layouts.emplace(stream, std::move(layout));
699 "dungeon stream pointer/data ranges"));
701 "dungeon stream allocation ranges"));
706 const Json& root,
int manifest_version,
707 const std::unordered_map<DungeonStreamType, DungeonStreamLayout>&
708 dungeon_stream_layouts) {
709 constexpr const char* kSection =
"graphics_sheet_regions";
711 constexpr uint32_t kGraphicsSheetCount = 223;
716 if (manifest_version < 3) {
717 return absl::InvalidArgumentError(
718 "graphics_sheet_regions requires manifest_version 3 or newer");
721 const Json& section = root[kSection];
723 return absl::InvalidArgumentError(
724 "graphics_sheet_regions must be a non-empty object");
726 for (
const auto& item : section.
items()) {
727 if (item.key() !=
"allocation_regions" && item.key() !=
"reserved_sheets" &&
728 item.key() !=
"description") {
729 return absl::InvalidArgumentError(absl::StrFormat(
730 "graphics_sheet_regions contains unknown key '%s'", item.key()));
734 if (section.
contains(
"allocation_regions")) {
741 std::vector<NamedPcRange> ranges;
746 absl::StrFormat(
"%s.allocation_regions[%zu]", kSection, index)});
748 for (
const auto& [stream, stream_layout] : dungeon_stream_layouts) {
749 for (
size_t index = 0; index < stream_layout.data_regions.size();
751 const auto& range = stream_layout.data_regions[index];
754 absl::StrFormat(
"dungeon_stream_regions.%s."
760 std::move(ranges),
"graphics allocation and dungeon stream ranges"));
763 if (section.
contains(
"reserved_sheets")) {
764 const Json& reserved = section[
"reserved_sheets"];
766 return absl::InvalidArgumentError(
767 "graphics_sheet_regions.reserved_sheets must be an array");
770 for (
const Json& value : reserved) {
776 "graphics_sheet_regions.reserved_sheets[%zu]", index),
777 kGraphicsSheetCount - 1));
812 std::ifstream file(filepath);
813 if (!file.is_open()) {
814 return absl::NotFoundError(
"Could not open manifest: " + filepath);
816 std::stringstream buffer;
817 buffer << file.rdbuf();
827 }
catch (
const std::exception& exc) {
828 return absl::InvalidArgumentError(
829 std::string(
"Failed to parse manifest JSON: ") + exc.what());
833 return absl::InvalidArgumentError(
"Hack manifest root must be an object");
836 if (root.
contains(
"lyra_private_reservation")) {
840 const auto& v = root.at(
"lyra_private_reservation");
842 if (!v.is_object() ||
843 v.at(
"kind") !=
"lyra.private_exact_save_producer_reservation.v1" ||
844 v.at(
"scope") !=
"private_candidate_only" ||
845 !v.at(
"production_admission").is_boolean() ||
846 v.at(
"production_admission") !=
false ||
847 v.at(
"producer_transfer").at(
"from") !=
848 "yaze.native_overworld_terrain" ||
849 v.at(
"producer_transfer").at(
"to") !=
850 "oracle.lyra_private_candidate" ||
851 v.at(
"producer_transfer").at(
"native_terrain_policy") !=
853 v.at(
"source_rom_sha256") != expected.source_rom_sha256) {
854 return absl::InvalidArgumentError(
855 "Invalid private Lyra producer transfer");
857 auto exact_numbers = [](
const Json& value,
const auto& expected_values) {
858 if (!value.
is_array() || value.
size() != expected_values.size())
860 for (
size_t i = 0; i < value.
size(); ++i) {
861 if (!value[i].is_number_integer() || value[i].
is_boolean() ||
862 value[i] != expected_values[i])
867 if (!exact_numbers(v.at(
"tile32_id_range"), expected.tile32_id_range) ||
868 !exact_numbers(v.at(
"stream_pc_range"), expected.stream_pc_range) ||
869 !exact_numbers(v.at(
"dictionary_bases_pc"),
870 expected.dictionary_bases_pc) ||
871 !v.at(
"packed_preimages").is_array() ||
872 v.at(
"packed_preimages").size() != 4) {
873 return absl::InvalidArgumentError(
874 "Private Lyra reservation geometry mismatch");
876 for (
size_t i = 0; i < 4; ++i) {
877 const auto& p = v.at(
"packed_preimages")[i];
878 if (!exact_numbers(p.at(
"pc"), expected.packed_preimages[i].pc) ||
879 p.at(
"sha256") != expected.packed_preimages[i].sha256) {
880 return absl::InvalidArgumentError(
881 "Private Lyra packed preimage mismatch");
884 const auto& p = v.at(
"stream_preimage");
885 if (!exact_numbers(p.at(
"pc"), expected.stream_preimage.pc) ||
886 p.at(
"sha256") != expected.stream_preimage.sha256) {
887 return absl::InvalidArgumentError(
888 "Private Lyra stream preimage mismatch");
891 }
catch (
const std::exception&) {
892 return absl::InvalidArgumentError(
"Malformed private Lyra reservation");
903 std::unordered_map<DungeonStreamType, DungeonStreamLayout>
904 dungeon_stream_layouts;
910 dungeon_stream_layouts));
917 if (root.
contains(
"build_pipeline")) {
918 auto& pipeline = root[
"build_pipeline"];
927 if (root.
contains(
"owned_banks") && root[
"owned_banks"].
contains(
"banks")) {
928 for (
auto& bank_json : root[
"owned_banks"][
"banks"]) {
930 uint32_t bank_u32 = 0;
932 ParseHexAddress(bank_json.value(
"bank",
"0x00")));
933 bank.
bank =
static_cast<uint8_t
>(bank_u32 & 0xFF);
934 if (bank.
bank >= 0x80) {
938 "bank_start",
"0x000000")));
940 "bank_end",
"0x000000")));
944 "ownership",
"asm_owned")));
952 for (
auto& tag_json : root[
"room_tags"][
"tags"]) {
954 uint32_t tag_id_u32 = 0;
956 ParseHexAddress(tag_json.value(
"tag_id",
"0x00")));
957 tag.
tag_id =
static_cast<uint8_t
>(tag_id_u32 & 0xFF);
959 ParseHexAddress(tag_json.value(
"address",
"0x000000")));
961 tag.
name = tag_json.value(
"name",
"");
962 tag.
purpose = tag_json.value(
"purpose",
"");
963 tag.
source = tag_json.value(
"source",
"");
965 tag.
enabled = tag_json.value(
"enabled",
true);
972 if (root.
contains(
"feature_flags") &&
973 root[
"feature_flags"].
contains(
"flags")) {
974 for (
auto& flag_json : root[
"feature_flags"][
"flags"]) {
976 flag.
name = flag_json.value(
"name",
"");
977 flag.
value = flag_json.value(
"value", 0);
978 flag.
enabled = flag_json.value(
"enabled",
false);
979 flag.
source = flag_json.value(
"source",
"");
987 for (
auto& var_json : root[
"sram"][
"variables"]) {
989 var.
name = var_json.value(
"name",
"");
991 ParseHexAddress(var_json.value(
"address",
"0x000000")));
992 var.
purpose = var_json.value(
"purpose",
"");
1000 auto& msg = root[
"messages"];
1001 if (!msg.is_object()) {
1002 return absl::InvalidArgumentError(
"messages must be an object");
1004 if (msg.contains(
"hook_address") && msg[
"hook_address"].is_string()) {
1006 ParseHexAddress(msg[
"hook_address"].get<std::string>()));
1010 if (msg.contains(
"data_start")) {
1012 ParseHexAddress(msg.value(
"data_start",
"0x000000")));
1016 if (msg.contains(
"data_end")) {
1018 ParseHexAddress(msg.value(
"data_end",
"0x000000")));
1021 if (msg.contains(
"vanilla_count")) {
1024 ParseBoundedNonnegativeInteger(
1025 msg[
"vanilla_count"],
"messages.vanilla_count",
1026 static_cast<uint64_t
>(std::numeric_limits<int>::max())));
1030 if (msg.contains(
"expanded_range")) {
1031 auto& expanded = msg[
"expanded_range"];
1032 if (!expanded.is_object()) {
1033 return absl::InvalidArgumentError(
1034 "messages.expanded_range must be an object");
1036 uint32_t first_id = 0;
1037 uint32_t last_id = 0;
1039 ParseHexAddress(expanded.value(
"first",
"0x000")));
1041 ParseHexAddress(expanded.value(
"last",
"0x000")));
1043 static_cast<uint16_t
>(first_id & 0xFFFF);
1045 static_cast<uint16_t
>(last_id & 0xFFFF);
1046 if (expanded.contains(
"count")) {
1047 constexpr uint64_t kMaximumExpandedMessageCount =
1048 static_cast<uint64_t
>(std::numeric_limits<uint16_t>::max()) + 1;
1050 ParseBoundedNonnegativeInteger(
1051 expanded[
"count"],
"messages.expanded_range.count",
1052 kMaximumExpandedMessageCount));
1057 if (msg.contains(
"source")) {
1058 const auto& source = msg[
"source"];
1059 if (!source.is_object()) {
1060 return absl::InvalidArgumentError(
"messages.source must be an object");
1062 constexpr std::array<absl::string_view, 4> kSourceKeys = {
1063 "format",
"version",
"canonical_bundle_path",
1064 "generated_asm_include_path"};
1065 for (
const auto& item : source.items()) {
1066 if (std::find(kSourceKeys.begin(), kSourceKeys.end(), item.key()) ==
1067 kSourceKeys.end()) {
1068 return absl::InvalidArgumentError(absl::StrFormat(
1069 "messages.source contains unknown field '%s'", item.key()));
1072 for (absl::string_view key : kSourceKeys) {
1073 if (!source.contains(key)) {
1074 return absl::InvalidArgumentError(
1075 absl::StrFormat(
"messages.source.%s is required", key));
1078 if (!source[
"format"].is_string() ||
1079 source[
"format"].get<std::string>() !=
"yaze-message-bundle") {
1080 return absl::InvalidArgumentError(
1081 "messages.source.format must be 'yaze-message-bundle'");
1083 int source_version = 0;
1084 auto source_version_or = ParseBoundedNonnegativeInteger(
1085 source[
"version"],
"messages.source.version",
1086 static_cast<uint64_t
>(std::numeric_limits<int>::max()));
1087 if (!source_version_or.ok() || *source_version_or != 1) {
1088 return absl::InvalidArgumentError(
1089 "messages.source.version must be integer 1");
1091 source_version = *source_version_or;
1092 if (!source[
"canonical_bundle_path"].is_string() ||
1093 source[
"canonical_bundle_path"].get<std::string>().empty()) {
1094 return absl::InvalidArgumentError(
1095 "messages.source.canonical_bundle_path must be a non-empty "
1098 if (!source[
"generated_asm_include_path"].is_string() ||
1099 source[
"generated_asm_include_path"].get<std::string>().empty()) {
1100 return absl::InvalidArgumentError(
1101 "messages.source.generated_asm_include_path must be a non-empty "
1105 .
format = source[
"format"].get<std::string>(),
1106 .version = source_version,
1107 .canonical_bundle_path =
1108 source[
"canonical_bundle_path"].get<std::string>(),
1109 .generated_asm_include_path =
1110 source[
"generated_asm_include_path"].get<std::string>(),
1116 if (root.
contains(
"minecart_tracks")) {
1117 const auto& minecart_tracks = root[
"minecart_tracks"];
1118 if (!minecart_tracks.is_object()) {
1119 return absl::InvalidArgumentError(
"minecart_tracks must be an object");
1121 constexpr std::array<absl::string_view, 1> kMinecartTrackKeys = {
"source"};
1122 for (
const auto& item : minecart_tracks.items()) {
1123 if (std::find(kMinecartTrackKeys.begin(), kMinecartTrackKeys.end(),
1124 item.key()) == kMinecartTrackKeys.end()) {
1125 return absl::InvalidArgumentError(absl::StrFormat(
1126 "minecart_tracks contains unknown field '%s'", item.key()));
1129 if (!minecart_tracks.contains(
"source")) {
1130 return absl::InvalidArgumentError(
1131 "minecart_tracks.source is required when minecart_tracks is present");
1134 const auto& source = minecart_tracks[
"source"];
1135 if (!source.is_object()) {
1136 return absl::InvalidArgumentError(
1137 "minecart_tracks.source must be an object");
1139 constexpr std::array<absl::string_view, 3> kSourceKeys = {
1140 "format",
"version",
"path"};
1141 for (
const auto& item : source.items()) {
1142 if (std::find(kSourceKeys.begin(), kSourceKeys.end(), item.key()) ==
1143 kSourceKeys.end()) {
1144 return absl::InvalidArgumentError(absl::StrFormat(
1145 "minecart_tracks.source contains unknown field '%s'", item.key()));
1148 for (absl::string_view key : kSourceKeys) {
1149 if (!source.contains(key)) {
1150 return absl::InvalidArgumentError(
1151 absl::StrFormat(
"minecart_tracks.source.%s is required", key));
1154 if (!source[
"format"].is_string() ||
1155 source[
"format"].get<std::string>() !=
"yaze-minecart-track-table") {
1156 return absl::InvalidArgumentError(
1157 "minecart_tracks.source.format must be "
1158 "'yaze-minecart-track-table'");
1160 const auto source_version_or = ParseBoundedNonnegativeInteger(
1161 source[
"version"],
"minecart_tracks.source.version",
1162 static_cast<uint64_t
>(std::numeric_limits<int>::max()));
1163 if (!source_version_or.ok() || *source_version_or != 1) {
1164 return absl::InvalidArgumentError(
1165 "minecart_tracks.source.version must be integer 1");
1167 if (!source[
"path"].is_string()) {
1168 return absl::InvalidArgumentError(
1169 "minecart_tracks.source.path must be a string");
1171 const std::string source_path = source[
"path"].get<std::string>();
1173 source_path,
"minecart_tracks.source.path"));
1175 .
format = source[
"format"].get<std::string>(),
1176 .version = *source_version_or,
1177 .path = source_path,
1187 return absl::OkStatus();
1194 address = NormalizeSnesAddress(address);
1206 const uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
1209 return bank_it->second.ownership;
1223 uint32_t address)
const {
1224 address = NormalizeSnesAddress(address);
1228 const auto upper = std::upper_bound(
1231 return addr < region.start;
1234 for (
auto iter = std::make_reverse_iterator(upper);
1236 if (address >= iter->start && address < iter->end) {
1248 address = NormalizeSnesAddress(address);
1249 const auto upper = std::upper_bound(
1252 return addr < region.start;
1258 return region.
start <= address && address < region.
end;
1262 uint8_t bank)
const {
1268 return std::nullopt;
1269 return iter->second.ownership;
1276 return iter->second.name;
1282 return std::nullopt;
1283 return iter->second;
1290 return iter->second.enabled;
1294 const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
const {
1295 std::vector<WriteConflict> conflicts;
1300 for (
const auto& range : ranges) {
1301 const uint32_t start = NormalizeSnesAddress(range.first);
1302 const uint32_t end = NormalizeSnesAddress(range.second);
1310 std::vector<uint32_t> boundaries = {start, end};
1311 const uint64_t first_bank_boundary =
1312 (
static_cast<uint64_t
>(start) & 0xFF0000u) + 0x010000u;
1313 for (uint64_t boundary = first_bank_boundary; boundary < end;
1314 boundary += 0x010000u) {
1315 boundaries.push_back(
static_cast<uint32_t
>(boundary));
1317 auto add_range_boundaries = [start, end, &boundaries](
const auto& regions) {
1318 for (
const auto& region : regions) {
1319 if (region.start < end && start < region.end) {
1320 boundaries.push_back(std::max(start, region.start));
1321 boundaries.push_back(std::min(end, region.end));
1328 std::sort(boundaries.begin(), boundaries.end());
1329 boundaries.erase(std::unique(boundaries.begin(), boundaries.end()),
1332 bool previous_was_conflict =
false;
1334 std::string previous_module;
1335 for (
size_t index = 0; index + 1 < boundaries.size(); ++index) {
1336 const uint32_t segment_start = boundaries[index];
1337 if (segment_start >= boundaries[index + 1]) {
1342 if (!IsAsmOwned(ownership)) {
1343 previous_was_conflict =
false;
1351 protected_region !=
nullptr) {
1352 module = protected_region->module;
1355 const uint8_t bank =
static_cast<uint8_t
>((segment_start >> 16) & 0xFF);
1358 module = bank_it->second.ownership_note;
1362 if (!previous_was_conflict || ownership != previous_ownership ||
1363 module != previous_module) {
1364 conflicts.push_back({segment_start, ownership,
module});
1366 previous_was_conflict =
true;
1367 previous_ownership = ownership;
1368 previous_module = std::move(module);
1376 const std::vector<std::pair<uint32_t, uint32_t>>& pc_ranges)
const {
1381 std::vector<std::pair<uint32_t, uint32_t>> snes_ranges;
1382 snes_ranges.reserve(pc_ranges.size());
1384 for (
const auto& range : pc_ranges) {
1385 uint32_t pc_start = range.first;
1386 const uint32_t pc_end = range.second;
1387 if (pc_end <= pc_start) {
1393 while (pc_start < pc_end) {
1394 const uint32_t next_boundary = (pc_start & ~0x7FFFu) + 0x8000u;
1395 const uint32_t seg_end = std::min(pc_end, next_boundary);
1396 const uint32_t seg_len = seg_end - pc_start;
1397 const uint32_t snes_start =
PcToSnes(pc_start);
1398 const uint32_t snes_end = snes_start + seg_len;
1399 snes_ranges.emplace_back(snes_start, snes_end);
1411 return iter->second.name;
1430 namespace fs = std::filesystem;
1433 fs::path base(code_folder);
1436 fs::path planning = base /
"Docs" /
"Dev" /
"Planning";
1439 fs::path dungeons_path = planning /
"dungeons.json";
1440 if (fs::exists(dungeons_path)) {
1441 std::ifstream file(dungeons_path);
1442 if (file.is_open()) {
1443 std::stringstream buffer;
1444 buffer << file.rdbuf();
1448 for (
const auto& dj : root[
"dungeons"]) {
1450 entry.
id = dj.value(
"id",
"");
1451 entry.
name = dj.value(
"name",
"");
1455 if (dj.contains(
"rooms") && dj[
"rooms"].is_array()) {
1456 for (
const auto& rj : dj[
"rooms"]) {
1458 std::string id_str = rj.value(
"id",
"0x00");
1459 auto parsed = ParseHexAddress(id_str);
1460 room.
id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
1461 room.
name = rj.value(
"name",
"");
1462 room.
floor = rj.value(
"floor",
"");
1464 rj.contains(
"grid_row") && rj.contains(
"grid_col");
1465 room.
grid_row = rj.value(
"grid_row", 0);
1466 room.
grid_col = rj.value(
"grid_col", 0);
1467 room.
type = rj.value(
"type",
"normal");
1468 room.
palette = rj.value(
"palette", 0);
1469 room.
blockset = rj.value(
"blockset", 0);
1470 room.
spriteset = rj.value(
"spriteset", 0);
1471 room.
tag1 =
static_cast<uint8_t
>(rj.value(
"tag1", 0));
1472 room.
tag2 =
static_cast<uint8_t
>(rj.value(
"tag2", 0));
1473 entry.
rooms.push_back(std::move(room));
1478 if (dj.contains(
"stairs") && dj[
"stairs"].is_array()) {
1479 for (
const auto& sj : dj[
"stairs"]) {
1481 std::string from_str = sj.value(
"from",
"0x00");
1482 std::string to_str = sj.value(
"to",
"0x00");
1483 auto from_parsed = ParseHexAddress(from_str);
1484 auto to_parsed = ParseHexAddress(to_str);
1486 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1488 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1489 conn.
label = sj.value(
"label",
"");
1490 entry.
stairs.push_back(std::move(conn));
1495 if (dj.contains(
"holewarps") && dj[
"holewarps"].is_array()) {
1496 for (
const auto& hj : dj[
"holewarps"]) {
1498 std::string from_str = hj.value(
"from",
"0x00");
1499 std::string to_str = hj.value(
"to",
"0x00");
1500 auto from_parsed = ParseHexAddress(from_str);
1501 auto to_parsed = ParseHexAddress(to_str);
1503 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1505 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1506 conn.
label = hj.value(
"label",
"");
1507 entry.
holewarps.push_back(std::move(conn));
1512 if (dj.contains(
"doors") && dj[
"doors"].is_array()) {
1513 for (
const auto& doorj : dj[
"doors"]) {
1515 std::string from_str = doorj.value(
"from",
"0x00");
1516 std::string to_str = doorj.value(
"to",
"0x00");
1517 auto from_parsed = ParseHexAddress(from_str);
1518 auto to_parsed = ParseHexAddress(to_str);
1520 from_parsed.ok() ?
static_cast<int>(*from_parsed) : 0;
1522 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
1523 conn.
label = doorj.value(
"label",
"");
1524 conn.
direction = doorj.value(
"direction",
"");
1525 entry.
doors.push_back(std::move(conn));
1532 }
catch (
const std::exception& exc) {
1533 LOG_WARN(
"HackManifest",
"Failed to parse dungeons.json: %s",
1540 fs::path overworld_path = planning /
"overworld.json";
1541 if (fs::exists(overworld_path)) {
1542 std::ifstream file(overworld_path);
1543 if (file.is_open()) {
1544 std::stringstream buffer;
1545 buffer << file.rdbuf();
1549 for (
const auto& aj : root[
"areas"]) {
1551 std::string id_str = aj.value(
"area_id",
"0x00");
1552 auto parsed = ParseHexAddress(id_str);
1553 area.
area_id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
1554 area.
name = aj.value(
"name",
"");
1555 area.
world = aj.value(
"world",
"");
1556 area.
grid_row = aj.value(
"grid_row", 0);
1557 area.
grid_col = aj.value(
"grid_col", 0);
1561 }
catch (
const std::exception& exc) {
1562 LOG_WARN(
"HackManifest",
"Failed to parse overworld.json: %s",
1571 fs::path unified_path = planning /
"oracle_resource_labels.json";
1572 fs::path legacy_path = planning /
"oracle_room_labels.json";
1574 auto normalize_label_id = [](
const std::string& raw) -> std::string {
1577 std::string s = raw;
1578 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.front()))) {
1581 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.back()))) {
1589 if (s.size() >= 2 && s[0] ==
'0' && (s[1] ==
'x' || s[1] ==
'X')) {
1599 const unsigned long value = std::stoul(s, &idx, base);
1600 if (idx != s.size()) {
1603 if (value >
static_cast<unsigned long>(std::numeric_limits<int>::max())) {
1606 return std::to_string(
static_cast<int>(value));
1612 if (fs::exists(unified_path)) {
1613 std::ifstream file(unified_path);
1614 if (file.is_open()) {
1615 std::stringstream buffer;
1616 buffer << file.rdbuf();
1620 const std::vector<std::string> label_types = {
1621 "room",
"sprite",
"item",
"item_receipt",
1622 "entrance",
"overworld_map",
"music"};
1623 for (
const auto& type_key : label_types) {
1625 for (
const auto& [key, value] : root[type_key].
items()) {
1626 if (value.is_string()) {
1627 const std::string normalized_key = normalize_label_id(key);
1630 value.get<std::string>();
1635 }
catch (
const std::exception& exc) {
1637 "Failed to parse oracle_resource_labels.json: %s", exc.what());
1640 }
else if (fs::exists(legacy_path)) {
1642 std::ifstream file(legacy_path);
1643 if (file.is_open()) {
1644 std::stringstream buffer;
1645 buffer << file.rdbuf();
1648 if (root.
contains(
"resource_labels") &&
1649 root[
"resource_labels"].
contains(
"room")) {
1650 for (
const auto& [key, value] :
1651 root[
"resource_labels"][
"room"].
items()) {
1652 const std::string normalized_key = normalize_label_id(key);
1654 value.get<std::string>();
1657 }
catch (
const std::exception& exc) {
1658 LOG_WARN(
"HackManifest",
"Failed to parse oracle_room_labels.json: %s",
1669 size_t dungeon_room_labels = 0;
1671 for (
const auto& room : dungeon.rooms) {
1672 if (room.id >= 0 && !room.name.empty()) {
1675 ++dungeon_room_labels;
1679 if (dungeon_room_labels > 0) {
1681 "Mirrored %zu dungeon room names into project resource labels",
1682 dungeon_room_labels);
1686 fs::path story_events_path = planning /
"story_events.json";
1687 if (fs::exists(story_events_path)) {
1690 if (story_status.ok()) {
1700 LOG_DEBUG(
"HackManifest",
"Loaded story events: %zu nodes, %zu edges",
1704 LOG_WARN(
"HackManifest",
"Failed to load story_events.json: %s",
1705 std::string(story_status.message()).c_str());
1709 size_t total_labels = 0;
1711 total_labels += labels.size();
1717 "Loaded project registry: %zu dungeons, %zu overworld areas, "
1718 "%zu resource labels (%zu types)",
1724 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::optional< LyraPrivateReservation > lyra_private_reservation_
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_
const GraphicsSheetLayout & graphics_sheet_layout() const
Graphics-sheet free space and reserved sheets. Empty when the optional graphics_sheet_regions section...
std::optional< AddressOwnership > GetBankOwnership(uint8_t bank) const
Get the bank ownership for a given bank number.
std::optional< OracleProgressionState > oracle_progression_state_
GraphicsSheetLayout graphics_sheet_layout_
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< GraphicsSheetLayout > ParseGraphicsSheetLayout(const Json &root, int manifest_version, const std::unordered_map< DungeonStreamType, DungeonStreamLayout > &dungeon_stream_layouts)
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.
LyraPrivateReservation PrivateLyraExactSaveReservation()
AddressOwnership
Ownership classification for ROM addresses and banks.
bool ParseHexString(absl::string_view str, uint64_t *out)
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.
Graphics-sheet storage rules declared by the hack (graphics_sheet_regions).
std::vector< uint16_t > reserved_sheets
std::vector< SnesAddressRange > allocation_regions
std::array< uint32_t, 2 > tile32_id_range
std::array< uint32_t, 4 > dictionary_bases_pc
std::string source_rom_sha256
std::array< uint32_t, 2 > stream_pc_range
std::array< Preimage, 4 > packed_preimages
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