Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# 0.7.0
> [!WARNING]
> ⚠️ This release contains a minimum Python version bump to 3.12 ⚠️

### Added
- Added support for the new (not new) TOC variables and conditionals
- Added support for client-specific TOC files based on the file name
- You can find this value, if specified, in the `TOCFile.ClientType` attribute. This attribute does not get exported

### Changed
- Migrated project management to `uv`
- Bumped minimum required Python version to 3.12

# 0.6.0
> [!WARNING]
> This release contains breaking changes, marked with ⚠️.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

# pytoc

A Python package for parsing World of Warcraft addon TOC files.
A Python package for reading and writing World of Warcraft addon [TOC](https://warcraft.wiki.gg/wiki/TOC_format) files.

## Installation

You can install this package via `pip`.

```py
```
pip install wow-pytoc
```

Expand Down
57 changes: 57 additions & 0 deletions src/pytoc/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from enum import StrEnum


class TOCGameType(StrEnum):
Standard = "standard"
Mainline = "mainline"
Wowhack = "wowhack"
Wowlabs = "wowlabs"
Plunderstorm = "plunderstorm"
Classic = "classic"
Vanilla = "vanilla"
TBC = "tbc"
Wrath = "wrath"
Mists = "mists"


class TOCEnvironment(StrEnum):
Global = "Global"
Glue = "Glue"
Both = "Both"


class TOCFamily(StrEnum):
Mainline = "Mainline"
Classic = "Classic"


class TOCTextLocale(StrEnum):
enUS = "enUS"
enGB = "enGB"
enTW = "enTW"
zhTW = "zhTW"
esES = "esES"
ruRU = "ruRU"
koKR = "koKR"
ptPT = "ptPT"
esMX = "esMX"
itIT = "itIT"
deDE = "deDE"
frFR = "frFR"
enCN = "enCN"
zhCN = "zhCN"
ptBR = "ptBR"


TOC_GAME_TYPE_TO_FAMILY = {
TOCGameType.Standard: TOCFamily.Mainline,
TOCGameType.Mainline: TOCFamily.Mainline,
TOCGameType.Wowhack: TOCFamily.Mainline,
TOCGameType.Wowlabs: TOCFamily.Mainline,
TOCGameType.Plunderstorm: TOCFamily.Mainline,
TOCGameType.Classic: TOCFamily.Classic,
TOCGameType.Vanilla: TOCFamily.Classic,
TOCGameType.TBC: TOCFamily.Classic,
TOCGameType.Wrath: TOCFamily.Classic,
TOCGameType.Mists: TOCFamily.Classic,
}
122 changes: 122 additions & 0 deletions src/pytoc/file_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import re

from dataclasses import dataclass
from abc import ABC, abstractmethod
from typing import Optional, Any

from .enums import *

# TOC eval context


@dataclass(frozen=True)
class TOCEvaluationContext:
GameType: TOCGameType
Environment: TOCEnvironment
TextLocale: TOCTextLocale

@property
def Family(self) -> TOCFamily:
try:
return TOC_GAME_TYPE_TO_FAMILY[self.GameType]
except KeyError:
raise KeyError(f"Unknown GameType specified: {self.GameType}")


# TOC conditions


class TOCCondition(ABC):
AllowedValues: frozenset[Any]
ExportName: str

@abstractmethod
def evaluate(self, ctx: TOCEvaluationContext) -> bool: ...

def export(self) -> str:
return f"[{self.ExportName} " + ", ".join(self.AllowedValues) + "]"


@dataclass(frozen=True)
class TOCAllowLoad(TOCCondition):
AllowedValues: frozenset[TOCEnvironment]
ExportName: str = "AllowLoad"

def evaluate(self, ctx: TOCEvaluationContext) -> bool:
return ctx.Environment in self.AllowedValues or TOCEnvironment.Both in self.AllowedValues


@dataclass(frozen=True)
class TOCAllowLoadEnvironment(TOCCondition):
AllowedValues: frozenset[TOCEnvironment]
ExportName: str = "AllowLoadEnvironment"

def evaluate(self, ctx: TOCEvaluationContext) -> bool:
return ctx.Environment in self.AllowedValues or TOCEnvironment.Both in self.AllowedValues


@dataclass(frozen=True)
class TOCAllowLoadGameType(TOCCondition):
AllowedValues: frozenset[TOCGameType]
ExportName: str = "AllowLoadGameType"

def evaluate(self, ctx: TOCEvaluationContext) -> bool:
return ctx.GameType in self.AllowedValues


@dataclass(frozen=True)
class TOCAllowLoadTextLocale(TOCCondition):
AllowedValues: frozenset[TOCTextLocale]
ExportName: str = "AllowLoadTextLocale"

def evaluate(self, ctx: TOCEvaluationContext) -> bool:
return ctx.TextLocale in self.AllowedValues


# TOC variables

_TOC_VAR_PATTERN = re.compile(r"\[([A-Za-z0-9_]+)\]")

_TOC_DEFAULT_VARIABLES = {"family": lambda ctx: ctx.Family, "game": lambda ctx: ctx.GameType, "textlocale": lambda ctx: ctx.TextLocale}


@dataclass(frozen=True)
class TOCFileEntry:
"""A file found in the 'files' section of a TOC file. Represents the .lua and .xml files."""

RawFilePath: str
Conditions: Optional[list[TOCCondition]] = None

def __str__(self):
return self.RawFilePath

def resolve_path(self, ctx: TOCEvaluationContext) -> str:
def replace(match: re.Match):
name = match.group(1)
try:
return _TOC_DEFAULT_VARIABLES[name.lower()](ctx)
except KeyError:
raise KeyError(f"Undefined file path variable: {name}")

return _TOC_VAR_PATTERN.sub(replace, self.RawFilePath)

def should_load(self, ctx: TOCEvaluationContext) -> bool:
should_load = True
if self.Conditions:
for condition in self.Conditions:
condition: TOCCondition
if not condition.evaluate(ctx):
should_load = False
break

return should_load

def export(self) -> str:
path = self.RawFilePath

if self.Conditions:
for condition in self.Conditions:
condition_str = f" {condition.export()}"
path += condition_str

return path.strip()
3 changes: 3 additions & 0 deletions src/pytoc/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def _cast(self, value, t):
origin = get_origin(t)
args = get_args(t)
if origin is None:
if isinstance(value, t):
return value

return t(value)
if origin is Union:
for t in get_args(t):
Expand Down
Loading