11#include "absl/status/statusor.h"
12#include "absl/strings/str_format.h"
23 return "vanilla_safe";
25 return "hook_patched";
31 return "asm_expansion";
44 if (str.size() >= 2 && str[0] ==
'0' && (str[1] ==
'x' || str[1] ==
'X')) {
45 return static_cast<uint32_t
>(std::stoul(str.substr(2),
nullptr, 16));
47 return static_cast<uint32_t
>(std::stoul(str,
nullptr, 16));
48 }
catch (
const std::exception& exc) {
49 return absl::InvalidArgumentError(
50 absl::StrFormat(
"Invalid hex address '%s': %s", str, exc.what()));
55 if (str ==
"vanilla_safe")
57 if (str ==
"hook_patched")
59 if (str ==
"asm_owned")
63 if (str ==
"asm_expansion")
69 return absl::InvalidArgumentError(
70 absl::StrFormat(
"Unknown ownership string '%s'", str));
92 if (address >= 0x800000 && address <= 0xFFFFFF) {
120 std::ifstream file(filepath);
121 if (!file.is_open()) {
122 return absl::NotFoundError(
"Could not open manifest: " + filepath);
124 std::stringstream buffer;
125 buffer << file.rdbuf();
135 }
catch (
const std::exception& exc) {
136 return absl::InvalidArgumentError(
137 std::string(
"Failed to parse manifest JSON: ") + exc.what());
144 if (root.
contains(
"build_pipeline")) {
145 auto& pipeline = root[
"build_pipeline"];
154 if (root.
contains(
"protected_regions")) {
155 auto& protected_json = root[
"protected_regions"];
157 if (protected_json.contains(
"regions") &&
158 protected_json[
"regions"].is_array()) {
159 for (
auto& region_json : protected_json[
"regions"]) {
162 "start",
"0x000000")));
164 ParseHexAddress(region_json.value(
"end",
"0x000000")));
165 region.
start = NormalizeSnesAddress(region.
start);
166 region.
end = NormalizeSnesAddress(region.
end);
167 region.
hook_count = region_json.value(
"hook_count", 0);
168 region.module = region_json.value(
"module",
"");
177 return lhs.start < rhs.start;
181 if (root.
contains(
"owned_banks") && root[
"owned_banks"].
contains(
"banks")) {
182 for (
auto& bank_json : root[
"owned_banks"][
"banks"]) {
184 uint32_t bank_u32 = 0;
186 ParseHexAddress(bank_json.value(
"bank",
"0x00")));
187 bank.
bank =
static_cast<uint8_t
>(bank_u32 & 0xFF);
188 if (bank.
bank >= 0x80) {
192 "bank_start",
"0x000000")));
194 "bank_end",
"0x000000")));
198 "ownership",
"asm_owned")));
206 for (
auto& tag_json : root[
"room_tags"][
"tags"]) {
208 uint32_t tag_id_u32 = 0;
210 ParseHexAddress(tag_json.value(
"tag_id",
"0x00")));
211 tag.
tag_id =
static_cast<uint8_t
>(tag_id_u32 & 0xFF);
213 ParseHexAddress(tag_json.value(
"address",
"0x000000")));
215 tag.
name = tag_json.value(
"name",
"");
216 tag.
purpose = tag_json.value(
"purpose",
"");
217 tag.
source = tag_json.value(
"source",
"");
219 tag.
enabled = tag_json.value(
"enabled",
true);
226 if (root.
contains(
"feature_flags") &&
227 root[
"feature_flags"].
contains(
"flags")) {
228 for (
auto& flag_json : root[
"feature_flags"][
"flags"]) {
230 flag.
name = flag_json.value(
"name",
"");
231 flag.
value = flag_json.value(
"value", 0);
232 flag.
enabled = flag_json.value(
"enabled",
false);
233 flag.
source = flag_json.value(
"source",
"");
241 for (
auto& var_json : root[
"sram"][
"variables"]) {
243 var.
name = var_json.value(
"name",
"");
245 ParseHexAddress(var_json.value(
"address",
"0x000000")));
246 var.
purpose = var_json.value(
"purpose",
"");
254 auto& msg = root[
"messages"];
255 if (msg.contains(
"hook_address") && msg[
"hook_address"].is_string()) {
257 ParseHexAddress(msg[
"hook_address"].get<std::string>()));
261 if (msg.contains(
"data_start")) {
263 ParseHexAddress(msg.value(
"data_start",
"0x000000")));
266 if (msg.contains(
"data_end")) {
268 ParseHexAddress(msg.value(
"data_end",
"0x000000")));
272 if (msg.contains(
"expanded_range")) {
273 auto& expanded = msg[
"expanded_range"];
274 uint32_t first_id = 0;
275 uint32_t last_id = 0;
277 ParseHexAddress(expanded.value(
"first",
"0x000")));
279 ParseHexAddress(expanded.value(
"last",
"0x000")));
281 static_cast<uint16_t
>(first_id & 0xFFFF);
283 static_cast<uint16_t
>(last_id & 0xFFFF);
289 return absl::OkStatus();
296 address = NormalizeSnesAddress(address);
299 uint8_t bank =
static_cast<uint8_t
>((address >> 16) & 0xFF);
302 return bank_it->second.ownership;
321 address = NormalizeSnesAddress(address);
324 auto iter = std::upper_bound(
327 return addr < region.start;
332 if (address >= iter->start && address < iter->end) {
340 uint8_t bank)
const {
347 return iter->second.ownership;
354 return iter->second.name;
368 return iter->second.enabled;
372 const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
const {
373 std::vector<WriteConflict> conflicts;
378 for (
const auto& range : ranges) {
379 const uint32_t start = NormalizeSnesAddress(range.first);
380 const uint32_t end = NormalizeSnesAddress(range.second);
387 uint32_t cur = start;
389 const uint8_t bank =
static_cast<uint8_t
>((cur >> 16) & 0xFF);
390 const uint32_t next_bank = (cur & 0xFF0000) + 0x010000;
391 const uint32_t seg_end = std::min(end, next_bank);
395 const auto ownership = bank_it->second.ownership;
396 if (IsAsmOwned(ownership) &&
401 conflict.module = bank_it->second.ownership_note;
402 conflicts.push_back(std::move(conflict));
411 auto iter = std::upper_bound(
414 return addr < region.start;
420 if (prev->end > start) {
426 const uint32_t overlap_start = std::max(start, iter->start);
427 const uint32_t overlap_end = std::min(end, iter->end);
428 if (overlap_start < overlap_end) {
430 conflict.
address = overlap_start;
432 conflict.module = iter->module;
433 conflicts.push_back(std::move(conflict));
442 const std::vector<std::pair<uint32_t, uint32_t>>& pc_ranges)
const {
447 std::vector<std::pair<uint32_t, uint32_t>> snes_ranges;
448 snes_ranges.reserve(pc_ranges.size());
450 for (
const auto& range : pc_ranges) {
451 uint32_t pc_start = range.first;
452 const uint32_t pc_end = range.second;
453 if (pc_end <= pc_start) {
459 while (pc_start < pc_end) {
460 const uint32_t next_boundary = (pc_start & ~0x7FFFu) + 0x8000u;
461 const uint32_t seg_end = std::min(pc_end, next_boundary);
462 const uint32_t seg_len = seg_end - pc_start;
463 const uint32_t snes_start =
PcToSnes(pc_start);
464 const uint32_t snes_end = snes_start + seg_len;
465 snes_ranges.emplace_back(snes_start, snes_end);
477 return iter->second.name;
490 const std::string& code_folder) {
491 namespace fs = std::filesystem;
494 fs::path base(code_folder);
497 fs::path planning = base /
"Docs" /
"Dev" /
"Planning";
500 fs::path dungeons_path = planning /
"dungeons.json";
501 if (fs::exists(dungeons_path)) {
502 std::ifstream file(dungeons_path);
503 if (file.is_open()) {
504 std::stringstream buffer;
505 buffer << file.rdbuf();
509 for (
const auto& dj : root[
"dungeons"]) {
511 entry.
id = dj.value(
"id",
"");
512 entry.
name = dj.value(
"name",
"");
516 if (dj.contains(
"rooms") && dj[
"rooms"].is_array()) {
517 for (
const auto& rj : dj[
"rooms"]) {
519 std::string id_str = rj.value(
"id",
"0x00");
520 auto parsed = ParseHexAddress(id_str);
521 room.
id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
522 room.
name = rj.value(
"name",
"");
523 room.
grid_row = rj.value(
"grid_row", 0);
524 room.
grid_col = rj.value(
"grid_col", 0);
525 room.
type = rj.value(
"type",
"normal");
526 room.
palette = rj.value(
"palette", 0);
527 room.
blockset = rj.value(
"blockset", 0);
528 room.
spriteset = rj.value(
"spriteset", 0);
529 room.
tag1 =
static_cast<uint8_t
>(rj.value(
"tag1", 0));
530 room.
tag2 =
static_cast<uint8_t
>(rj.value(
"tag2", 0));
531 entry.
rooms.push_back(std::move(room));
536 if (dj.contains(
"stairs") && dj[
"stairs"].is_array()) {
537 for (
const auto& sj : dj[
"stairs"]) {
539 std::string from_str = sj.value(
"from",
"0x00");
540 std::string to_str = sj.value(
"to",
"0x00");
541 auto from_parsed = ParseHexAddress(from_str);
542 auto to_parsed = ParseHexAddress(to_str);
544 ?
static_cast<int>(*from_parsed)
547 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
548 conn.
label = sj.value(
"label",
"");
549 entry.
stairs.push_back(std::move(conn));
554 if (dj.contains(
"holewarps") && dj[
"holewarps"].is_array()) {
555 for (
const auto& hj : dj[
"holewarps"]) {
557 std::string from_str = hj.value(
"from",
"0x00");
558 std::string to_str = hj.value(
"to",
"0x00");
559 auto from_parsed = ParseHexAddress(from_str);
560 auto to_parsed = ParseHexAddress(to_str);
562 ?
static_cast<int>(*from_parsed)
565 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
566 conn.
label = hj.value(
"label",
"");
567 entry.
holewarps.push_back(std::move(conn));
572 if (dj.contains(
"doors") && dj[
"doors"].is_array()) {
573 for (
const auto& doorj : dj[
"doors"]) {
575 std::string from_str = doorj.value(
"from",
"0x00");
576 std::string to_str = doorj.value(
"to",
"0x00");
577 auto from_parsed = ParseHexAddress(from_str);
578 auto to_parsed = ParseHexAddress(to_str);
580 ?
static_cast<int>(*from_parsed)
583 to_parsed.ok() ?
static_cast<int>(*to_parsed) : 0;
584 conn.
label = doorj.value(
"label",
"");
585 conn.
direction = doorj.value(
"direction",
"");
586 entry.
doors.push_back(std::move(conn));
593 }
catch (
const std::exception& exc) {
594 LOG_WARN(
"HackManifest",
"Failed to parse dungeons.json: %s",
601 fs::path overworld_path = planning /
"overworld.json";
602 if (fs::exists(overworld_path)) {
603 std::ifstream file(overworld_path);
604 if (file.is_open()) {
605 std::stringstream buffer;
606 buffer << file.rdbuf();
610 for (
const auto& aj : root[
"areas"]) {
612 std::string id_str = aj.value(
"area_id",
"0x00");
613 auto parsed = ParseHexAddress(id_str);
614 area.
area_id = parsed.ok() ?
static_cast<int>(*parsed) : 0;
615 area.
name = aj.value(
"name",
"");
616 area.
world = aj.value(
"world",
"");
617 area.
grid_row = aj.value(
"grid_row", 0);
618 area.
grid_col = aj.value(
"grid_col", 0);
622 }
catch (
const std::exception& exc) {
623 LOG_WARN(
"HackManifest",
"Failed to parse overworld.json: %s",
632 fs::path unified_path = planning /
"oracle_resource_labels.json";
633 fs::path legacy_path = planning /
"oracle_room_labels.json";
635 auto normalize_label_id = [](
const std::string& raw) -> std::string {
639 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.front()))) {
642 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.back()))) {
650 if (s.size() >= 2 && s[0] ==
'0' && (s[1] ==
'x' || s[1] ==
'X')) {
660 const unsigned long value = std::stoul(s, &idx, base);
661 if (idx != s.size()) {
664 if (value >
static_cast<unsigned long>(std::numeric_limits<int>::max())) {
667 return std::to_string(
static_cast<int>(value));
673 if (fs::exists(unified_path)) {
674 std::ifstream file(unified_path);
675 if (file.is_open()) {
676 std::stringstream buffer;
677 buffer << file.rdbuf();
681 const std::vector<std::string> label_types = {
682 "room",
"sprite",
"item",
"entrance",
"overworld_map",
"music"};
683 for (
const auto& type_key : label_types) {
685 for (
const auto& [key, value] : root[type_key].
items()) {
686 if (value.is_string()) {
687 const std::string normalized_key = normalize_label_id(key);
689 value.get<std::string>();
694 }
catch (
const std::exception& exc) {
696 "Failed to parse oracle_resource_labels.json: %s",
700 }
else if (fs::exists(legacy_path)) {
702 std::ifstream file(legacy_path);
703 if (file.is_open()) {
704 std::stringstream buffer;
705 buffer << file.rdbuf();
708 if (root.
contains(
"resource_labels") &&
709 root[
"resource_labels"].
contains(
"room")) {
710 for (
const auto& [key, value] :
711 root[
"resource_labels"][
"room"].
items()) {
712 const std::string normalized_key = normalize_label_id(key);
714 value.get<std::string>();
717 }
catch (
const std::exception& exc) {
719 "Failed to parse oracle_room_labels.json: %s", exc.what());
725 fs::path story_events_path = planning /
"story_events.json";
726 if (fs::exists(story_events_path)) {
728 story_events_path.string());
729 if (story_status.ok()) {
739 LOG_DEBUG(
"HackManifest",
"Loaded story events: %zu nodes, %zu edges",
743 LOG_WARN(
"HackManifest",
"Failed to load story_events.json: %s",
744 std::string(story_status.message()).c_str());
748 size_t total_labels = 0;
750 total_labels += labels.size();
756 "Loaded project registry: %zu dungeons, %zu overworld areas, "
757 "%zu resource labels (%zu types)",
763 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
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_
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
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)
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)
bool IsAsmOwned(AddressOwnership ownership)
uint32_t NormalizeSnesAddress(uint32_t address)
absl::StatusOr< uint32_t > ParseHexAddress(const std::string &str)
std::string AddressOwnershipToString(AddressOwnership ownership)
AddressOwnership
Ownership classification for ROM addresses and banks.
uint32_t PcToSnes(uint32_t addr)
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.
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 custom SRAM variable definition.
A conflict detected when yaze wants to write to an ASM-owned address.
AddressOwnership ownership