SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
gen_ds1_world_events.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Build `db_ds1/world_events.json` — Dark Souls 1 world flags, by what they record.
3
4Source: `test/flag_data/files8/dark_souls_1_flags.csv`, extracted from
5`HotPocketRemix/DSEventScriptTools`, which commits every vanilla PtDE `.emevd` unpacked
6AND the `.emeld` files carrying FromSoft's own event names. The extraction attributes each
7`Set Event Flag` instruction to its enclosing `Event ID: N` header, then looks N up in the
8matching `.names.txt`.
9
10READ THIS BEFORE TRUSTING A NUMBER HERE. Everything in this table is `derived`, and it is
11shipped on the strength of its source rather than on a save this repo has verified it
12against. What was checked is that it decodes (398/398 through the repo's own DS1
13addressing) and that it discriminates -- 140 set on the NG+2 all-bonfires mule against 110
14on a mid-game character and 85 on an all-items mule, which is the right order. What was
15NOT established is that each family means what its name says:
16
17 * `lever` reads 0 of 11 on a save with every bonfire lit and 23 bosses dead. A flag that
18 must be true there and is not is TRANSIENT -- set while the event runs, cleared after.
19 The same signature got DS1's "Boss Fight" category dropped from `known_flags.json`.
20 * `boss_defeat` reads 33 on a mule with four bosses proven dead, so it counts FLAGS and
21 not bosses: arena, phase and cutscene flags ride in the same family.
22
23Both are printed anyway, with the section note saying so, because the source is real and a
24count that moves is better than a blank. Neither number is a boss count and neither should
25be read as one. Fixing this means splitting the families per row against the `.emevd` text.
26
27Names are FromSoft's own, VERBATIM JAPANESE. They are not translated here and they are not
28rendered -- the section prints per-family counts only. Guessing at a translation would be
29inventing a label, and a Japanese string in an English report teaches a reader nothing.
30
31Run from repo root: python3 tools/gen_ds1_world_events.py
32"""
33
34import csv
35import json
36import os
37import sys
38
39BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
40SRC = os.path.join(BASE, "test", "flag_data", "files8", "dark_souls_1_flags.csv")
41OUT = os.path.join(BASE, "db_ds1", "world_events.json")
42
43# What each family slug is called in the report. A family with no entry keeps its slug
44# title-cased, so a source that grows a new one degrades to something readable.
45TITLE = {
46 "absolution": "Absolution",
47 "bell": "Bells",
48 "bonfire": "Bonfire events",
49 "boss_defeat": "Boss-fight flags",
50 "boss_event": "Boss events",
51 "covenant": "Covenant events",
52 "door": "Doors",
53 "elevator": "Elevators",
54 "enemy_spawn": "One-time enemy spawns",
55 "lever": "Levers",
56 "npc_death": "NPC deaths",
57 "npc_hostile": "NPCs turned hostile",
58 "seal": "Seals",
59 "trap_switch": "Trap switches",
60 "treasure_chest": "Treasure chests",
61}
62
63
64## @brief Byte offset and uint32 mask for an 8-digit DS1 flag, or None.
65# @details `G AAA S NNN`, MSB-first within the word. The same expression
66# `db_ds1/known_flags.json` is generated with, and it reproduces all twelve
67# independently-derived boss flags exactly.
68def address(fid, groups, areas):
69 text = str(fid).zfill(8)
70 if len(text) != 8:
71 return None
72 group, area, section, number = int(text[0]), text[1:4], int(text[4]), int(text[5:8])
73 if group not in groups or area not in areas:
74 return None
75 off = (
76 groups[group]
77 + areas[area] * 0x500
78 + section * 128
79 + (number - number % 32) // 8
80 )
81 return off, 0x80000000 >> (number % 32)
82
83
85 path = os.path.join(BASE, "test", "flag_data", "files2", "ds1_flag_addressing.csv")
86 groups, areas = {}, {}
87 with open(path, encoding="utf-8-sig") as f:
88 for row in csv.DictReader(f):
89 table = (row.get("table") or "").strip()
90 key, value = (
91 (row.get("key") or "").strip(),
92 (row.get("value") or "").strip(),
93 )
94 if table.startswith("group"):
95 groups[int(key)] = int(value, 16)
96 elif table.startswith("area"):
97 areas[key] = int(value)
98 return groups, areas
99
100
101def main():
102 groups, areas = addressing()
103 table, seen, undecodable = {}, set(), 0
104 with open(SRC, encoding="utf-8") as f:
105 for r in csv.DictReader(f):
106 fid = r["flag_id"]
107 if not fid.isdigit():
108 continue
109 addr = address(int(fid), groups, areas)
110 if addr is None:
111 undecodable += 1
112 continue
113 # The source lists a flag once per instruction that sets it, so the same flag
114 # arrives several times. One flag is one thing that happened.
115 key = (r["family"], addr)
116 if key in seen:
117 continue
118 seen.add(key)
119 title = TITLE.get(r["family"], r["family"].replace("_", " ").title())
120 table.setdefault(title, []).append([addr[0], addr[1], r["name"]])
121 if not table:
122 sys.exit("no rows survived -- check the source CSV")
123 for rows in table.values():
124 rows.sort()
125 with open(OUT, "w", encoding="utf-8") as f:
126 json.dump(dict(sorted(table.items())), f, ensure_ascii=False, indent=1)
127 f.write("\n")
128 total = sum(len(v) for v in table.values())
129 print(
130 f"wrote {OUT}: {total} flags in {len(table)} families ({undecodable} undecodable)"
131 )
132
133
134if __name__ == "__main__":
135 main()
address(fid, groups, areas)
Byte offset and uint32 mask for an 8-digit DS1 flag, or None.