This script requires two Python tools, d2animdata and d2txt. You will have to install both using:
Code: Select all
pip install d2animdata
pip install d2txt
Code: Select all
"""Speeds up slow monster attack & skill casting animations in AnimData.D2."""
import argparse
from collections import defaultdict
from typing import DefaultDict, List, Set
import d2animdata
from d2txt import D2TXT
# A COF name consists of 3 components:
#
# DIGHHTH = DI + GH + HTH
# ^^ ^^ ^^^
# (1) (2) (3)
#
# (1) Token: A 2-letter code that represents the general type of character or
# monster. It specifies the name of a directory under data/global/chars/ (for
# character animations) or data/global/monsters/ (for monster animations) in
# the MPQ files. Also used by the Code column of MonStats.txt, and possibly
# others.
# (2) Animation mode: A 2-letter code that specifies the name of a directory
# under each token directory in the MPQ files. Also used by various columns in
# MonStats.txt, MonStats2.txt, and possibly others.
# (3) Hit class: A 3-letter code that represents the weapon type associated with
# an animation. Most monsters just use HTH.
#
# This short guide is based on:
# https://d2mods.info/resources/infinitum/tut_files/token-tutorial.html
# Based on PlrType.txt
PLAYER_TOKENS = {
"AM": "Amazon",
"SO": "Sorceress",
"NE": "Necromancer",
"PA": "Paladin",
"BA": "Barbarian",
"DZ": "Druid",
"AI": "Assassin",
}
# Based on https://d2mods.info/forum/viewtopic.php?t=2780
ANIMATION_MODE_NAMES = {
"NU": "Neutral",
"DT": "Death",
"DD": "Dead (Corpse)",
"A1": "Attack 1",
"A2": "Attack 2",
"S1": "Skill 1",
"S2": "Skill 2",
"S3": "Skill 3",
"S4": "Skill 4",
"TN": "Town Neutral",
"TW": "Town Walk",
"KK": "Kick",
"SC": "Cast",
"GH": "Get Hit",
"BL": "Block",
"WL": "Walk",
"RN": "Run",
"TH": "Throw",
}
def make_token_to_monsters(monstats: D2TXT) -> DefaultDict[str, Set[str]]:
"""Returns a mapping of tokens to actively hostile monsters."""
token_to_monsters = defaultdict(set)
for row in monstats:
alignment = int(row["Align"] or 0)
# Skip if friendly (NPCs, summons) or unaligned (e.g. cows)
if alignment != 0 or row["npc"]:
continue
# Skip if AI is not hostile
if row["AI"].lower() in {"idle", "npc"}:
continue
token = row["Code"].upper()
base_id = row["BaseId"]
token_to_monsters[token].add(base_id)
return token_to_monsters
def main(argv: List[str] = None) -> None:
"""Entrypoint for the CLI script."""
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("animdata_d2", help="AnimData.D2 file to modify")
parser.add_argument(
"monstats_txt", help="MonStats.txt to use for reference (read only)"
)
parser.add_argument(
"--min-speed",
default=256,
type=int,
help="Minimum animation speed for attacking and casting spells",
)
args = parser.parse_args(argv)
with open(args.animdata_d2, mode="rb") as animdata_file:
animdata = d2animdata.load(animdata_file)
token_to_monsters = make_token_to_monsters(D2TXT.load_txt(args.monstats_txt))
num_anims_updated = 0
for record in animdata:
token = record.cof_name[:2]
anim_mode = record.cof_name[2:4]
# Only include monster tokens
if token not in token_to_monsters:
continue
# Select attack and spell-casting animations only
if anim_mode not in {"A1", "A2", "S1", "S2", "S3", "S4", "SC", "TH"}:
continue
# Filter only animations that are slower than default (256)
if record.animation_speed >= args.min_speed:
continue
record.animation_speed = args.min_speed
num_anims_updated += 1
with open(args.animdata_d2, mode="wb") as animdata_file:
d2animdata.dump(animdata, animdata_file)
# Phrozen Keep doesn't allow code that calls the print function!
# This is a workaround
print_ = print
print_(f"{num_anims_updated} animations updated")
if __name__ == "__main__":
main()
Finally, run the following command:
Code: Select all
python accelerate_anim.py path/to/AnimData.D2 path/to/MonStats.txt