SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
gen_ds3_enemies.py
Go to the documentation of this file.
1
#!/usr/bin/env python3
2
"""Build `db_ds3/enemies.json` — the one-time enemies whose death Dark Souls III flags.
3
4
Source: `test/flag_data/files7/ds3_onetime_enemy_flags.csv`, itself extracted from the
5
`.emevd` committed in `thefifthmatt/SoulsRandomizers` `dist/Base/`. The rows are calls to
6
`common_func` templates that take a death flag and an entity id -- `20005340`, `20005341`,
7
`20005342`, `20000343` (the mimics), `20005416`, `20005061`, `20005760`, plus map-local
8
events that gate on a flag at start and set it after a death check -- those are deduped by
9
entity id, for the reason given at the dedupe.
10
11
WHY THIS IS TRUSTED, since a table of ids is worth nothing on its own. Run against a
12
79-save ladder in play-time order the count climbs 3 -> 98 with ZERO regressions, and the
13
per-area breakdown lands where the player actually was: at 12:17:13 it reads Cathedral 9,
14
Road of Sacrifices 7, High Wall 3, Undead Settlement 3, and nothing at all in Irithyll,
15
Archdragon Peak, the Grand Archives or either DLC. A wrong base reads noise, and noise
16
does not do that. `scratch/ds3_enemy_flags.py` is the check; re-run it if this table moves.
17
18
The flags do NOT reset on a new journey -- the NG+2 ending save still reads 98, which is
19
`63xx` boss-victory behaviour rather than per-map `13xxxx8xx` behaviour.
20
21
Five more rows are dropped: their groups (`8`, `9`, `13105`) have no base in the CE
22
table, and an unmapped group is absent rather than guessed, exactly like the three
23
world-pickup groups. `X8` of the mimic template is NOT read -- the packet calls it a drop
24
flag, but all six Irithyll Dungeon copies turn on in one snapshot while their death flags
25
turn on hours apart, which is what an area-enable flag looks like.
26
27
Run from repo root: python3 tools/gen_ds3_enemies.py
28
"""
29
30
import
csv
31
import
json
32
import
os
33
import
re
34
import
sys
35
36
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
37
SRC = os.path.join(BASE,
"test"
,
"flag_data"
,
"files7"
,
"ds3_onetime_enemy_flags.csv"
)
38
BASES = os.path.join(BASE,
"scratch"
,
"ds3flags"
,
"ds3_flag_group_bases.csv"
)
39
OUT = os.path.join(BASE,
"db_ds3"
,
"enemies.json"
)
40
41
# The save sits this far past the Cheat Engine table's memory offset, verified across all
42
# sixteen map groups when the bonfires were derived.
43
SAVE_DELTA = 111
44
45
46
## @brief Flag id -> (distance into the flag region, bit), or None for an unmapped group.
47
# @details The byte counts DOWN inside each uint32 word. Same expression as every other
48
# DS3 table here, and it reproduces all 77 shipped bonfires exactly.
49
def
address
(fid, ks):
50
k = ks.get(fid // 1000)
51
if
k
is
None
:
52
return
None
53
n = fid % 1000
54
return
k * 0x500 + (n >> 5) * 4 + 3 - ((n & 31) >> 3) + SAVE_DELTA, 7 - (n & 7)
55
56
57
## @brief "Crystal Lizard [entity 3200259]" -> "Crystal Lizard".
58
# @details The entity id is how the extractor kept rows apart; it is not a name, and
59
# 47 lines reading "Crystal Lizard [entity ...]" say less than 47 reading the type and a
60
# count. Duplicates ACROSS rows are real and are collapsed at render time by
61
# `count_dupes`, the same way seven separate mimics and four Shura Samurai already are —
62
# but a duplicate INSIDE one row is one flag covering two enemies, so it collapses here.
63
# Rows the source could not name keep their entity id, because "Unnamed enemy" three
64
# times would merge three different enemies into one line.
65
def
clean
(name):
66
name = re.sub(
r"\s*\[entity [\d/]+\]"
,
""
, name).strip()
67
parts, seen = [], set()
68
for
part
in
re.split(
r"\s*(?:\|\||\+)\s*"
, name):
69
part = part.strip()
70
part = re.sub(
r"^entity (\d+)$"
,
r"Unnamed enemy (\1)"
, part)
71
if
part
and
part
not
in
seen:
72
seen.add(part)
73
parts.append(part)
74
return
" + "
.join(parts)
75
76
77
## @brief "Lothric Castle (m30_01_00_00)" -> "Lothric Castle".
78
# @details The map id is what the EMEVD file was called. A reader wants the area, and the
79
# spelling has to match nothing else here -- this is its own section, not a join.
80
def
area_of
(text):
81
return
re.sub(
r"\s*\(m\d\d_\d\d_\d\d_\d\d\)"
,
""
, text).strip()
82
83
84
def
main
():
85
with
open(BASES)
as
f:
86
ks = {
87
int(r[
"flag_group (id//1000)"
]): int(r[
"k (base/0x500)"
])
88
for
r
in
csv.DictReader(f)
89
}
90
table, dropped, local, seen_entities = {}, [], 0, set()
91
with
open(SRC)
as
f:
92
for
r
in
csv.DictReader(f):
93
fid = int(r[
"flag_id"
])
94
# MAP-LOCAL rows are kept, but DEDUPED BY ENTITY ID, and the Nameless King is
95
# why: three separate map-local flags (13200850/855/856) all carry his one
96
# entity id, so listing each would report him killed three times. One entity
97
# is one enemy, so the first flag wins and the rest are dropped. The
98
# `common_func` rows are template invocations with one flag per entity and do
99
# not have the problem. These rows are still the softer half of the table --
100
# a flag set after a death check can also be a phase or music flag, and the
101
# source's own filter (gated at event start AND set after the death check) is
102
# what stands between them and a false kill.
103
entity = re.search(
r"\[entity ([\d/]+)\]"
, r[
"enemy_name"
])
104
if
"Map-local event"
in
r[
"notes"
]:
105
key = entity.group(1)
if
entity
else
r[
"enemy_name"
]
106
if
key
in
seen_entities:
107
local += 1
108
continue
109
seen_entities.add(key)
110
addr =
address
(fid, ks)
111
if
addr
is
None
:
112
dropped.append(fid)
113
continue
114
table.setdefault(
area_of
(r[
"area_or_map"
]), []).append(
115
[addr[0], addr[1],
clean
(r[
"enemy_name"
])]
116
)
117
if
not
table:
118
sys.exit(
"no rows survived -- check the source CSV"
)
119
for
rows
in
table.values():
120
rows.sort(key=
lambda
t: (t[2], t[0], t[1]))
121
with
open(OUT,
"w"
, encoding=
"utf-8"
)
as
f:
122
json.dump(dict(sorted(table.items())), f, ensure_ascii=
False
, indent=1)
123
f.write(
"\n"
)
124
total = sum(len(v)
for
v
in
table.values())
125
print(f
"wrote {OUT}: {total} enemies in {len(table)} areas"
)
126
print(
127
f
"dropped {len(dropped)} in groups with no base: {sorted({d // 1000 for d in dropped})}"
128
)
129
print(f
"dropped {local} duplicate map-local rows (same entity, another flag)"
)
130
131
132
if
__name__ ==
"__main__"
:
133
main
()
gen_ds3_enemies.area_of
area_of(text)
"Lothric Castle (m30_01_00_00)" -> "Lothric Castle".
Definition
gen_ds3_enemies.py:80
gen_ds3_enemies.main
main()
Definition
gen_ds3_enemies.py:84
gen_ds3_enemies.address
address(fid, ks)
Flag id -> (distance into the flag region, bit), or None for an unmapped group.
Definition
gen_ds3_enemies.py:49
gen_ds3_enemies.clean
clean(name)
"Crystal Lizard [entity 3200259]" -> "Crystal Lizard".
Definition
gen_ds3_enemies.py:65
tools
gen_ds3_enemies.py
Generated by
1.9.8