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 <array>
5#include <cctype>
6#include <filesystem>
7#include <fstream>
8#include <iterator>
9#include <limits>
10#include <sstream>
11#include <utility>
12
13#include "absl/status/statusor.h"
14#include "absl/strings/str_format.h"
15#include "rom/snes.h"
16#include "util/json.h"
17#include "util/log.h"
18#include "util/macro.h"
19
20namespace yaze::core {
21
23 switch (ownership) {
25 return "vanilla_safe";
27 return "hook_patched";
29 return "asm_owned";
31 return "shared";
33 return "asm_expansion";
35 return "ram";
37 return "mirror";
38 }
39 return "unknown";
40}
41
42namespace {
43
44absl::StatusOr<uint32_t> ParseHexAddress(const std::string& str) {
45 try {
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));
48 }
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()));
53 }
54}
55
56absl::StatusOr<AddressOwnership> ParseOwnership(const std::string& str) {
57 if (str == "vanilla_safe")
59 if (str == "hook_patched")
61 if (str == "asm_owned")
63 if (str == "shared")
65 if (str == "asm_expansion")
67 if (str == "ram")
69 if (str == "mirror")
71 return absl::InvalidArgumentError(
72 absl::StrFormat("Unknown ownership string '%s'", str));
73}
74
76 switch (ownership) {
81 return true;
85 return false;
86 }
87 return false;
88}
89
90uint32_t NormalizeSnesAddress(uint32_t address) {
91 // Treat FastROM mirrors ($80-$FF) as equivalent to the canonical banks
92 // ($00-$7F). The hack manifest is emitted from ASM `org $XXXXXX` directives
93 // and typically uses canonical addresses.
94 if (address >= 0x800000 && address <= 0xFFFFFF) {
95 address &= 0x7FFFFF;
96 }
97 return address;
98}
99
100constexpr uint64_t kCanonicalLoRomSize = 0x400000;
101
103 switch (stream) {
105 return "objects";
107 return "sprites";
109 return "pot_items";
110 }
111 return "unknown";
112}
113
114absl::StatusOr<uint32_t> ParseStrictHexValue(const Json& value,
115 const std::string& field,
116 uint32_t maximum) {
117 if (!value.is_string()) {
118 return absl::InvalidArgumentError(
119 absl::StrFormat("%s must be a hexadecimal string", field));
120 }
121
122 const std::string input = value.get<std::string>();
123 size_t digits_begin = 0;
124 if (!input.empty() && input.front() == '$') {
125 digits_begin = 1;
126 } else if (input.size() >= 2 && input[0] == '0' &&
127 (input[1] == 'x' || input[1] == 'X')) {
128 digits_begin = 2;
129 }
130 if (digits_begin == input.size()) {
131 return absl::InvalidArgumentError(
132 absl::StrFormat("%s has no hexadecimal digits", field));
133 }
134 for (size_t index = digits_begin; index < input.size(); ++index) {
135 if (!std::isxdigit(static_cast<unsigned char>(input[index]))) {
136 return absl::InvalidArgumentError(absl::StrFormat(
137 "%s contains an invalid hexadecimal value '%s'", field, input));
138 }
139 }
140
141 try {
142 const uint64_t parsed =
143 std::stoull(input.substr(digits_begin), nullptr, 16);
144 if (parsed > maximum) {
145 return absl::InvalidArgumentError(
146 absl::StrFormat("%s value '%s' exceeds 0x%X", field, input, maximum));
147 }
148 return static_cast<uint32_t>(parsed);
149 } catch (const std::exception& exc) {
150 return absl::InvalidArgumentError(
151 absl::StrFormat("Invalid hexadecimal value for %s ('%s'): %s", field,
152 input, exc.what()));
153 }
154}
155
156absl::StatusOr<int> ParseManifestVersion(const Json& root) {
157 if (!root.contains("manifest_version")) {
158 return 0;
159 }
160
161 const Json& value = root["manifest_version"];
162 uint64_t version = 0;
163 if (value.is_number_unsigned()) {
164 version = value.get<uint64_t>();
165 } else if (value.is_number_integer()) {
166 const int64_t signed_version = value.get<int64_t>();
167 if (signed_version < 0) {
168 return absl::InvalidArgumentError(
169 "manifest_version must be a non-negative integer");
170 }
171 version = static_cast<uint64_t>(signed_version);
172 } else {
173 return absl::InvalidArgumentError(
174 "manifest_version must be a non-negative integer");
175 }
176
177 if (version > static_cast<uint64_t>(std::numeric_limits<int>::max())) {
178 return absl::InvalidArgumentError("manifest_version is too large");
179 }
180 return static_cast<int>(version);
181}
182
183absl::StatusOr<uint32_t> ParseLoRomAddress(const Json& object,
184 const std::string& key,
185 const std::string& path) {
186 if (!object.contains(key)) {
187 return absl::InvalidArgumentError(
188 absl::StrFormat("%s.%s is required", path, key));
189 }
190
191 uint32_t address = 0;
192 ASSIGN_OR_RETURN(address, ParseStrictHexValue(object[key], path + "." + key,
193 /*maximum=*/0xFFFFFF));
194 address = NormalizeSnesAddress(address);
195 const uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFF);
196 if (bank == 0x7E || bank == 0x7F) {
197 return absl::InvalidArgumentError(absl::StrFormat(
198 "%s.%s must not use SNES WRAM bank 0x%02X", path, key, bank));
199 }
200 if ((address & 0xFFFF) < 0x8000) {
201 return absl::InvalidArgumentError(absl::StrFormat(
202 "%s.%s must be a mapped LoROM address (low word >= 0x8000)", path,
203 key));
204 }
205 if (NormalizeSnesAddress(PcToSnes(SnesToPc(address))) != address) {
206 return absl::InvalidArgumentError(absl::StrFormat(
207 "%s.%s is not convertible to a canonical LoROM address", path, key));
208 }
209 return address;
210}
211
212absl::StatusOr<uint32_t> ParsePointerCount(const Json& object,
213 const std::string& path) {
214 constexpr const char* kField = "pointer_count";
215 constexpr uint32_t kMaxDungeonPointerCount = 296;
216 if (!object.contains(kField)) {
217 return absl::InvalidArgumentError(
218 absl::StrFormat("%s.%s is required", path, kField));
219 }
220
221 const Json& value = object[kField];
222 uint64_t count = 0;
223 if (value.is_number_unsigned()) {
224 count = value.get<uint64_t>();
225 } else if (value.is_number_integer()) {
226 const int64_t signed_count = value.get<int64_t>();
227 if (signed_count <= 0) {
228 return absl::InvalidArgumentError(
229 absl::StrFormat("%s.%s must be greater than zero", path, kField));
230 }
231 count = static_cast<uint64_t>(signed_count);
232 } else {
233 return absl::InvalidArgumentError(
234 absl::StrFormat("%s.%s must be an integer", path, kField));
235 }
236
237 if (count == 0 || count > kMaxDungeonPointerCount) {
238 return absl::InvalidArgumentError(absl::StrFormat(
239 "%s.%s must be in [1, %u]", path, kField, kMaxDungeonPointerCount));
240 }
241 return static_cast<uint32_t>(count);
242}
243
244absl::StatusOr<DungeonPointerEncoding> ParsePointerEncoding(
245 const Json& object, const std::string& path) {
246 constexpr const char* kField = "pointer_encoding";
247 if (!object.contains(kField) || !object[kField].is_string()) {
248 return absl::InvalidArgumentError(
249 absl::StrFormat("%s.%s must be 'long24' or 'bank16'", path, kField));
250 }
251 const std::string value = object[kField].get<std::string>();
252 if (value == "long24") {
254 }
255 if (value == "bank16") {
257 }
258 return absl::InvalidArgumentError(
259 absl::StrFormat("%s.%s has unknown encoding '%s'", path, kField, value));
260}
261
262absl::StatusOr<DungeonWriteStrategy> ParseWriteStrategy(
263 const Json& object, const std::string& path) {
264 constexpr const char* kField = "strategy";
265 if (!object.contains(kField) || !object[kField].is_string()) {
266 return absl::InvalidArgumentError(absl::StrFormat(
267 "%s.%s must be 'copy_on_write' or 'repack_all'", path, kField));
268 }
269 const std::string value = object[kField].get<std::string>();
270 if (value == "copy_on_write") {
272 }
273 if (value == "repack_all") {
275 }
276 return absl::InvalidArgumentError(
277 absl::StrFormat("%s.%s has unknown strategy '%s'", path, kField, value));
278}
279
281 uint32_t start;
282 uint32_t end;
283 std::string name;
284};
285
286absl::Status ValidateDisjointRanges(std::vector<NamedPcRange> ranges,
287 const std::string& description) {
288 std::sort(ranges.begin(), ranges.end(),
289 [](const NamedPcRange& lhs, const NamedPcRange& rhs) {
290 return lhs.start < rhs.start ||
291 (lhs.start == rhs.start && lhs.end < rhs.end);
292 });
293 if (ranges.empty()) {
294 return absl::OkStatus();
295 }
296
297 NamedPcRange active = ranges.front();
298 for (size_t index = 1; index < ranges.size(); ++index) {
299 const NamedPcRange& current = ranges[index];
300 if (current.start < active.end) {
301 return absl::InvalidArgumentError(
302 absl::StrFormat("%s overlap: %s conflicts with %s", description,
303 current.name, active.name));
304 }
305 if (current.end > active.end) {
306 active = current;
307 }
308 }
309 return absl::OkStatus();
310}
311
312absl::StatusOr<std::vector<SnesAddressRange>> ParseAddressRanges(
313 const Json& object, const std::string& key, const std::string& path) {
314 const std::string field_path = path + "." + key;
315 if (!object.contains(key) || !object[key].is_array() || object[key].empty()) {
316 return absl::InvalidArgumentError(
317 absl::StrFormat("%s must be a non-empty array", field_path));
318 }
319
320 std::vector<SnesAddressRange> ranges;
321 std::vector<NamedPcRange> pc_ranges;
322 ranges.reserve(object[key].size());
323 pc_ranges.reserve(object[key].size());
324 size_t index = 0;
325 for (const Json& range_json : object[key]) {
326 const std::string range_path =
327 absl::StrFormat("%s[%zu]", field_path, index);
328 if (!range_json.is_object()) {
329 return absl::InvalidArgumentError(
330 absl::StrFormat("%s must be an object", range_path));
331 }
332
333 SnesAddressRange range;
335 ParseLoRomAddress(range_json, "start", range_path));
336 ASSIGN_OR_RETURN(range.end,
337 ParseLoRomAddress(range_json, "end", range_path));
338 const uint32_t pc_start = SnesToPc(range.start);
339 const uint32_t pc_end = SnesToPc(range.end);
340 if (range.end <= range.start || pc_end <= pc_start) {
341 return absl::InvalidArgumentError(absl::StrFormat(
342 "%s must be a non-empty half-open range with end > start",
343 range_path));
344 }
345
346 ranges.push_back(range);
347 pc_ranges.push_back({pc_start, pc_end, range_path});
348 ++index;
349 }
350
351 RETURN_IF_ERROR(ValidateDisjointRanges(std::move(pc_ranges), field_path));
352 std::sort(ranges.begin(), ranges.end(),
353 [](const SnesAddressRange& lhs, const SnesAddressRange& rhs) {
354 return lhs.start < rhs.start;
355 });
356 return ranges;
357}
358
359absl::StatusOr<std::vector<SnesAddressRange>> ParseEditorManagedRegions(
360 const Json& root, int manifest_version) {
361 constexpr const char* kSection = "editor_managed_regions";
362 if (!root.contains(kSection)) {
363 return std::vector<SnesAddressRange>{};
364 }
365 if (manifest_version < 3) {
366 return absl::InvalidArgumentError(
367 "editor_managed_regions requires manifest_version 3 or newer");
368 }
369
370 const Json& section = root[kSection];
371 if (!section.is_object() || section.empty()) {
372 return absl::InvalidArgumentError(
373 "editor_managed_regions must be a non-empty object");
374 }
375 return ParseAddressRanges(section, "regions", kSection);
376}
377
379 int total_hooks = 0;
380 std::vector<ProtectedRegion> regions;
381};
382
383absl::StatusOr<uint32_t> ParseProtectedAddress(const Json& object,
384 const std::string& key,
385 const std::string& path,
386 bool require_mapped_lorom) {
387 if (require_mapped_lorom) {
388 return ParseLoRomAddress(object, key, path);
389 }
390 if (!object.contains(key)) {
391 return absl::InvalidArgumentError(
392 absl::StrFormat("%s.%s is required", path, key));
393 }
394
395 uint32_t address = 0;
396 ASSIGN_OR_RETURN(address, ParseStrictHexValue(object[key], path + "." + key,
397 /*maximum=*/0xFFFFFF));
398 return NormalizeSnesAddress(address);
399}
400
401absl::StatusOr<ParsedProtectedRegions> ParseProtectedRegions(
402 const Json& root, int manifest_version) {
404 constexpr const char* kSection = "protected_regions";
405 if (!root.contains(kSection)) {
406 return parsed;
407 }
408
409 const Json& section = root[kSection];
410 if (!section.is_object()) {
411 return absl::InvalidArgumentError("protected_regions must be an object");
412 }
413 if (!section.contains("regions") || !section["regions"].is_array()) {
414 return absl::InvalidArgumentError(
415 "protected_regions.regions must be an array");
416 }
417
418 parsed.total_hooks = section.value("total_hooks", 0);
419 parsed.regions.reserve(section["regions"].size());
420 // Version 3 is the first schema that can exempt editor-managed ranges, so
421 // its protected endpoints must be canonical mapped LoROM addresses. Legacy
422 // manifests still require structurally valid ranges, but retain low-half
423 // addresses emitted by older Oracle sources (for example $1E7F21).
424 const bool require_mapped_lorom = manifest_version >= 3;
425 size_t index = 0;
426 for (const Json& region_json : section["regions"]) {
427 const std::string path =
428 absl::StrFormat("protected_regions.regions[%zu]", index);
429 if (!region_json.is_object()) {
430 return absl::InvalidArgumentError(
431 absl::StrFormat("%s must be an object", path));
432 }
433
434 ProtectedRegion region;
435 ASSIGN_OR_RETURN(region.start,
436 ParseProtectedAddress(region_json, "start", path,
437 require_mapped_lorom));
438 ASSIGN_OR_RETURN(region.end, ParseProtectedAddress(region_json, "end", path,
439 require_mapped_lorom));
440 if (region.end <= region.start ||
441 SnesToPc(region.end) <= SnesToPc(region.start)) {
442 return absl::InvalidArgumentError(absl::StrFormat(
443 "%s must be a non-empty half-open range with end > start", path));
444 }
445
446 region.hook_count = region_json.value("hook_count", 0);
447 region.module = region_json.value("module", "");
448 parsed.regions.push_back(std::move(region));
449 ++index;
450 }
451
452 // Unlike editor-managed and allocator ranges, protected hooks may overlap.
453 // FindProtectedRegion() deliberately walks backward across all candidates.
454 std::sort(parsed.regions.begin(), parsed.regions.end(),
455 [](const ProtectedRegion& lhs, const ProtectedRegion& rhs) {
456 return lhs.start < rhs.start;
457 });
458 return parsed;
459}
460
462 const SnesAddressRange& inner) {
463 return SnesToPc(outer.start) <= SnesToPc(inner.start) &&
464 SnesToPc(inner.end) <= SnesToPc(outer.end);
465}
466
467absl::StatusOr<DungeonStreamLayout> ParseDungeonStreamLayout(
468 DungeonStreamType stream, const Json& json) {
469 const std::string path = absl::StrFormat("dungeon_stream_regions.%s",
471 if (!json.is_object()) {
472 return absl::InvalidArgumentError(
473 absl::StrFormat("%s must be an object", path));
474 }
475
476 DungeonStreamLayout layout;
478 ParseLoRomAddress(json, "pointer_table", path));
483 ParseAddressRanges(json, "data_regions", path));
485 ParseAddressRanges(json, "allocation_regions", path));
486
488 if (!json.contains("pointer_bank")) {
489 return absl::InvalidArgumentError(
490 absl::StrFormat("%s.pointer_bank is required for bank16", path));
491 }
492 uint32_t bank = 0;
494 bank, ParseStrictHexValue(json["pointer_bank"], path + ".pointer_bank",
495 /*maximum=*/0xFF));
496 if (bank >= 0x80) {
497 bank &= 0x7F;
498 }
499 if (bank == 0x7E || bank == 0x7F) {
500 return absl::InvalidArgumentError(absl::StrFormat(
501 "%s.pointer_bank must not use SNES WRAM bank 0x%02X", path, bank));
502 }
503 layout.pointer_bank = static_cast<uint8_t>(bank);
504
505 const uint32_t bank_start_snes = (bank << 16) | 0x8000;
506 const uint32_t bank_start_pc = SnesToPc(bank_start_snes);
507 const uint64_t bank_end_pc = static_cast<uint64_t>(bank_start_pc) + 0x8000;
508 for (const SnesAddressRange& range : layout.data_regions) {
509 if (SnesToPc(range.start) < bank_start_pc ||
510 SnesToPc(range.end) > bank_end_pc) {
511 return absl::InvalidArgumentError(absl::StrFormat(
512 "%s.data_regions must stay within pointer_bank 0x%02X for "
513 "bank16 encoding",
514 path, bank));
515 }
516 }
517 } else if (json.contains("pointer_bank")) {
518 return absl::InvalidArgumentError(
519 absl::StrFormat("%s.pointer_bank is not allowed for long24", path));
520 }
521
522 for (const SnesAddressRange& allocation : layout.allocation_regions) {
523 const bool contained =
524 std::any_of(layout.data_regions.begin(), layout.data_regions.end(),
525 [&allocation](const SnesAddressRange& data) {
526 return PcRangeContains(data, allocation);
527 });
528 if (!contained) {
529 return absl::InvalidArgumentError(absl::StrFormat(
530 "%s allocation range [0x%06X, 0x%06X) is not fully contained in "
531 "a data_region",
532 path, allocation.start, allocation.end));
533 }
534 }
535
536 const uint64_t pointer_width =
538 const uint64_t table_start_pc = SnesToPc(layout.pointer_table);
539 const uint64_t table_size =
540 pointer_width * static_cast<uint64_t>(layout.pointer_count);
541 const uint64_t table_end_pc = table_start_pc + table_size;
542 if (table_start_pc > kCanonicalLoRomSize ||
543 table_end_pc > kCanonicalLoRomSize) {
544 return absl::InvalidArgumentError(absl::StrFormat(
545 "%s pointer table extends beyond canonical LoROM address space", path));
546 }
548 const uint64_t runtime_table_end =
549 static_cast<uint64_t>(layout.pointer_table & 0xFFFFu) + table_size;
550 if (runtime_table_end > 0x10000u) {
551 return absl::InvalidArgumentError(absl::StrFormat(
552 "%s bank16 pointer table crosses its runtime CPU bank", path));
553 }
554 }
555
556 return layout;
557}
558
559absl::StatusOr<std::unordered_map<DungeonStreamType, DungeonStreamLayout>>
561 std::unordered_map<DungeonStreamType, DungeonStreamLayout> layouts;
562 if (!root.contains("dungeon_stream_regions")) {
563 return layouts;
564 }
565
566 const Json& section = root["dungeon_stream_regions"];
567 if (!section.is_object() || section.empty()) {
568 return absl::InvalidArgumentError(
569 "dungeon_stream_regions must be a non-empty object");
570 }
571
572 constexpr std::array<std::pair<const char*, DungeonStreamType>, 3> kStreams =
573 {{{"objects", DungeonStreamType::kObjects},
574 {"sprites", DungeonStreamType::kSprites},
575 {"pot_items", DungeonStreamType::kPotItems}}};
576 for (const auto& item : section.items()) {
577 const auto& key = item.key();
578 bool known = false;
579 for (const auto& entry : kStreams) {
580 if (key == entry.first) {
581 known = true;
582 break;
583 }
584 }
585 if (!known) {
586 return absl::InvalidArgumentError(absl::StrFormat(
587 "dungeon_stream_regions contains unknown stream '%s'", key));
588 }
589 }
590
591 std::vector<NamedPcRange> occupied_ranges;
592 std::vector<NamedPcRange> allocation_ranges;
593 for (const auto& [key, stream] : kStreams) {
594 if (!section.contains(key)) {
595 continue;
596 }
597
598 DungeonStreamLayout layout;
599 ASSIGN_OR_RETURN(layout, ParseDungeonStreamLayout(stream, section[key]));
600
601 const uint64_t pointer_width =
603 const uint64_t table_start_pc = SnesToPc(layout.pointer_table);
604 const uint64_t table_end_pc =
605 table_start_pc +
606 pointer_width * static_cast<uint64_t>(layout.pointer_count);
607 occupied_ranges.push_back({static_cast<uint32_t>(table_start_pc),
608 static_cast<uint32_t>(table_end_pc),
609 absl::StrFormat("%s.pointer_table", key)});
610
611 for (size_t index = 0; index < layout.data_regions.size(); ++index) {
612 const auto& range = layout.data_regions[index];
613 occupied_ranges.push_back(
614 {SnesToPc(range.start), SnesToPc(range.end),
615 absl::StrFormat("%s.data_regions[%zu]", key, index)});
616 }
617 for (size_t index = 0; index < layout.allocation_regions.size(); ++index) {
618 const auto& range = layout.allocation_regions[index];
619 allocation_ranges.push_back(
620 {SnesToPc(range.start), SnesToPc(range.end),
621 absl::StrFormat("%s.allocation_regions[%zu]", key, index)});
622 }
623 layouts.emplace(stream, std::move(layout));
624 }
625
626 RETURN_IF_ERROR(ValidateDisjointRanges(std::move(occupied_ranges),
627 "dungeon stream pointer/data ranges"));
628 RETURN_IF_ERROR(ValidateDisjointRanges(std::move(allocation_ranges),
629 "dungeon stream allocation ranges"));
630 return layouts;
631}
632
633} // namespace
634
636 loaded_ = false;
638 hack_name_.clear();
639 total_hooks_ = 0;
640 protected_regions_.clear();
642 owned_banks_.clear();
643 room_tag_map_.clear();
644 room_tags_.clear();
645 feature_flag_map_.clear();
646 feature_flags_.clear();
647 sram_map_.clear();
648 sram_variables_.clear();
654}
655
656absl::Status HackManifest::LoadFromFile(const std::string& filepath) {
657 std::ifstream file(filepath);
658 if (!file.is_open()) {
659 return absl::NotFoundError("Could not open manifest: " + filepath);
660 }
661 std::stringstream buffer;
662 buffer << file.rdbuf();
663 return LoadFromString(buffer.str());
664}
665
666absl::Status HackManifest::LoadFromString(const std::string& json_content) {
667 Reset();
668
669 Json root;
670 try {
671 root = Json::parse(json_content);
672 } catch (const std::exception& exc) {
673 return absl::InvalidArgumentError(
674 std::string("Failed to parse manifest JSON: ") + exc.what());
675 }
676
677 if (!root.is_object()) {
678 return absl::InvalidArgumentError("Hack manifest root must be an object");
679 }
680
681 ASSIGN_OR_RETURN(manifest_version_, ParseManifestVersion(root));
682 hack_name_ = root.value("hack_name", "");
683
684 std::vector<SnesAddressRange> editor_managed_regions;
686 ParseEditorManagedRegions(root, manifest_version_));
687
688 std::unordered_map<DungeonStreamType, DungeonStreamLayout>
689 dungeon_stream_layouts;
690 ASSIGN_OR_RETURN(dungeon_stream_layouts, ParseDungeonStreamLayouts(root));
691
692 ParsedProtectedRegions protected_regions;
694 ParseProtectedRegions(root, manifest_version_));
695
696 // Build pipeline
697 if (root.contains("build_pipeline")) {
698 auto& pipeline = root["build_pipeline"];
699 build_pipeline_.dev_rom = pipeline.value("dev_rom", "");
700 build_pipeline_.patched_rom = pipeline.value("patched_rom", "");
701 build_pipeline_.assembler = pipeline.value("assembler", "");
702 build_pipeline_.entry_point = pipeline.value("entry_point", "");
703 build_pipeline_.build_script = pipeline.value("build_script", "");
704 }
705
706 // Owned banks
707 if (root.contains("owned_banks") && root["owned_banks"].contains("banks")) {
708 for (auto& bank_json : root["owned_banks"]["banks"]) {
709 OwnedBank bank;
710 uint32_t bank_u32 = 0;
711 ASSIGN_OR_RETURN(bank_u32,
712 ParseHexAddress(bank_json.value("bank", "0x00")));
713 bank.bank = static_cast<uint8_t>(bank_u32 & 0xFF);
714 if (bank.bank >= 0x80) {
715 bank.bank &= 0x7F;
716 }
717 ASSIGN_OR_RETURN(bank.bank_start, ParseHexAddress(bank_json.value(
718 "bank_start", "0x000000")));
719 ASSIGN_OR_RETURN(bank.bank_end, ParseHexAddress(bank_json.value(
720 "bank_end", "0x000000")));
721 bank.bank_start = NormalizeSnesAddress(bank.bank_start);
722 bank.bank_end = NormalizeSnesAddress(bank.bank_end);
723 ASSIGN_OR_RETURN(bank.ownership, ParseOwnership(bank_json.value(
724 "ownership", "asm_owned")));
725 bank.ownership_note = bank_json.value("ownership_note", "");
726 owned_banks_[bank.bank] = bank;
727 }
728 }
729
730 // Room tags
731 if (root.contains("room_tags") && root["room_tags"].contains("tags")) {
732 for (auto& tag_json : root["room_tags"]["tags"]) {
733 RoomTagEntry tag;
734 uint32_t tag_id_u32 = 0;
735 ASSIGN_OR_RETURN(tag_id_u32,
736 ParseHexAddress(tag_json.value("tag_id", "0x00")));
737 tag.tag_id = static_cast<uint8_t>(tag_id_u32 & 0xFF);
739 ParseHexAddress(tag_json.value("address", "0x000000")));
740 tag.address = NormalizeSnesAddress(tag.address);
741 tag.name = tag_json.value("name", "");
742 tag.purpose = tag_json.value("purpose", "");
743 tag.source = tag_json.value("source", "");
744 tag.feature_flag = tag_json.value("feature_flag", "");
745 tag.enabled = tag_json.value("enabled", true);
746 room_tag_map_[tag.tag_id] = tag;
747 room_tags_.push_back(tag);
748 }
749 }
750
751 // Feature flags
752 if (root.contains("feature_flags") &&
753 root["feature_flags"].contains("flags")) {
754 for (auto& flag_json : root["feature_flags"]["flags"]) {
755 FeatureFlag flag;
756 flag.name = flag_json.value("name", "");
757 flag.value = flag_json.value("value", 0);
758 flag.enabled = flag_json.value("enabled", false);
759 flag.source = flag_json.value("source", "");
760 feature_flag_map_[flag.name] = flag;
761 feature_flags_.push_back(flag);
762 }
763 }
764
765 // SRAM variables
766 if (root.contains("sram") && root["sram"].contains("variables")) {
767 for (auto& var_json : root["sram"]["variables"]) {
768 SramVariable var;
769 var.name = var_json.value("name", "");
771 ParseHexAddress(var_json.value("address", "0x000000")));
772 var.purpose = var_json.value("purpose", "");
773 sram_map_[var.address] = var;
774 sram_variables_.push_back(var);
775 }
776 }
777
778 // Message layout
779 if (root.contains("messages")) {
780 auto& msg = root["messages"];
781 if (msg.contains("hook_address") && msg["hook_address"].is_string()) {
783 ParseHexAddress(msg["hook_address"].get<std::string>()));
785 NormalizeSnesAddress(message_layout_.hook_address);
786 }
787 if (msg.contains("data_start")) {
789 ParseHexAddress(msg.value("data_start", "0x000000")));
791 NormalizeSnesAddress(message_layout_.data_start);
792 }
793 if (msg.contains("data_end")) {
795 ParseHexAddress(msg.value("data_end", "0x000000")));
796 message_layout_.data_end = NormalizeSnesAddress(message_layout_.data_end);
797 }
798 message_layout_.vanilla_count = msg.value("vanilla_count", 397);
799 if (msg.contains("expanded_range")) {
800 auto& expanded = msg["expanded_range"];
801 uint32_t first_id = 0;
802 uint32_t last_id = 0;
803 ASSIGN_OR_RETURN(first_id,
804 ParseHexAddress(expanded.value("first", "0x000")));
805 ASSIGN_OR_RETURN(last_id,
806 ParseHexAddress(expanded.value("last", "0x000")));
808 static_cast<uint16_t>(first_id & 0xFFFF);
810 static_cast<uint16_t>(last_id & 0xFFFF);
811 message_layout_.expanded_count = expanded.value("count", 0);
812 }
813 }
814
815 total_hooks_ = protected_regions.total_hooks;
816 protected_regions_ = std::move(protected_regions.regions);
818 dungeon_stream_layouts_ = std::move(dungeon_stream_layouts);
819 loaded_ = true;
820 return absl::OkStatus();
821}
822
824 if (!loaded_)
826
827 address = NormalizeSnesAddress(address);
828
829 // Explicit hooks always win, including hooks that overlap an exact
830 // editor-managed exemption inside an ASM-owned bank.
831 if (IsProtected(address)) {
833 }
834
835 if (IsEditorManaged(address)) {
837 }
838
839 const uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFF);
840 const auto bank_it = owned_banks_.find(bank);
841 if (bank_it != owned_banks_.end()) {
842 return bank_it->second.ownership;
843 }
844
846}
847
848bool HackManifest::IsWriteOverwritten(uint32_t address) const {
849 const AddressOwnership ownership = ClassifyAddress(address);
850 return ownership == AddressOwnership::kHookPatched ||
851 ownership == AddressOwnership::kAsmOwned ||
853}
854
856 uint32_t address) const {
857 address = NormalizeSnesAddress(address);
858
859 // Start with the last possible region, then walk backwards so nested or
860 // overlapping protected regions cannot hide an earlier containing hook.
861 const auto upper = std::upper_bound(
862 protected_regions_.begin(), protected_regions_.end(), address,
863 [](uint32_t addr, const ProtectedRegion& region) {
864 return addr < region.start;
865 });
866
867 for (auto iter = std::make_reverse_iterator(upper);
868 iter != protected_regions_.rend(); ++iter) {
869 if (address >= iter->start && address < iter->end) {
870 return &*iter;
871 }
872 }
873 return nullptr;
874}
875
876bool HackManifest::IsProtected(uint32_t address) const {
877 return FindProtectedRegion(address) != nullptr;
878}
879
880bool HackManifest::IsEditorManaged(uint32_t address) const {
881 address = NormalizeSnesAddress(address);
882 const auto upper = std::upper_bound(
883 editor_managed_regions_.begin(), editor_managed_regions_.end(), address,
884 [](uint32_t addr, const SnesAddressRange& region) {
885 return addr < region.start;
886 });
887 if (upper == editor_managed_regions_.begin()) {
888 return false;
889 }
890 const SnesAddressRange& region = *std::prev(upper);
891 return region.start <= address && address < region.end;
892}
893
894std::optional<AddressOwnership> HackManifest::GetBankOwnership(
895 uint8_t bank) const {
896 if (bank >= 0x80) {
897 bank &= 0x7F;
898 }
899 auto iter = owned_banks_.find(bank);
900 if (iter == owned_banks_.end())
901 return std::nullopt;
902 return iter->second.ownership;
903}
904
905std::string HackManifest::GetRoomTagLabel(uint8_t tag_id) const {
906 auto iter = room_tag_map_.find(tag_id);
907 if (iter == room_tag_map_.end())
908 return "";
909 return iter->second.name;
910}
911
912std::optional<RoomTagEntry> HackManifest::GetRoomTag(uint8_t tag_id) const {
913 auto iter = room_tag_map_.find(tag_id);
914 if (iter == room_tag_map_.end())
915 return std::nullopt;
916 return iter->second;
917}
918
919bool HackManifest::IsFeatureEnabled(const std::string& flag_name) const {
920 auto iter = feature_flag_map_.find(flag_name);
921 if (iter == feature_flag_map_.end())
922 return false;
923 return iter->second.enabled;
924}
925
926std::vector<WriteConflict> HackManifest::AnalyzeWriteRanges(
927 const std::vector<std::pair<uint32_t, uint32_t>>& ranges) const {
928 std::vector<WriteConflict> conflicts;
929 if (!loaded_) {
930 return conflicts;
931 }
932
933 for (const auto& range : ranges) {
934 const uint32_t start = NormalizeSnesAddress(range.first);
935 const uint32_t end = NormalizeSnesAddress(range.second);
936 if (end <= start) {
937 continue;
938 }
939
940 // Classify one segment at a time. Every possible ownership transition is
941 // a boundary: bank edges, exact editor-managed edges, and hook edges.
942 // This prevents a partially exempt write from bypassing an owned neighbor.
943 std::vector<uint32_t> boundaries = {start, end};
944 const uint64_t first_bank_boundary =
945 (static_cast<uint64_t>(start) & 0xFF0000u) + 0x010000u;
946 for (uint64_t boundary = first_bank_boundary; boundary < end;
947 boundary += 0x010000u) {
948 boundaries.push_back(static_cast<uint32_t>(boundary));
949 }
950 auto add_range_boundaries = [start, end, &boundaries](const auto& regions) {
951 for (const auto& region : regions) {
952 if (region.start < end && start < region.end) {
953 boundaries.push_back(std::max(start, region.start));
954 boundaries.push_back(std::min(end, region.end));
955 }
956 }
957 };
958 add_range_boundaries(editor_managed_regions_);
959 add_range_boundaries(protected_regions_);
960
961 std::sort(boundaries.begin(), boundaries.end());
962 boundaries.erase(std::unique(boundaries.begin(), boundaries.end()),
963 boundaries.end());
964
965 bool previous_was_conflict = false;
967 std::string previous_module;
968 for (size_t index = 0; index + 1 < boundaries.size(); ++index) {
969 const uint32_t segment_start = boundaries[index];
970 if (segment_start >= boundaries[index + 1]) {
971 continue;
972 }
973
974 const AddressOwnership ownership = ClassifyAddress(segment_start);
975 if (!IsAsmOwned(ownership)) {
976 previous_was_conflict = false;
977 continue;
978 }
979
980 std::string module;
981 if (ownership == AddressOwnership::kHookPatched) {
982 if (const ProtectedRegion* protected_region =
983 FindProtectedRegion(segment_start);
984 protected_region != nullptr) {
985 module = protected_region->module;
986 }
987 } else {
988 const uint8_t bank = static_cast<uint8_t>((segment_start >> 16) & 0xFF);
989 if (const auto bank_it = owned_banks_.find(bank);
990 bank_it != owned_banks_.end()) {
991 module = bank_it->second.ownership_note;
992 }
993 }
994
995 if (!previous_was_conflict || ownership != previous_ownership ||
996 module != previous_module) {
997 conflicts.push_back({segment_start, ownership, module});
998 }
999 previous_was_conflict = true;
1000 previous_ownership = ownership;
1001 previous_module = std::move(module);
1002 }
1003 }
1004
1005 return conflicts;
1006}
1007
1008std::vector<WriteConflict> HackManifest::AnalyzePcWriteRanges(
1009 const std::vector<std::pair<uint32_t, uint32_t>>& pc_ranges) const {
1010 if (!loaded_) {
1011 return {};
1012 }
1013
1014 std::vector<std::pair<uint32_t, uint32_t>> snes_ranges;
1015 snes_ranges.reserve(pc_ranges.size());
1016
1017 for (const auto& range : pc_ranges) {
1018 uint32_t pc_start = range.first;
1019 const uint32_t pc_end = range.second;
1020 if (pc_end <= pc_start) {
1021 continue;
1022 }
1023
1024 // Split at LoROM bank boundaries (0x8000 bytes per bank segment in PC
1025 // space). PcToSnes() is linear within these segments.
1026 while (pc_start < pc_end) {
1027 const uint32_t next_boundary = (pc_start & ~0x7FFFu) + 0x8000u;
1028 const uint32_t seg_end = std::min(pc_end, next_boundary);
1029 const uint32_t seg_len = seg_end - pc_start;
1030 const uint32_t snes_start = PcToSnes(pc_start);
1031 const uint32_t snes_end = snes_start + seg_len;
1032 snes_ranges.emplace_back(snes_start, snes_end);
1033 pc_start = seg_end;
1034 }
1035 }
1036
1037 return AnalyzeWriteRanges(snes_ranges);
1038}
1039
1040std::string HackManifest::GetSramVariableName(uint32_t address) const {
1041 auto iter = sram_map_.find(address);
1042 if (iter == sram_map_.end())
1043 return "";
1044 return iter->second.name;
1045}
1046
1047bool HackManifest::IsExpandedMessage(uint16_t message_id) const {
1048 return message_id >= message_layout_.first_expanded_id &&
1049 message_id <= message_layout_.last_expanded_id;
1050}
1051
1053 DungeonStreamType stream) const {
1054 const auto iter = dungeon_stream_layouts_.find(stream);
1055 return iter == dungeon_stream_layouts_.end() ? nullptr : &iter->second;
1056}
1057
1058// ============================================================================
1059// Project Registry Loading
1060// ============================================================================
1061
1062absl::Status HackManifest::LoadProjectRegistry(const std::string& code_folder) {
1063 namespace fs = std::filesystem;
1064
1066 fs::path base(code_folder);
1067
1068 // Look for data files in Docs/Dev/Planning/ (generated output location)
1069 fs::path planning = base / "Docs" / "Dev" / "Planning";
1070
1071 // ── Load dungeons.json ──────────────────────────────────────────────────
1072 fs::path dungeons_path = planning / "dungeons.json";
1073 if (fs::exists(dungeons_path)) {
1074 std::ifstream file(dungeons_path);
1075 if (file.is_open()) {
1076 std::stringstream buffer;
1077 buffer << file.rdbuf();
1078 try {
1079 Json root = Json::parse(buffer.str());
1080 if (root.contains("dungeons") && root["dungeons"].is_array()) {
1081 for (const auto& dj : root["dungeons"]) {
1082 DungeonEntry entry;
1083 entry.id = dj.value("id", "");
1084 entry.name = dj.value("name", "");
1085 entry.vanilla_name = dj.value("vanilla_name", "");
1086
1087 // Rooms
1088 if (dj.contains("rooms") && dj["rooms"].is_array()) {
1089 for (const auto& rj : dj["rooms"]) {
1090 DungeonRoom room;
1091 std::string id_str = rj.value("id", "0x00");
1092 auto parsed = ParseHexAddress(id_str);
1093 room.id = parsed.ok() ? static_cast<int>(*parsed) : 0;
1094 room.name = rj.value("name", "");
1095 room.floor = rj.value("floor", "");
1096 room.has_grid_position =
1097 rj.contains("grid_row") && rj.contains("grid_col");
1098 room.grid_row = rj.value("grid_row", 0);
1099 room.grid_col = rj.value("grid_col", 0);
1100 room.type = rj.value("type", "normal");
1101 room.palette = rj.value("palette", 0);
1102 room.blockset = rj.value("blockset", 0);
1103 room.spriteset = rj.value("spriteset", 0);
1104 room.tag1 = static_cast<uint8_t>(rj.value("tag1", 0));
1105 room.tag2 = static_cast<uint8_t>(rj.value("tag2", 0));
1106 entry.rooms.push_back(std::move(room));
1107 }
1108 }
1109
1110 // Stairs
1111 if (dj.contains("stairs") && dj["stairs"].is_array()) {
1112 for (const auto& sj : dj["stairs"]) {
1113 DungeonConnection conn;
1114 std::string from_str = sj.value("from", "0x00");
1115 std::string to_str = sj.value("to", "0x00");
1116 auto from_parsed = ParseHexAddress(from_str);
1117 auto to_parsed = ParseHexAddress(to_str);
1118 conn.from_room =
1119 from_parsed.ok() ? static_cast<int>(*from_parsed) : 0;
1120 conn.to_room =
1121 to_parsed.ok() ? static_cast<int>(*to_parsed) : 0;
1122 conn.label = sj.value("label", "");
1123 entry.stairs.push_back(std::move(conn));
1124 }
1125 }
1126
1127 // Holewarps
1128 if (dj.contains("holewarps") && dj["holewarps"].is_array()) {
1129 for (const auto& hj : dj["holewarps"]) {
1130 DungeonConnection conn;
1131 std::string from_str = hj.value("from", "0x00");
1132 std::string to_str = hj.value("to", "0x00");
1133 auto from_parsed = ParseHexAddress(from_str);
1134 auto to_parsed = ParseHexAddress(to_str);
1135 conn.from_room =
1136 from_parsed.ok() ? static_cast<int>(*from_parsed) : 0;
1137 conn.to_room =
1138 to_parsed.ok() ? static_cast<int>(*to_parsed) : 0;
1139 conn.label = hj.value("label", "");
1140 entry.holewarps.push_back(std::move(conn));
1141 }
1142 }
1143
1144 // Doors
1145 if (dj.contains("doors") && dj["doors"].is_array()) {
1146 for (const auto& doorj : dj["doors"]) {
1147 DungeonConnection conn;
1148 std::string from_str = doorj.value("from", "0x00");
1149 std::string to_str = doorj.value("to", "0x00");
1150 auto from_parsed = ParseHexAddress(from_str);
1151 auto to_parsed = ParseHexAddress(to_str);
1152 conn.from_room =
1153 from_parsed.ok() ? static_cast<int>(*from_parsed) : 0;
1154 conn.to_room =
1155 to_parsed.ok() ? static_cast<int>(*to_parsed) : 0;
1156 conn.label = doorj.value("label", "");
1157 conn.direction = doorj.value("direction", "");
1158 entry.doors.push_back(std::move(conn));
1159 }
1160 }
1161
1162 project_registry_.dungeons.push_back(std::move(entry));
1163 }
1164 }
1165 } catch (const std::exception& exc) {
1166 LOG_WARN("HackManifest", "Failed to parse dungeons.json: %s",
1167 exc.what());
1168 }
1169 }
1170 }
1171
1172 // ── Load overworld.json ─────────────────────────────────────────────────
1173 fs::path overworld_path = planning / "overworld.json";
1174 if (fs::exists(overworld_path)) {
1175 std::ifstream file(overworld_path);
1176 if (file.is_open()) {
1177 std::stringstream buffer;
1178 buffer << file.rdbuf();
1179 try {
1180 Json root = Json::parse(buffer.str());
1181 if (root.contains("areas") && root["areas"].is_array()) {
1182 for (const auto& aj : root["areas"]) {
1183 OverworldArea area;
1184 std::string id_str = aj.value("area_id", "0x00");
1185 auto parsed = ParseHexAddress(id_str);
1186 area.area_id = parsed.ok() ? static_cast<int>(*parsed) : 0;
1187 area.name = aj.value("name", "");
1188 area.world = aj.value("world", "");
1189 area.grid_row = aj.value("grid_row", 0);
1190 area.grid_col = aj.value("grid_col", 0);
1191 project_registry_.overworld_areas.push_back(std::move(area));
1192 }
1193 }
1194 } catch (const std::exception& exc) {
1195 LOG_WARN("HackManifest", "Failed to parse overworld.json: %s",
1196 exc.what());
1197 }
1198 }
1199 }
1200
1201 // ── Load resource labels ────────────────────────────────────────────────
1202 // Priority 1: Unified oracle_resource_labels.json (all types)
1203 // Priority 2: Legacy oracle_room_labels.json (room type only)
1204 fs::path unified_path = planning / "oracle_resource_labels.json";
1205 fs::path legacy_path = planning / "oracle_room_labels.json";
1206
1207 auto normalize_label_id = [](const std::string& raw) -> std::string {
1208 // Project resource_labels keys are decimal strings (std::to_string(id)).
1209 // Support either decimal ("57") or explicit hex ("0x39") here.
1210 std::string s = raw;
1211 while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
1212 s.erase(s.begin());
1213 }
1214 while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
1215 s.pop_back();
1216 }
1217 if (s.empty()) {
1218 return raw;
1219 }
1220
1221 int base = 10;
1222 if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
1223 base = 16;
1224 s = s.substr(2);
1225 if (s.empty()) {
1226 return raw;
1227 }
1228 }
1229
1230 try {
1231 size_t idx = 0;
1232 const unsigned long value = std::stoul(s, &idx, base);
1233 if (idx != s.size()) {
1234 return raw;
1235 }
1236 if (value > static_cast<unsigned long>(std::numeric_limits<int>::max())) {
1237 return raw;
1238 }
1239 return std::to_string(static_cast<int>(value));
1240 } catch (...) {
1241 return raw;
1242 }
1243 };
1244
1245 if (fs::exists(unified_path)) {
1246 std::ifstream file(unified_path);
1247 if (file.is_open()) {
1248 std::stringstream buffer;
1249 buffer << file.rdbuf();
1250 try {
1251 Json root = Json::parse(buffer.str());
1252 // Parse all type sections (room, sprite, item, entrance, etc.)
1253 const std::vector<std::string> label_types = {
1254 "room", "sprite", "item", "entrance", "overworld_map", "music"};
1255 for (const auto& type_key : label_types) {
1256 if (root.contains(type_key) && root[type_key].is_object()) {
1257 for (const auto& [key, value] : root[type_key].items()) {
1258 if (value.is_string()) {
1259 const std::string normalized_key = normalize_label_id(key);
1261 .all_resource_labels[type_key][normalized_key] =
1262 value.get<std::string>();
1263 }
1264 }
1265 }
1266 }
1267 } catch (const std::exception& exc) {
1268 LOG_WARN("HackManifest",
1269 "Failed to parse oracle_resource_labels.json: %s", exc.what());
1270 }
1271 }
1272 } else if (fs::exists(legacy_path)) {
1273 // Backward compat: load room-only labels from legacy file
1274 std::ifstream file(legacy_path);
1275 if (file.is_open()) {
1276 std::stringstream buffer;
1277 buffer << file.rdbuf();
1278 try {
1279 Json root = Json::parse(buffer.str());
1280 if (root.contains("resource_labels") &&
1281 root["resource_labels"].contains("room")) {
1282 for (const auto& [key, value] :
1283 root["resource_labels"]["room"].items()) {
1284 const std::string normalized_key = normalize_label_id(key);
1285 project_registry_.all_resource_labels["room"][normalized_key] =
1286 value.get<std::string>();
1287 }
1288 }
1289 } catch (const std::exception& exc) {
1290 LOG_WARN("HackManifest", "Failed to parse oracle_room_labels.json: %s",
1291 exc.what());
1292 }
1293 }
1294 }
1295
1296 // `dungeons.json` is the canonical Oracle room/dungeon structure. Mirror its
1297 // room names into the generic resource-label registry so opening an Oracle
1298 // project can populate YazeProject::resource_labels and serialize those names
1299 // into the project file. This intentionally runs after oracle_*_labels.json
1300 // so stale generated label bundles cannot mask the current dungeon registry.
1301 size_t dungeon_room_labels = 0;
1302 for (const auto& dungeon : project_registry_.dungeons) {
1303 for (const auto& room : dungeon.rooms) {
1304 if (room.id >= 0 && !room.name.empty()) {
1305 project_registry_.all_resource_labels["room"][std::to_string(room.id)] =
1306 room.name;
1307 ++dungeon_room_labels;
1308 }
1309 }
1310 }
1311 if (dungeon_room_labels > 0) {
1312 LOG_DEBUG("HackManifest",
1313 "Mirrored %zu dungeon room names into project resource labels",
1314 dungeon_room_labels);
1315 }
1316
1317 // ── Load story events ──────────────────────────────────────────────────
1318 fs::path story_events_path = planning / "story_events.json";
1319 if (fs::exists(story_events_path)) {
1320 auto story_status =
1321 project_registry_.story_events.LoadFromJson(story_events_path.string());
1322 if (story_status.ok()) {
1324 if (oracle_progression_state_.has_value()) {
1326 } else {
1327 // Default to an initial "no progress" state so the graph renders with
1328 // sensible locked/available coloring even before live SRAM is wired up.
1329 project_registry_.story_events.UpdateStatus(/*crystal_bitfield=*/0,
1330 /*game_state=*/0);
1331 }
1332 LOG_DEBUG("HackManifest", "Loaded story events: %zu nodes, %zu edges",
1335 } else {
1336 LOG_WARN("HackManifest", "Failed to load story_events.json: %s",
1337 std::string(story_status.message()).c_str());
1338 }
1339 }
1340
1341 size_t total_labels = 0;
1342 for (const auto& [type, labels] : project_registry_.all_resource_labels) {
1343 total_labels += labels.size();
1344 }
1345
1346 if (!project_registry_.dungeons.empty() ||
1347 !project_registry_.overworld_areas.empty() || total_labels > 0) {
1348 LOG_DEBUG("HackManifest",
1349 "Loaded project registry: %zu dungeons, %zu overworld areas, "
1350 "%zu resource labels (%zu types)",
1352 project_registry_.overworld_areas.size(), total_labels,
1354 }
1355
1356 return absl::OkStatus();
1357}
1358
1366
1370 project_registry_.story_events.UpdateStatus(/*crystal_bitfield=*/0,
1371 /*game_state=*/0);
1372 }
1373}
1374
1375} // 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
bool is_string() const
Definition json.h:59
items_view items()
Definition json.h:88
T get() const
Definition json.h:49
bool empty() const
Definition json.h:62
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
bool IsEditorManaged(uint32_t address) const
Check if an address is explicitly managed by yaze.
BuildPipeline build_pipeline_
std::optional< AddressOwnership > GetBankOwnership(uint8_t bank) const
Get the bank ownership for a given bank number.
std::optional< OracleProgressionState > oracle_progression_state_
std::unordered_map< uint32_t, SramVariable > sram_map_
std::string GetSramVariableName(uint32_t address) const
std::unordered_map< uint8_t, RoomTagEntry > room_tag_map_
std::vector< SnesAddressRange > editor_managed_regions_
std::unordered_map< DungeonStreamType, DungeonStreamLayout > dungeon_stream_layouts_
const std::vector< ProtectedRegion > & protected_regions() const
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
bool IsProtected(uint32_t address) const
Check if an address is in a protected region.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
absl::Status LoadFromString(const std::string &json_content)
Load manifest from a JSON string.
MessageLayout message_layout_
std::string GetRoomTagLabel(uint8_t tag_id) const
Get the human-readable label for a room tag ID.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
void SetOracleProgressionState(const OracleProgressionState &state)
const ProtectedRegion * FindProtectedRegion(uint32_t address) const
const std::vector< SnesAddressRange > & editor_managed_regions() const
std::vector< RoomTagEntry > room_tags_
std::vector< FeatureFlag > feature_flags_
const std::vector< StoryEventNode > & nodes() const
void AutoLayout()
Compute layout positions using topological sort + layered positioning.
void UpdateStatus(uint8_t crystal_bitfield, uint8_t game_state)
Update node completion status based on SRAM state.
bool loaded() const
Check if the graph has been loaded.
const std::vector< StoryEdge > & edges() const
absl::Status LoadFromJson(const std::string &path)
Load the graph from a JSON file.
#define LOG_DEBUG(category, format,...)
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)
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)
absl::StatusOr< uint32_t > ParsePointerCount(const Json &object, const std::string &path)
const char * DungeonStreamTypeToString(DungeonStreamType stream)
absl::StatusOr< uint32_t > ParseLoRomAddress(const Json &object, const std::string &key, const std::string &path)
absl::StatusOr< int > ParseManifestVersion(const Json &root)
absl::StatusOr< std::unordered_map< DungeonStreamType, DungeonStreamLayout > > ParseDungeonStreamLayouts(const Json &root)
bool PcRangeContains(const SnesAddressRange &outer, const SnesAddressRange &inner)
absl::StatusOr< DungeonStreamLayout > ParseDungeonStreamLayout(DungeonStreamType stream, const Json &json)
absl::StatusOr< std::vector< SnesAddressRange > > ParseAddressRanges(const Json &object, const std::string &key, const std::string &path)
absl::StatusOr< std::vector< SnesAddressRange > > ParseEditorManagedRegions(const Json &root, int manifest_version)
absl::StatusOr< DungeonPointerEncoding > ParsePointerEncoding(const Json &object, const std::string &path)
std::string AddressOwnershipToString(AddressOwnership ownership)
DungeonStreamType
Dungeon stream kinds with allocator layouts in the hack manifest.
AddressOwnership
Ownership classification for ROM addresses and banks.
nlohmann::json json
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
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.
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 half-open [start, end) range of canonical LoROM SNES addresses.
A custom SRAM variable definition.