11#include "absl/status/status.h"
12#include "absl/strings/str_format.h"
13#include "nlohmann/json.hpp"
18using json = nlohmann::json;
22static std::vector<StoryFlag> ParseFlags(
const json& arr) {
23 std::vector<StoryFlag> flags;
24 if (!arr.is_array())
return flags;
25 for (
const auto& item : arr) {
27 flag.
name = item.value(
"name",
"");
28 flag.
value = item.value(
"value",
"");
29 flag.
reg = item.value(
"register",
"");
30 flag.
bit = item.value(
"bit", -1);
31 flag.
operation = item.value(
"operation",
"");
32 flags.push_back(flag);
37static std::vector<StoryLocation> ParseLocations(
const json& arr) {
38 std::vector<StoryLocation> locations;
39 if (!arr.is_array())
return locations;
40 for (
const auto& item : arr) {
42 loc.name = item.value(
"name",
"");
43 loc.entrance_id = item.value(
"entrance_id",
"");
44 loc.overworld_id = item.value(
"overworld_id",
"");
45 loc.special_world_id = item.value(
"special_world_id",
"");
46 loc.room_id = item.value(
"room_id",
"");
47 locations.push_back(loc);
52static std::vector<std::string> ParseStringArray(
const json& arr) {
53 std::vector<std::string> result;
54 if (!arr.is_array())
return result;
55 for (
const auto& item : arr) {
56 if (item.is_string()) {
57 result.push_back(item.get<std::string>());
63static int ParseIntFlexible(
const json& obj,
const char* key,
int def) {
64 if (!obj.is_object() || !obj.contains(key)) {
67 const auto& v = obj.at(key);
68 if (v.is_number_integer()) {
71 if (v.is_number_unsigned()) {
72 const auto u = v.get<
unsigned int>();
73 return (u >
static_cast<unsigned int>(std::numeric_limits<int>::max()))
75 :
static_cast<int>(u);
80 const std::string s = v.get<std::string>();
81 const int parsed = std::stoi(s, &idx, 0);
82 if (idx == s.size()) {
91static std::vector<std::string> ParseScriptArray(
const json& arr) {
92 std::vector<std::string> result;
93 if (!arr.is_array())
return result;
95 for (
const auto& item : arr) {
96 if (item.is_string()) {
97 result.push_back(item.get<std::string>());
100 if (!item.is_object()) {
105 if (item.contains(
"ref") && item[
"ref"].is_string()) {
106 result.push_back(item[
"ref"].get<std::string>());
110 if (item.contains(
"script_id") && item[
"script_id"].is_string()) {
111 result.push_back(item[
"script_id"].get<std::string>());
114 if (item.contains(
"id") && item[
"id"].is_string()) {
115 result.push_back(item[
"id"].get<std::string>());
119 const std::string file = item.value(
"file",
"");
120 const std::string symbol = item.value(
"symbol",
"");
121 if (!symbol.empty()) {
123 result.push_back(file +
":" + symbol);
125 result.push_back(symbol);
131 const int line = ParseIntFlexible(item,
"line", -1);
132 if (line > 0 && !file.empty()) {
133 result.push_back(file +
":" + std::to_string(line));
140static uint32_t ParseUintFlexible(
const json& obj,
const char* key,
142 if (!obj.is_object() || !obj.contains(key)) {
145 const auto& v = obj.at(key);
146 if (v.is_number_unsigned()) {
147 return v.get<uint32_t>();
149 if (v.is_number_integer()) {
150 const int64_t i = v.get<int64_t>();
151 return (i < 0) ? def :
static_cast<uint32_t
>(i);
156 const std::string s = v.get<std::string>();
157 const unsigned long parsed = std::stoul(s, &idx, 0);
158 if (idx == s.size()) {
159 return static_cast<uint32_t
>(parsed);
167static std::vector<StoryPredicate> ParsePredicates(
const json& arr) {
168 std::vector<StoryPredicate> preds;
169 if (!arr.is_array())
return preds;
170 for (
const auto& item : arr) {
171 if (!item.is_object())
continue;
173 p.reg = item.value(
"register",
"");
175 p.reg = item.value(
"reg",
"");
177 p.op = item.value(
"op",
"");
178 p.value = ParseIntFlexible(item,
"value", 0);
179 p.bit = ParseIntFlexible(item,
"bit", -1);
182 p.mask = ParseUintFlexible(item,
"mask", 0);
183 if (p.mask == 0 && item.contains(
"value")) {
184 p.mask =
static_cast<uint32_t
>(ParseUintFlexible(item,
"value", 0));
187 preds.push_back(std::move(p));
193 std::ifstream file(path);
194 if (!file.is_open()) {
195 return absl::NotFoundError(
"Cannot open story events file: " + path);
197 std::stringstream buffer;
198 buffer << file.rdbuf();
205 root = json::parse(json_content);
206 }
catch (
const json::parse_error& e) {
207 return absl::InvalidArgumentError(
208 std::string(
"JSON parse error: ") + e.what());
214 std::unordered_map<std::string, std::string> id_aliases;
216 auto add_alias = [&](
const std::string& alias,
const std::string& canonical,
217 const char* field_name) -> absl::Status {
219 return absl::OkStatus();
221 auto [it, inserted] = id_aliases.emplace(alias, canonical);
222 if (!inserted && it->second != canonical) {
223 return absl::InvalidArgumentError(absl::StrFormat(
224 "Conflicting story event %s alias '%s' maps to both '%s' and '%s'",
225 field_name, alias, it->second, canonical));
227 return absl::OkStatus();
231 if (root.contains(
"events") && root[
"events"].is_array()) {
232 size_t event_index = 0;
233 for (
const auto& item : root[
"events"]) {
235 const std::string legacy_id = item.value(
"id",
"");
236 const std::string stable_id = item.value(
"stable_id",
"");
237 const std::string key_id = item.value(
"key",
"");
238 node.
id = !stable_id.empty()
240 : (!legacy_id.empty() ? legacy_id : key_id);
241 if (node.
id.empty()) {
242 return absl::InvalidArgumentError(absl::StrFormat(
243 "Story event at index %zu is missing required id (set stable_id or id)",
247 return absl::InvalidArgumentError(
248 absl::StrFormat(
"Duplicate story event id: %s", node.
id));
250 node.
name = item.value(
"name",
"");
251 node.
flags = ParseFlags(item.value(
"flags", json::array()));
252 node.
locations = ParseLocations(item.value(
"locations", json::array()));
253 node.
scripts = ParseScriptArray(item.value(
"scripts", json::array()));
254 node.
text_ids = ParseStringArray(item.value(
"text_ids", json::array()));
256 ParsePredicates(item.value(
"completed_when", json::array()));
258 ParseStringArray(item.value(
"dependencies", json::array()));
259 node.
unlocks = ParseStringArray(item.value(
"unlocks", json::array()));
260 node.
evidence = item.value(
"evidence",
"");
262 node.
notes = item.value(
"notes",
"");
270 nodes_.push_back(std::move(node));
276 if (root.contains(
"edges") && root[
"edges"].is_array()) {
277 for (
const auto& item : root[
"edges"]) {
279 edge.
from = item.value(
"from",
"");
280 edge.
to = item.value(
"to",
"");
281 edge.
type = item.value(
"type",
"dependency");
282 edges_.push_back(std::move(edge));
286 auto canonicalize_id = [&](
const std::string& raw) -> std::string {
287 auto it = id_aliases.find(raw);
288 return it != id_aliases.end() ? it->second : raw;
292 for (
auto& node :
nodes_) {
293 for (
auto& dep : node.dependencies) {
294 dep = canonicalize_id(dep);
296 for (
auto& unlock : node.unlocks) {
297 unlock = canonicalize_id(unlock);
300 for (
auto& edge :
edges_) {
301 edge.from = canonicalize_id(edge.from);
302 edge.to = canonicalize_id(edge.to);
306 for (
const auto& node :
nodes_) {
307 for (
const auto& dep : node.dependencies) {
309 return absl::InvalidArgumentError(absl::StrFormat(
310 "Story event '%s' has unknown dependency '%s'", node.id, dep));
313 for (
const auto& unlock : node.unlocks) {
314 if (!unlock.empty() && !
node_index_.contains(unlock)) {
315 return absl::InvalidArgumentError(absl::StrFormat(
316 "Story event '%s' has unknown unlock '%s'", node.id, unlock));
320 for (
const auto& edge :
edges_) {
321 if (!edge.from.empty() && !
node_index_.contains(edge.from)) {
322 return absl::InvalidArgumentError(
323 absl::StrFormat(
"Story edge has unknown from node '%s'", edge.from));
325 if (!edge.to.empty() && !
node_index_.contains(edge.to)) {
326 return absl::InvalidArgumentError(
327 absl::StrFormat(
"Story edge has unknown to node '%s'", edge.to));
331 return absl::OkStatus();
337 if (
nodes_.empty())
return;
340 std::unordered_map<std::string, int> in_degree;
341 std::unordered_map<std::string, std::vector<std::string>> adj;
343 for (
const auto& node :
nodes_) {
344 in_degree[node.id] = 0;
347 for (
const auto& edge :
edges_) {
348 adj[edge.from].push_back(edge.to);
349 in_degree[edge.to]++;
353 std::queue<std::string> queue;
354 std::unordered_map<std::string, int> layer;
356 for (
const auto& [
id, deg] : in_degree) {
364 while (!queue.empty()) {
365 std::string current = queue.front();
368 for (
const auto& next : adj[current]) {
369 int new_layer = layer[current] + 1;
370 if (layer.find(next) == layer.end() || new_layer > layer[next]) {
371 layer[next] = new_layer;
374 if (in_degree[next] == 0) {
376 max_layer = std::max(max_layer, layer[next]);
382 std::vector<std::vector<size_t>> layers(max_layer + 1);
383 for (
size_t i = 0; i <
nodes_.size(); ++i) {
384 int l = layer.count(
nodes_[i].
id) ? layer[
nodes_[i].id] : 0;
385 layers[l].push_back(i);
389 constexpr float kLayerSpacing = 200.0f;
390 constexpr float kNodeSpacing = 120.0f;
392 for (
int l = 0; l <= max_layer; ++l) {
393 const size_t count = layers[l].size();
394 if (count == 0)
continue;
396 float total_height =
static_cast<float>(count - 1) * kNodeSpacing;
397 float start_y = -total_height / 2.0f;
399 for (
size_t j = 0; j < count; ++j) {
400 nodes_[layers[l][j]].pos_x =
static_cast<float>(l) * kLayerSpacing;
401 nodes_[layers[l][j]].pos_y =
402 start_y +
static_cast<float>(j) * kNodeSpacing;
413 out.reserve(input.size());
414 for (
unsigned char ch : input) {
415 if (std::isalnum(ch)) {
416 out.push_back(
static_cast<char>(std::toupper(ch)));
423 std::string_view reg) {
425 if (key ==
"CRYSTALS" || key ==
"CRYSTAL" || key ==
"CRYSTALBITFIELD") {
428 if (key ==
"GAMESTATE" || key ==
"GAME") {
431 if (key ==
"OOSPROG") {
434 if (key ==
"OOSPROG2") {
437 if (key ==
"SIDEQUEST" || key ==
"SIDEQUESTS") {
440 if (key ==
"PENDANTS" || key ==
"PENDANT") {
449 if (!reg_val_opt.has_value()) {
452 const uint32_t reg_val = *reg_val_opt;
455 if (op ==
"BITSET") {
456 if (p.
bit < 0 || p.
bit >= 32)
return false;
457 return (reg_val & (1u <<
static_cast<uint32_t
>(p.
bit))) != 0;
459 if (op ==
"BITCLEAR") {
460 if (p.
bit < 0 || p.
bit >= 32)
return false;
461 return (reg_val & (1u <<
static_cast<uint32_t
>(p.
bit))) == 0;
463 if (op ==
"MASKANY") {
464 return p.
mask != 0 && (reg_val & p.
mask) != 0;
466 if (op ==
"MASKALL") {
471 const int lhs =
static_cast<int>(reg_val);
472 const int rhs = p.
value;
473 if (p.
op ==
"==" || op ==
"EQ")
return lhs == rhs;
474 if (p.
op ==
"!=" || op ==
"NE")
return lhs != rhs;
475 if (p.
op ==
">=" || op ==
"GE")
return lhs >= rhs;
476 if (p.
op ==
"<=" || op ==
"LE")
return lhs <= rhs;
477 if (p.
op ==
">" || op ==
"GT")
return lhs > rhs;
478 if (p.
op ==
"<" || op ==
"LT")
return lhs < rhs;
486 uint8_t game_state) {
495 std::unordered_map<std::string, bool> completed_set;
496 for (
const auto&
id : completed) {
497 completed_set[id] =
true;
500 for (
auto& node :
nodes_) {
501 if (completed_set.count(node.id)) {
507 bool all_deps_met =
true;
508 for (
const auto& dep : node.dependencies) {
509 if (!completed_set.count(dep)) {
510 all_deps_met =
false;
521 uint8_t crystal_bitfield, uint8_t game_state)
const {
530 std::vector<std::string> completed;
532 for (
const auto& node :
nodes_) {
534 if (!node.completed_when.empty()) {
536 for (
const auto& p : node.completed_when) {
537 if (!EvaluatePredicate(p, state)) {
543 completed.push_back(node.id);
549 bool is_completed =
false;
551 for (
const auto& flag : node.flags) {
553 if (flag.name ==
"IntroState" || flag.name ==
"GameState") {
556 required = std::stoi(flag.value);
566 if (flag.name.find(
"Crystal") != std::string::npos ||
567 flag.name.find(
"Fortress") != std::string::npos) {
576 if (node.id ==
"EV-001" && state.
game_state >= 1) {
579 if ((node.id ==
"EV-002" || node.id ==
"EV-003") && state.
game_state >= 1) {
582 if (node.id ==
"EV-005" && state.
game_state >= 2) {
585 if (node.id ==
"EV-008" && state.
game_state >= 3) {
590 completed.push_back(node.id);
600 return &
nodes_[it->second];
void AutoLayout()
Compute layout positions using topological sort + layered positioning.
std::vector< StoryEventNode > nodes_
std::vector< StoryEdge > edges_
const StoryEventNode * GetNode(const std::string &id) const
void UpdateStatus(uint8_t crystal_bitfield, uint8_t game_state)
Update node completion status based on SRAM state.
std::vector< std::string > GetCompletedNodes(uint8_t crystal_bitfield, uint8_t game_state) const
Get IDs of events that are completed based on SRAM state.
absl::Status LoadFromJson(const std::string &path)
Load the graph from a JSON file.
std::unordered_map< std::string, size_t > node_index_
absl::Status LoadFromString(const std::string &json_content)
Load the graph from a JSON string.
std::string NormalizeRegKey(std::string_view input)
bool EvaluatePredicate(const StoryPredicate &p, const OracleProgressionState &state)
std::optional< uint32_t > GetRegisterValue(const OracleProgressionState &state, std::string_view reg)
#define RETURN_IF_ERROR(expr)
Oracle of Secrets game progression state parsed from SRAM.
A directed edge in the story event graph.
A node in the Oracle story event graph.
std::vector< StoryLocation > locations
std::vector< std::string > unlocks
std::vector< std::string > dependencies
std::vector< std::string > scripts
std::vector< std::string > text_ids
std::vector< StoryFlag > flags
std::string last_verified
std::vector< StoryPredicate > completed_when
A flag set or cleared by a story event.
A predicate for determining event completion from SRAM state.