yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
hack_manifest.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <limits>
6#include <filesystem>
7#include <fstream>
8#include <sstream>
9#include <utility>
10
11#include "absl/status/statusor.h"
12#include "absl/strings/str_format.h"
13#include "rom/snes.h"
14#include "util/json.h"
15#include "util/log.h"
16#include "util/macro.h"
17
18namespace yaze::core {
19
21 switch (ownership) {
23 return "vanilla_safe";
25 return "hook_patched";
27 return "asm_owned";
29 return "shared";
31 return "asm_expansion";
33 return "ram";
35 return "mirror";
36 }
37 return "unknown";
38}
39
40namespace {
41
42absl::StatusOr<uint32_t> ParseHexAddress(const std::string& str) {
43 try {
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));
46 }
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()));
51 }
52}
53
54absl::StatusOr<AddressOwnership> ParseOwnership(const std::string& str) {
55 if (str == "vanilla_safe")
57 if (str == "hook_patched")
59 if (str == "asm_owned")
61 if (str == "shared")
63 if (str == "asm_expansion")
65 if (str == "ram")
67 if (str == "mirror")
69 return absl::InvalidArgumentError(
70 absl::StrFormat("Unknown ownership string '%s'", str));
71}
72
74 switch (ownership) {
79 return true;
83 return false;
84 }
85 return false;
86}
87
88uint32_t NormalizeSnesAddress(uint32_t address) {
89 // Treat FastROM mirrors ($80-$FF) as equivalent to the canonical banks
90 // ($00-$7F). The hack manifest is emitted from ASM `org $XXXXXX` directives
91 // and typically uses canonical addresses.
92 if (address >= 0x800000 && address <= 0xFFFFFF) {
93 address &= 0x7FFFFF;
94 }
95 return address;
96}
97
98} // namespace
99
101 loaded_ = false;
103 hack_name_.clear();
104 total_hooks_ = 0;
105 protected_regions_.clear();
106 owned_banks_.clear();
107 room_tag_map_.clear();
108 room_tags_.clear();
109 feature_flag_map_.clear();
110 feature_flags_.clear();
111 sram_map_.clear();
112 sram_variables_.clear();
117}
118
119absl::Status HackManifest::LoadFromFile(const std::string& filepath) {
120 std::ifstream file(filepath);
121 if (!file.is_open()) {
122 return absl::NotFoundError("Could not open manifest: " + filepath);
123 }
124 std::stringstream buffer;
125 buffer << file.rdbuf();
126 return LoadFromString(buffer.str());
127}
128
129absl::Status HackManifest::LoadFromString(const std::string& json_content) {
130 Reset();
131
132 Json root;
133 try {
134 root = Json::parse(json_content);
135 } catch (const std::exception& exc) {
136 return absl::InvalidArgumentError(
137 std::string("Failed to parse manifest JSON: ") + exc.what());
138 }
139
140 manifest_version_ = root.value("manifest_version", 0);
141 hack_name_ = root.value("hack_name", "");
142
143 // Build pipeline
144 if (root.contains("build_pipeline")) {
145 auto& pipeline = root["build_pipeline"];
146 build_pipeline_.dev_rom = pipeline.value("dev_rom", "");
147 build_pipeline_.patched_rom = pipeline.value("patched_rom", "");
148 build_pipeline_.assembler = pipeline.value("assembler", "");
149 build_pipeline_.entry_point = pipeline.value("entry_point", "");
150 build_pipeline_.build_script = pipeline.value("build_script", "");
151 }
152
153 // Protected regions
154 if (root.contains("protected_regions")) {
155 auto& protected_json = root["protected_regions"];
156 total_hooks_ = protected_json.value("total_hooks", 0);
157 if (protected_json.contains("regions") &&
158 protected_json["regions"].is_array()) {
159 for (auto& region_json : protected_json["regions"]) {
160 ProtectedRegion region;
161 ASSIGN_OR_RETURN(region.start, ParseHexAddress(region_json.value(
162 "start", "0x000000")));
163 ASSIGN_OR_RETURN(region.end,
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", "");
169 protected_regions_.push_back(region);
170 }
171 }
172 }
173
174 // Sort protected regions for binary search
175 std::sort(protected_regions_.begin(), protected_regions_.end(),
176 [](const ProtectedRegion& lhs, const ProtectedRegion& rhs) {
177 return lhs.start < rhs.start;
178 });
179
180 // Owned banks
181 if (root.contains("owned_banks") && root["owned_banks"].contains("banks")) {
182 for (auto& bank_json : root["owned_banks"]["banks"]) {
183 OwnedBank bank;
184 uint32_t bank_u32 = 0;
185 ASSIGN_OR_RETURN(bank_u32,
186 ParseHexAddress(bank_json.value("bank", "0x00")));
187 bank.bank = static_cast<uint8_t>(bank_u32 & 0xFF);
188 if (bank.bank >= 0x80) {
189 bank.bank &= 0x7F;
190 }
191 ASSIGN_OR_RETURN(bank.bank_start, ParseHexAddress(bank_json.value(
192 "bank_start", "0x000000")));
193 ASSIGN_OR_RETURN(bank.bank_end, ParseHexAddress(bank_json.value(
194 "bank_end", "0x000000")));
195 bank.bank_start = NormalizeSnesAddress(bank.bank_start);
196 bank.bank_end = NormalizeSnesAddress(bank.bank_end);
197 ASSIGN_OR_RETURN(bank.ownership, ParseOwnership(bank_json.value(
198 "ownership", "asm_owned")));
199 bank.ownership_note = bank_json.value("ownership_note", "");
200 owned_banks_[bank.bank] = bank;
201 }
202 }
203
204 // Room tags
205 if (root.contains("room_tags") && root["room_tags"].contains("tags")) {
206 for (auto& tag_json : root["room_tags"]["tags"]) {
207 RoomTagEntry tag;
208 uint32_t tag_id_u32 = 0;
209 ASSIGN_OR_RETURN(tag_id_u32,
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")));
214 tag.address = NormalizeSnesAddress(tag.address);
215 tag.name = tag_json.value("name", "");
216 tag.purpose = tag_json.value("purpose", "");
217 tag.source = tag_json.value("source", "");
218 tag.feature_flag = tag_json.value("feature_flag", "");
219 tag.enabled = tag_json.value("enabled", true);
220 room_tag_map_[tag.tag_id] = tag;
221 room_tags_.push_back(tag);
222 }
223 }
224
225 // Feature flags
226 if (root.contains("feature_flags") &&
227 root["feature_flags"].contains("flags")) {
228 for (auto& flag_json : root["feature_flags"]["flags"]) {
229 FeatureFlag flag;
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", "");
234 feature_flag_map_[flag.name] = flag;
235 feature_flags_.push_back(flag);
236 }
237 }
238
239 // SRAM variables
240 if (root.contains("sram") && root["sram"].contains("variables")) {
241 for (auto& var_json : root["sram"]["variables"]) {
242 SramVariable var;
243 var.name = var_json.value("name", "");
245 ParseHexAddress(var_json.value("address", "0x000000")));
246 var.purpose = var_json.value("purpose", "");
247 sram_map_[var.address] = var;
248 sram_variables_.push_back(var);
249 }
250 }
251
252 // Message layout
253 if (root.contains("messages")) {
254 auto& msg = root["messages"];
255 if (msg.contains("hook_address") && msg["hook_address"].is_string()) {
257 ParseHexAddress(msg["hook_address"].get<std::string>()));
259 NormalizeSnesAddress(message_layout_.hook_address);
260 }
261 if (msg.contains("data_start")) {
263 ParseHexAddress(msg.value("data_start", "0x000000")));
265 }
266 if (msg.contains("data_end")) {
268 ParseHexAddress(msg.value("data_end", "0x000000")));
269 message_layout_.data_end = NormalizeSnesAddress(message_layout_.data_end);
270 }
271 message_layout_.vanilla_count = msg.value("vanilla_count", 397);
272 if (msg.contains("expanded_range")) {
273 auto& expanded = msg["expanded_range"];
274 uint32_t first_id = 0;
275 uint32_t last_id = 0;
276 ASSIGN_OR_RETURN(first_id,
277 ParseHexAddress(expanded.value("first", "0x000")));
278 ASSIGN_OR_RETURN(last_id,
279 ParseHexAddress(expanded.value("last", "0x000")));
281 static_cast<uint16_t>(first_id & 0xFFFF);
283 static_cast<uint16_t>(last_id & 0xFFFF);
284 message_layout_.expanded_count = expanded.value("count", 0);
285 }
286 }
287
288 loaded_ = true;
289 return absl::OkStatus();
290}
291
293 if (!loaded_)
295
296 address = NormalizeSnesAddress(address);
297
298 // Check bank ownership first
299 uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFF);
300 auto bank_it = owned_banks_.find(bank);
301 if (bank_it != owned_banks_.end()) {
302 return bank_it->second.ownership;
303 }
304
305 // Check protected regions (binary search since they're sorted)
306 if (IsProtected(address)) {
308 }
309
311}
312
313bool HackManifest::IsWriteOverwritten(uint32_t address) const {
314 auto ownership = ClassifyAddress(address);
315 return ownership == AddressOwnership::kHookPatched ||
316 ownership == AddressOwnership::kAsmOwned ||
318}
319
320bool HackManifest::IsProtected(uint32_t address) const {
321 address = NormalizeSnesAddress(address);
322
323 // Binary search for the region containing this address
324 auto iter = std::upper_bound(
325 protected_regions_.begin(), protected_regions_.end(), address,
326 [](uint32_t addr, const ProtectedRegion& region) {
327 return addr < region.start;
328 });
329
330 if (iter != protected_regions_.begin()) {
331 --iter;
332 if (address >= iter->start && address < iter->end) {
333 return true;
334 }
335 }
336 return false;
337}
338
339std::optional<AddressOwnership> HackManifest::GetBankOwnership(
340 uint8_t bank) const {
341 if (bank >= 0x80) {
342 bank &= 0x7F;
343 }
344 auto iter = owned_banks_.find(bank);
345 if (iter == owned_banks_.end())
346 return std::nullopt;
347 return iter->second.ownership;
348}
349
350std::string HackManifest::GetRoomTagLabel(uint8_t tag_id) const {
351 auto iter = room_tag_map_.find(tag_id);
352 if (iter == room_tag_map_.end())
353 return "";
354 return iter->second.name;
355}
356
357std::optional<RoomTagEntry> HackManifest::GetRoomTag(uint8_t tag_id) const {
358 auto iter = room_tag_map_.find(tag_id);
359 if (iter == room_tag_map_.end())
360 return std::nullopt;
361 return iter->second;
362}
363
364bool HackManifest::IsFeatureEnabled(const std::string& flag_name) const {
365 auto iter = feature_flag_map_.find(flag_name);
366 if (iter == feature_flag_map_.end())
367 return false;
368 return iter->second.enabled;
369}
370
371std::vector<WriteConflict> HackManifest::AnalyzeWriteRanges(
372 const std::vector<std::pair<uint32_t, uint32_t>>& ranges) const {
373 std::vector<WriteConflict> conflicts;
374 if (!loaded_) {
375 return conflicts;
376 }
377
378 for (const auto& range : ranges) {
379 const uint32_t start = NormalizeSnesAddress(range.first);
380 const uint32_t end = NormalizeSnesAddress(range.second);
381 if (end <= start) {
382 continue;
383 }
384
385 // 1) Conflicts at the bank level (asm_owned/asm_expansion/mirror/ram/shared)
386 // Only report banks that are truly ASM-owned (not shared).
387 uint32_t cur = start;
388 while (cur < end) {
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);
392
393 auto bank_it = owned_banks_.find(bank);
394 if (bank_it != owned_banks_.end()) {
395 const auto ownership = bank_it->second.ownership;
396 if (IsAsmOwned(ownership) &&
397 ownership != AddressOwnership::kHookPatched) {
398 WriteConflict conflict;
399 conflict.address = cur;
400 conflict.ownership = ownership;
401 conflict.module = bank_it->second.ownership_note;
402 conflicts.push_back(std::move(conflict));
403 }
404 }
405
406 cur = seg_end;
407 }
408
409 // 2) Conflicts from hook-patched protected regions (vanilla banks).
410 // protected_regions_ is sorted by start, with [start, end) semantics.
411 auto iter = std::upper_bound(
412 protected_regions_.begin(), protected_regions_.end(), start,
413 [](uint32_t addr, const ProtectedRegion& region) {
414 return addr < region.start;
415 });
416
417 if (iter != protected_regions_.begin()) {
418 auto prev = iter;
419 --prev;
420 if (prev->end > start) {
421 iter = prev;
422 }
423 }
424
425 for (; iter != protected_regions_.end() && iter->start < end; ++iter) {
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) {
429 WriteConflict conflict;
430 conflict.address = overlap_start;
432 conflict.module = iter->module;
433 conflicts.push_back(std::move(conflict));
434 }
435 }
436 }
437
438 return conflicts;
439}
440
441std::vector<WriteConflict> HackManifest::AnalyzePcWriteRanges(
442 const std::vector<std::pair<uint32_t, uint32_t>>& pc_ranges) const {
443 if (!loaded_) {
444 return {};
445 }
446
447 std::vector<std::pair<uint32_t, uint32_t>> snes_ranges;
448 snes_ranges.reserve(pc_ranges.size());
449
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) {
454 continue;
455 }
456
457 // Split at LoROM bank boundaries (0x8000 bytes per bank segment in PC
458 // space). PcToSnes() is linear within these segments.
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);
466 pc_start = seg_end;
467 }
468 }
469
470 return AnalyzeWriteRanges(snes_ranges);
471}
472
473std::string HackManifest::GetSramVariableName(uint32_t address) const {
474 auto iter = sram_map_.find(address);
475 if (iter == sram_map_.end())
476 return "";
477 return iter->second.name;
478}
479
480bool HackManifest::IsExpandedMessage(uint16_t message_id) const {
481 return message_id >= message_layout_.first_expanded_id &&
482 message_id <= message_layout_.last_expanded_id;
483}
484
485// ============================================================================
486// Project Registry Loading
487// ============================================================================
488
490 const std::string& code_folder) {
491 namespace fs = std::filesystem;
492
494 fs::path base(code_folder);
495
496 // Look for data files in Docs/Dev/Planning/ (generated output location)
497 fs::path planning = base / "Docs" / "Dev" / "Planning";
498
499 // ── Load dungeons.json ──────────────────────────────────────────────────
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();
506 try {
507 Json root = Json::parse(buffer.str());
508 if (root.contains("dungeons") && root["dungeons"].is_array()) {
509 for (const auto& dj : root["dungeons"]) {
510 DungeonEntry entry;
511 entry.id = dj.value("id", "");
512 entry.name = dj.value("name", "");
513 entry.vanilla_name = dj.value("vanilla_name", "");
514
515 // Rooms
516 if (dj.contains("rooms") && dj["rooms"].is_array()) {
517 for (const auto& rj : dj["rooms"]) {
518 DungeonRoom room;
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));
532 }
533 }
534
535 // Stairs
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);
543 conn.from_room = from_parsed.ok()
544 ? static_cast<int>(*from_parsed)
545 : 0;
546 conn.to_room =
547 to_parsed.ok() ? static_cast<int>(*to_parsed) : 0;
548 conn.label = sj.value("label", "");
549 entry.stairs.push_back(std::move(conn));
550 }
551 }
552
553 // Holewarps
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);
561 conn.from_room = from_parsed.ok()
562 ? static_cast<int>(*from_parsed)
563 : 0;
564 conn.to_room =
565 to_parsed.ok() ? static_cast<int>(*to_parsed) : 0;
566 conn.label = hj.value("label", "");
567 entry.holewarps.push_back(std::move(conn));
568 }
569 }
570
571 // Doors
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);
579 conn.from_room = from_parsed.ok()
580 ? static_cast<int>(*from_parsed)
581 : 0;
582 conn.to_room =
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));
587 }
588 }
589
590 project_registry_.dungeons.push_back(std::move(entry));
591 }
592 }
593 } catch (const std::exception& exc) {
594 LOG_WARN("HackManifest", "Failed to parse dungeons.json: %s",
595 exc.what());
596 }
597 }
598 }
599
600 // ── Load overworld.json ─────────────────────────────────────────────────
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();
607 try {
608 Json root = Json::parse(buffer.str());
609 if (root.contains("areas") && root["areas"].is_array()) {
610 for (const auto& aj : root["areas"]) {
611 OverworldArea area;
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);
619 project_registry_.overworld_areas.push_back(std::move(area));
620 }
621 }
622 } catch (const std::exception& exc) {
623 LOG_WARN("HackManifest", "Failed to parse overworld.json: %s",
624 exc.what());
625 }
626 }
627 }
628
629 // ── Load resource labels ────────────────────────────────────────────────
630 // Priority 1: Unified oracle_resource_labels.json (all types)
631 // Priority 2: Legacy oracle_room_labels.json (room type only)
632 fs::path unified_path = planning / "oracle_resource_labels.json";
633 fs::path legacy_path = planning / "oracle_room_labels.json";
634
635 auto normalize_label_id = [](const std::string& raw) -> std::string {
636 // Project resource_labels keys are decimal strings (std::to_string(id)).
637 // Support either decimal ("57") or explicit hex ("0x39") here.
638 std::string s = raw;
639 while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
640 s.erase(s.begin());
641 }
642 while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
643 s.pop_back();
644 }
645 if (s.empty()) {
646 return raw;
647 }
648
649 int base = 10;
650 if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
651 base = 16;
652 s = s.substr(2);
653 if (s.empty()) {
654 return raw;
655 }
656 }
657
658 try {
659 size_t idx = 0;
660 const unsigned long value = std::stoul(s, &idx, base);
661 if (idx != s.size()) {
662 return raw;
663 }
664 if (value > static_cast<unsigned long>(std::numeric_limits<int>::max())) {
665 return raw;
666 }
667 return std::to_string(static_cast<int>(value));
668 } catch (...) {
669 return raw;
670 }
671 };
672
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();
678 try {
679 Json root = Json::parse(buffer.str());
680 // Parse all type sections (room, sprite, item, entrance, etc.)
681 const std::vector<std::string> label_types = {
682 "room", "sprite", "item", "entrance", "overworld_map", "music"};
683 for (const auto& type_key : label_types) {
684 if (root.contains(type_key) && root[type_key].is_object()) {
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);
688 project_registry_.all_resource_labels[type_key][normalized_key] =
689 value.get<std::string>();
690 }
691 }
692 }
693 }
694 } catch (const std::exception& exc) {
695 LOG_WARN("HackManifest",
696 "Failed to parse oracle_resource_labels.json: %s",
697 exc.what());
698 }
699 }
700 } else if (fs::exists(legacy_path)) {
701 // Backward compat: load room-only labels from legacy file
702 std::ifstream file(legacy_path);
703 if (file.is_open()) {
704 std::stringstream buffer;
705 buffer << file.rdbuf();
706 try {
707 Json root = Json::parse(buffer.str());
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);
713 project_registry_.all_resource_labels["room"][normalized_key] =
714 value.get<std::string>();
715 }
716 }
717 } catch (const std::exception& exc) {
718 LOG_WARN("HackManifest",
719 "Failed to parse oracle_room_labels.json: %s", exc.what());
720 }
721 }
722 }
723
724 // ── Load story events ──────────────────────────────────────────────────
725 fs::path story_events_path = planning / "story_events.json";
726 if (fs::exists(story_events_path)) {
727 auto story_status = project_registry_.story_events.LoadFromJson(
728 story_events_path.string());
729 if (story_status.ok()) {
731 if (oracle_progression_state_.has_value()) {
733 } else {
734 // Default to an initial "no progress" state so the graph renders with
735 // sensible locked/available coloring even before live SRAM is wired up.
736 project_registry_.story_events.UpdateStatus(/*crystal_bitfield=*/0,
737 /*game_state=*/0);
738 }
739 LOG_DEBUG("HackManifest", "Loaded story events: %zu nodes, %zu edges",
742 } else {
743 LOG_WARN("HackManifest", "Failed to load story_events.json: %s",
744 std::string(story_status.message()).c_str());
745 }
746 }
747
748 size_t total_labels = 0;
749 for (const auto& [type, labels] : project_registry_.all_resource_labels) {
750 total_labels += labels.size();
751 }
752
753 if (!project_registry_.dungeons.empty() ||
754 !project_registry_.overworld_areas.empty() || total_labels > 0) {
755 LOG_DEBUG("HackManifest",
756 "Loaded project registry: %zu dungeons, %zu overworld areas, "
757 "%zu resource labels (%zu types)",
759 project_registry_.overworld_areas.size(), total_labels,
761 }
762
763 return absl::OkStatus();
764}
765
772
776 project_registry_.story_events.UpdateStatus(/*crystal_bitfield=*/0,
777 /*game_state=*/0);
778 }
779}
780
781} // namespace yaze::core
bool is_object() const
Definition json.h:57
static Json parse(const std::string &)
Definition json.h:36
bool is_array() const
Definition json.h:58
items_view items()
Definition json.h:88
bool contains(const std::string &) const
Definition json.h:53
T value(const std::string &, const T &def) const
Definition json.h:50
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_
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,...)
Definition log.h:103
#define LOG_WARN(category, format,...)
Definition log.h:107
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< AddressOwnership > ParseOwnership(const std::string &str)
bool IsAsmOwned(AddressOwnership ownership)
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)
Definition snes.h:17
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.
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
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