Add writing of filter rules

This commit is contained in:
2023-11-02 20:53:33 +01:00
parent 098c537e2f
commit fe7aed4c0a
7 changed files with 325 additions and 52 deletions

View File

@@ -6,16 +6,26 @@ from d2lootfilter.format import Class
_item_names = []
_item_runes = []
_item_assets = {}
_class_map = {}
_path = None
def init(path: Path):
global _item_names, _item_runes
global _item_names, _item_runes, _path, _item_assets
_path = path
with open(path / "local" / "lng" / "strings" / "item-names.json", encoding="utf-8-sig") as f:
_item_names = json.load(f)
with open(path / "local" / "lng" / "strings" / "item-runes.json", encoding="utf-8-sig") as f:
_item_runes = json.load(f)
with open(path / "hd" / "items" / "items.json", encoding="utf-8") as f:
dat = json.load(f)
for row in dat:
base = next(iter(row))
asset = row[base]["asset"]
_item_assets[base] = asset
for cls in Class:
_class_map[cls] = set()
@@ -39,6 +49,9 @@ def init(path: Path):
cls = Class.Belt
_class_map[cls].add(row["code"])
# TODO: weapon.txt, misc.txt -> class_map
for entry in _item_names:
if "Potion" in entry["enUS"]:
_class_map[Class.Potion].add(entry["Key"])
# Rune class
for i in range(1, 34):
@@ -55,3 +68,9 @@ def item_runes() -> list:
def class_to_bases(cls: str) -> set:
return _class_map[cls]
def item_asset(code: str) -> Path:
base: Path = _path / "hd" / "items"
file = next(base.glob(f"**/{_item_assets[code]}.json"))
return file

View File

@@ -1,4 +1,5 @@
from pathlib import Path
import sys
from d2lootfilter import data
from d2lootfilter.format import FilterRule
from d2lootfilter.parser import parse_format
@@ -9,21 +10,30 @@ def main():
# TODO: Parse args
data_dir = Path("C:\\Program Files (x86)\\Diablo II Resurrected\\Data")
# Remove any previous rules
remover = FilterRemover(data_dir)
remover.remove_all_rules()
# Init data
data.init(data_dir)
# Parse filter for rules
rules = []
with open("test.filter") as f:
with open("example.filter") as f:
lines = f.read().splitlines()
instructions = parse_format(lines)
for instr in instructions:
nline, verb, prop = instr
rules.append(FilterRule(nline, verb, prop))
rules.append(FilterRule(source_line=nline, verb=verb, props=prop))
# Remove any previous rules
remover = FilterRemover(data_dir)
remover.remove_all_rules()
# Check for any base type duplicates
observed = {}
for rule in rules:
for base in rule.base_types:
if base in observed:
print(f"Error: base {base!r} appears in rule at line {observed[base]} and line {rule.source_line}")
sys.exit(1)
observed[base] = rule.source_line
# Write new rules
writer = FilterRuleWriter(data_dir)

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
from enum import Enum
import re
@@ -20,6 +21,7 @@ class Property(str, Enum):
To = "To" # only for verb Rename
AddVfx = "AddVfx"
PlaySound = "PlaySound"
TextColor = "TextColor"
class Class(str, Enum):
@@ -33,6 +35,7 @@ class Class(str, Enum):
Amulet = "Amulet"
Belt = "Belt"
Rune = "Rune"
Potion = "Potion"
# TODO: Add missing classes
@@ -42,18 +45,30 @@ class RelationalOp(str, Enum):
class VisualEffect(str, Enum):
Beam = "Beam"
Glitter = "Glitter"
Flash = "Flash"
class Color(str, Enum):
Red = "Red"
Blue = "Blue"
Purple = "Purple"
@dataclass
class FilterRule:
def __init__(self, source_line: int, verb: Verb, props: list[tuple[Property, str]]):
self.source_line = source_line
self.base_types = None
self.class_bases = None
self.rename = None
self.vfx = []
self.sounds = []
self.verb = verb
for prop in props:
verb: Verb
source_line: int
props: list[tuple[Property, str]]
base_types: list[str] | None = None
class_bases: list[str] | None = None
vfx: list[VisualEffect] = field(default_factory=list)
sounds: list[str] = field(default_factory=list)
rename: str | None = None
text_color: Color | None = None
def __post_init__(self):
for prop in self.props:
proptype, args = prop
self.eval_prop(proptype, args)
@@ -75,6 +90,8 @@ class FilterRule:
self._add_vfx(args)
case Property.PlaySound:
self._play_sound(args)
case Property.TextColor:
self._text_color(args)
case _:
raise NotImplementedError()
@@ -95,7 +112,7 @@ class FilterRule:
found = True
for runeitem in data.item_runes():
if (
re.match(r"r[0-9][0-9]", runeitem["Key"])
re.match(r"^r[0-9][0-9]$", runeitem["Key"])
and (not op and arg in runeitem["enUS"])
or (op and arg == runeitem["enUS"])
):
@@ -144,6 +161,18 @@ class FilterRule:
def _play_sound(self, args):
raise NotImplementedError()
def _text_color(self, args):
arg, rest = self._arg_next_literal(args, False)
try:
color = Color(arg)
except ValueError:
raise FormatError(f"Format error @ rule starting on line {self.source_line}: Invalid color {arg!r}.")
if len(rest) > 0:
raise FormatError(
f"Format error @ rule starting on line {self.source_line}: Trailing data after 'TextColor'"
)
self.text_color = color
def _arg_str(self, args, optional=False):
if len(args) > 0 and args[0] == '"':
args = args[1:]

View File

@@ -14,18 +14,25 @@ def parse_format(
properties = []
source_line = 0
for n, line in enumerate(lines):
if line.startswith("#"):
continue
if line.startswith(" ") or line.startswith("\t"):
indented = True
comment = line.startswith("#")
indented = line.startswith(" ") or line.startswith("\t")
if len(line) == 0:
indented = False
if verb is not None:
instructions.append((source_line, verb, properties))
verb = None
properties = []
elif not indented:
if (comment or not indented) and verb is not None:
instructions.append((source_line, verb, properties))
verb = None
properties = []
if comment or len(line) == 0:
continue
if indented:
splits = line.lstrip().split(" ", maxsplit=1)
try:
prop = Property(splits[0])
except ValueError:
raise ParseError(f"Error @ line {n + 1}: Invalid property {splits[0]!r}")
properties.append((prop, "" if len(splits) == 0 else splits[1]))
else:
if verb:
raise ParseError(f"Error @ line {n + 1}: Expected indentation")
try:
@@ -33,14 +40,6 @@ def parse_format(
source_line = n + 1
except ValueError:
raise ParseError(f"Error @ line {n + 1}: Invalid verb {line!r}")
else:
splits = line.lstrip().split(" ", maxsplit=1)
print(splits)
try:
prop = Property(splits[0])
except ValueError:
raise ParseError(f"Error @ line {n + 1}: Invalid property {splits[0]!r}")
properties.append((prop, "" if len(splits) == 0 else splits[1]))
if verb is not None:
instructions.append((source_line, verb, properties))

View File

@@ -1,21 +1,172 @@
from contextlib import contextmanager
from io import TextIOWrapper
from itertools import chain
import json
import os
from pathlib import Path
import re
import shutil
from typing import Any, Iterator
from d2lootfilter.data import item_asset
from d2lootfilter.format import FilterRule
from d2lootfilter.format import Color, FilterRule, Verb, VisualEffect
class FilterRuleWriter:
def __init__(self, data_dir: Path):
pass
self.data_dir = data_dir
def write(self, rule: FilterRule):
import pdb
match rule.verb:
case Verb.Rename:
self._rename(rule.base_types, rule.rename, rule.text_color)
case Verb.Hide:
self._rename(rule.base_types, "", rule.text_color)
case Verb.Show:
if rule.text_color:
self._rename(rule.base_types, "$BaseType$", rule.text_color)
case _:
raise NotImplementedError()
for vfx in rule.vfx:
self._vfx(rule.base_types, vfx)
pdb.set_trace()
def _rename(self, bases: list[str], name: str, color: Color | None):
names = None
runes = None
for base in bases:
if re.match(r"^r[0-9][0-9]$", base):
if runes is None:
runes = self._read_json(self.data_dir / "local" / "lng" / "strings" / "item-runes.json")
for entry in runes:
if entry["Key"] == base:
entry["enUS"] = self._color(color) + name.replace("$BaseType$", entry["enUS"])
break
else:
if names is None:
names = self._read_json(self.data_dir / "local" / "lng" / "strings" / "item-names.json")
for entry in names:
if entry["Key"] == base:
entry["enUS"] = self._color(color) + name.replace("$BaseType$", entry["enUS"])
break
if names:
self._write_json(self.data_dir / "local" / "lng" / "strings" / "item-names.json", names)
if runes:
self._write_json(self.data_dir / "local" / "lng" / "strings" / "item-runes.json", names)
def _vfx(self, bases: list[str], vfx: VisualEffect):
for base in bases:
path = item_asset(base)
dat = self._read_json(path)
match vfx:
case VisualEffect.Beam:
dat["dependencies"]["particles"].append(
{"path": "data/hd/vfx/particles/overlays/object/horadric_light/fx_horadric_light.particles"}
)
dat["entities"].append(
{
"type": "Entity",
"name": "entity_beam",
"id": 987654321001,
"components": [
{
"type": "TransformDefinitionComponent",
"name": "component_transform1",
"position": {"x": 0, "y": 0, "z": 0},
"orientation": {"x": 0, "y": 0, "z": 0, "w": 1},
"scale": {"x": 1, "y": 1, "z": 1},
"inheritOnlyPosition": False,
},
{
"type": "VfxDefinitionComponent",
"name": "entity_vfx_beam",
"filename": "data/hd/vfx/particles/overlays/object/horadric_light/fx_horadric_light.particles",
"hardKillOnDestroy": False,
},
],
}
)
case VisualEffect.Glitter:
dat["entities"].append(
{
"type": "Entity",
"name": "entity_glitter",
"id": 987654321002,
"components": [
{
"type": "VfxDefinitionComponent",
"name": "entity_vfx_glitter",
"filename": "data/hd/vfx/particles/overlays/paladin/aura_fanatic/aura_fanatic.particles",
"hardKillOnDestroy": False,
}
],
}
)
case VisualEffect.Flash:
dat["entities"].append(
{
"type": "Entity",
"name": "entity_flash",
"id": 987654321003,
"components": [
{
"type": "TransformDefinitionComponent",
"name": "component_transform1",
"position": {"x": 0, "y": 0, "z": 0},
"orientation": {"x": 0, "y": 0, "z": 0, "w": 1},
"scale": {"x": 1, "y": 1, "z": 1},
"inheritOnlyPosition": False,
},
{
"type": "VfxDefinitionComponent",
"name": "entity_vfx_flash",
"filename": "data/hd/vfx/particles/overlays/common/valkyriestart/valkriestart_overlay.particles",
"hardKillOnDestroy": False,
},
],
}
)
case _:
raise RuntimeError(f"Unhandled vfx type {vfx}")
self._write_json(path, dat)
@contextmanager
def _open_bk(self, path: Path) -> Iterator[TextIOWrapper]:
bk_path = path.parent / (path.name + ".d2lootfilter")
if not bk_path.exists():
shutil.copy(path, bk_path)
f = open(path, "w", encoding="utf-8-sig")
yield f
f.close()
def _write_json(self, path: Path, data: Any):
with self._open_bk(path) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"Wrote new rules to {path.relative_to(self.data_dir)}")
def _read_json(self, path: Path):
with open(path, encoding="utf-8-sig") as f:
return json.load(f)
def _color(self, color: Color | None) -> str:
match color:
case Color.Red:
return "ÿc1"
case Color.Blue:
return "ÿc3"
case Color.Purple:
return "ÿc;"
case _:
pass
return ""
class FilterRemover:
def __init__(self, data_dir: Path):
pass
self.data_dir = data_dir
def remove_all_rules(self):
pass
paths = chain(self.data_dir.glob("local/**/*.d2lootfilter"), self.data_dir.glob("hd/**/*.d2lootfilter"))
for path in paths:
dst_path = Path(path).with_suffix("")
os.replace(path, dst_path)
print(f"Removed filter rules in {dst_path.relative_to(self.data_dir)}")

76
example.filter Normal file
View File

@@ -0,0 +1,76 @@
Show
BaseType "Mal" "Ist" "Gul" "Vex" "Ohm" "Lo" "Sur" "Ber" "Jah" "Cham" "Zod"
Class "Rune"
AddVfx Beam
AddVfx Glitter
AddVfx Flash
Hide
BaseType "Gas" "Thawing" "Antidote" "Fulminating" "Exploding" "Oil" "Stamina"
Class "Potion"
Rename
BaseType "Small Charm" "Grand Charm"
To "** $BaseType$ **"
Rename
BaseType == "Dusk Shroud" "Mage Plate" "Archon Plate" "Vortex Shield" "Sacred Targe" "Monarch" "Phase Blade" "Berserker Axe"
To "++ $BaseType$ ++"
Rename
BaseType == "Thresher" "Cryptic Axe" "Colossus Voulge" "Giant Thresher" "Sacred Armor" "Lacquered Plate" "Hellforge Plate" "Kraken Shell" "Balrog Skin" "Boneweave" "Great Hauberk" "Loricated Mail"
To "[eth?] $BaseType$"
# Healing Potions
Rename
BaseType == "Minor Healing Potion"
To "hp1"
TextColor Red
Rename
BaseType == "Light Healing Potion"
To "hp2"
TextColor Red
Rename
BaseType == "Healing Potion"
To "hp3"
TextColor Red
Rename
BaseType == "Greater Healing Potion"
To "hp4"
TextColor Red
Rename
BaseType == "Super Healing Potion"
To "HP5"
TextColor Red
# Mana Potions
Rename
BaseType == "Minor Mana Potion"
To "mp1"
TextColor Blue
Rename
BaseType == "Light Mana Potion"
To "mp2"
TextColor Blue
Rename
BaseType == "Mana Potion"
To "mp3"
TextColor Blue
Rename
BaseType == "Greater Mana Potion"
To "mp4"
TextColor Blue
Rename
BaseType == "Super Mana Potion"
To "MP5"
TextColor Blue
# Rejuvenation Potions
Rename
BaseType == "Rejuvenation Potion"
To "rp"
TextColor Purple
Rename
BaseType == "Full Rejuvenation Potion"
To "** FP **"
TextColor Purple

View File

@@ -1,11 +0,0 @@
Show
BaseType "Mal" "Ist" "Gul" "Vex" "Ohm" "Lo" "Sur" "Ber" "Jah" "Cham" "Zod"
Class "Rune"
AddVfx Beam
Hide
BaseType == "Key"
Rename
BaseType "Charm"
To "** $BaseType$ **"