-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ed8eb02
commit 61f6860
Showing
22 changed files
with
1,103 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
__all__ = ["base", "common", "generic", "map", "vmf"] | ||
|
||
from . import base # Pattern, AttrMap & MetaPattern | ||
from . import common # Integer, Float, Point & Plane patterns | ||
from . import generic # Brush, BrushSide, Entity & Displacement / Patch baseclasses | ||
# file formats | ||
from . import map # oops, collides with a builtin function | ||
from . import vmf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
from __future__ import annotations | ||
import re | ||
from typing import Callable, Dict, Union | ||
|
||
|
||
# TODO: caches & write-once members | ||
# -- lock _regex etc. to class definition | ||
# -- store results of properties / MetaPattern regex generators | ||
|
||
|
||
class Pattern: | ||
regex: str | ||
ValueType: Callable[str, object] | ||
value: object | ||
|
||
def __init__(self, *args, **kwargs): | ||
self.value = self.ValueType(*args, **kwargs) | ||
|
||
def __repr__(self) -> str: | ||
return f'{self.__class__.__name__}("{self!s}")' | ||
|
||
def __str__(self) -> str: | ||
return str(self.value) | ||
|
||
@classmethod | ||
def from_string(cls, string: str): | ||
match = re.match(cls.regex, string) | ||
assert match is not None, "string doesn't match regex" | ||
return cls(string) | ||
|
||
|
||
def escape(string: str) -> str: | ||
special = ".?*+^$[](){}|\\" | ||
return "".join([(f"\\{c}" if c in special else c) for c in string]) | ||
|
||
|
||
class AttrMap: | ||
"""mutable dict-like object""" | ||
|
||
def __init__(self, **kwargs): | ||
self._keys = tuple(kwargs.keys()) | ||
for a, v in kwargs.items(): | ||
setattr(self, a, v) | ||
|
||
def __eq__(self, other: AttrMap) -> bool: | ||
if isinstance(other, AttrMap): | ||
return self.as_dict() == other.as_dict() | ||
return False | ||
|
||
def __repr__(self) -> str: | ||
values = ", ".join([f"{k}={getattr(self, k)}" for k in self._keys]) | ||
return f"{self.__class__.__name__}({values})" | ||
|
||
def as_dict(self) -> Dict[str, object]: | ||
return {k: getattr(self, k) for k in self._keys} | ||
|
||
|
||
class MetaPattern: | ||
"""container for regex patterns""" | ||
spec: str | ||
# ^ "SYMBOL name1 name2 name3" | ||
patterns: Dict[str, Union[Pattern, MetaPattern]] | ||
# ^ {"name1": PatternSubclass, "name2": MetaPattern} | ||
ValueType: Callable[Dict[str, object], object] = AttrMap | ||
value: object # type should be ValueType | ||
# ^ value = ValueType(**{"key": value}) | ||
|
||
def __init__(self, *args, **kwargs): | ||
attrs = {t: self.patterns[t]().value for t in self.spec.split() if t in self.patterns} | ||
attrs.update(dict(zip((t for t in self.spec.split() if t in self.patterns), args))) | ||
attrs.update(kwargs) | ||
self.value = self.ValueType(**attrs) | ||
|
||
def __repr__(self) -> str: | ||
return f'{self.__class__.__name__}("{self!s}")' | ||
|
||
def __str__(self) -> str: | ||
return " ".join([ | ||
str(getattr(self.value, t)) if t in self.patterns else t | ||
for t in self.spec.split()]) | ||
|
||
@classmethod | ||
def from_string(cls, string: str) -> MetaPattern: | ||
matches = re.match(cls.regex_groups(), string) | ||
assert matches is not None, f"string does not match pattern: {cls.regex_groups()}" | ||
return cls(**{t: cls.patterns[t].from_string(m).value for t, m in matches.groupdict().items()}) | ||
|
||
# REGEX ASSEMBLERS | ||
|
||
@classmethod | ||
def regex(cls): | ||
"""no groups; for matching""" | ||
regexes = list() | ||
for token in cls.spec.split(" "): | ||
if token in cls.patterns: | ||
pattern = cls.patterns[token] | ||
if issubclass(pattern, Pattern): | ||
regexes.append(pattern.regex) | ||
elif issubclass(pattern, MetaPattern): | ||
regexes.append(pattern.regex()) | ||
else: | ||
raise TypeError(f"{token}: {pattern} is invalid") | ||
else: # symbol / keyword | ||
regexes.append(escape(token)) | ||
return r"\s*".join(regexes) | ||
|
||
@classmethod | ||
def regex_groups(cls): | ||
"""named top-level groups only""" | ||
regexes = list() | ||
for token in cls.spec.split(" "): | ||
if token in cls.patterns: | ||
pattern = cls.patterns[token] | ||
if issubclass(pattern, Pattern): | ||
regexes.append(f"(?P<{token}>{pattern.regex})") | ||
elif issubclass(pattern, MetaPattern): | ||
regexes.append(f"(?P<{token}>{pattern.regex()})") | ||
else: | ||
raise TypeError() | ||
else: # symbol / keyword | ||
regexes.append(escape(token)) | ||
return r"\s*".join(regexes) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from typing import Dict | ||
|
||
from ...branches import physics | ||
from ...branches import vector | ||
from . import base | ||
|
||
|
||
class Comment(base.Pattern): | ||
regex = r"(\\\\|//) .*" | ||
ValueType = str | ||
|
||
|
||
class Everything(base.Pattern): | ||
regex = r".*" | ||
ValueType = str | ||
|
||
|
||
class TrailingComment(base.MetaPattern): | ||
spec = r"line \\ comment" | ||
patterns = {"line": Everything, "comment": Everything} | ||
|
||
|
||
class Filepath(base.Pattern): | ||
regex = r"[A-Za-z0-9_\./\\:]*" | ||
ValueType = str | ||
|
||
|
||
class Float(base.Pattern): | ||
regex = r"[+-]?[0-9]+(\.[0-9]+?(e[+-]?[0-9]+)?)?" | ||
ValueType = float | ||
|
||
|
||
class Integer(base.Pattern): | ||
regex = r"[+-]?[0-9]+" | ||
ValueType = int | ||
|
||
|
||
class KeyValuePair(base.MetaPattern): | ||
spec = r'" key " " value "' | ||
patterns = {"key": Everything, "value": Everything} | ||
|
||
|
||
class Point(base.MetaPattern): | ||
spec = "( x y z )" | ||
patterns = {a: Float for a in "xyz"} | ||
ValueType = vector.vec3 | ||
|
||
|
||
class Plane(base.MetaPattern): | ||
spec = "A B C" | ||
patterns = {P: Point for P in "ABC"} | ||
|
||
class ValueType(physics.Plane): | ||
def __init__(self, **kwargs: Dict[str, Point]): | ||
if kwargs["A"] == kwargs["B"] == kwargs["C"]: | ||
# TODO: warning for invalid face | ||
self.normal, self.distance = vector.vec3(z=1), 0 | ||
else: | ||
plane = physics.Plane.from_triangle(*[kwargs[P] for P in "ABC"]) | ||
self.normal, self.distance = plane.normal, plane.distance | ||
|
||
def __str__(self) -> str: | ||
return " ".join([f"({P.x} {P.y} {P.z})" for P in self.value.as_triangle()]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from __future__ import annotations | ||
from collections import defaultdict | ||
from typing import Any, Dict, List | ||
|
||
from ...branches import physics | ||
from ...branches import texture | ||
|
||
|
||
# TODO: Curve (CoDRadiant) | ||
# TODO: Displacement (Source) | ||
# TODO: Patch (IdTech 3) | ||
# TODO: Tricoll (Titanfall) [for MRVN-Radiant] | ||
# -- misc_model (embeds a .mdl for lightmapping) | ||
# -- .gltf? iirc Respawn builds terrain in Autodesk Maya [citation needed] | ||
|
||
|
||
class Brush: | ||
sides: List[BrushSide] | ||
|
||
def __init__(self, sides=None): | ||
self.sides = sides if sides is not None else list() | ||
|
||
def __repr__(self) -> str: | ||
return f"<Brush {len(self.sides)} sides @ 0x{id(self):012X}>" | ||
|
||
def as_physics(self) -> physics.Brush: | ||
# need to calculate brush bounds from sides | ||
# also need to confirm brush is valid (convex & closed) | ||
raise NotImplementedError() | ||
|
||
|
||
class BrushSide: | ||
plane: physics.Plane | ||
shader: str | ||
texture_vector: texture.TextureVector | ||
# TODO: include rotation in texture.TextureVector | ||
texture_rotation: float | ||
|
||
def __init__(self, plane=physics.Plane((0, 0, 1), 0), shader="__TB_empty", texture_vector=None, rotation=0): | ||
self.plane = plane | ||
self.shader = shader | ||
if texture_vector is None: | ||
self.texture_vector = texture.TextureVector.from_normal(self.plane.normal) | ||
else: | ||
self.texture_vector = texture_vector | ||
self.texture_rotation = rotation | ||
|
||
def __repr__(self) -> str: | ||
# TODO: kwargs for texture axis & rotation (if shorter) | ||
return f"BrushSide({self.plane!r}, {self.shader!r}, ...)" | ||
|
||
|
||
class Entity: | ||
# could do a class for each classname & use fgd-tool for type conversion... | ||
classname: str # must be defined | ||
# NOT KEYVALUES | ||
brushes: List[Brush] | ||
_keys: List[str] # key-value pairs to write | ||
# NOTE: keys will be stored w/ setattr | ||
|
||
def __init__(self, **kwargs): | ||
self.brushes = list() | ||
self._keys = list() | ||
for key, value in kwargs.items(): | ||
self[key] = value | ||
|
||
def __delitem__(self, key: str): | ||
assert key in self._keys, f'cannot delete key "{key}"' | ||
self._keys.remove(key) | ||
delattr(self, key) | ||
|
||
def __getitem__(self, key: str) -> str: | ||
return getattr(self, key) | ||
|
||
def __repr__(self) -> str: | ||
lines = [ | ||
"{", | ||
*[f'"{k}" "{self[k]}"' for k in self._keys], | ||
f"...skipped {len(self.brushes)} brushes...", | ||
"}"] | ||
return "\n".join(lines) | ||
|
||
def __setitem__(self, key: str, value: str): | ||
assert key not in ("_keys", "brushes"), f'cannot set key "{key}"' | ||
self._keys.append(key) | ||
setattr(self, key, value) | ||
|
||
def __str__(self): | ||
lines = [ | ||
"{", | ||
*[f'"{k}" "{self[k]}"' for k in self._keys], | ||
*map(str, self.brushes), | ||
"}"] | ||
return "\n".join(lines) | ||
|
||
def get(self, key: str, default: Any = None) -> Any: | ||
return getattr(self, key, default) | ||
|
||
|
||
class MapFile: | ||
comments: Dict[int, str] | ||
# ^ {line_no: "comment"} | ||
entities: List[Entity] | ||
worldspawn: Entity = property(lambda s: s.entities[0]) | ||
|
||
def __init__(self): | ||
self.comments = dict() | ||
self.entities = list() | ||
|
||
def __repr__(self) -> str: | ||
return f"<{self.__class__.__name__} {len(self.entities)} entities>" | ||
|
||
def search(self, **search: Dict[str, str]) -> List[Entity]: | ||
"""Search for entities by key-values; e.g. .search(key=value) -> [{"key": value, ...}, ...]""" | ||
return [e for e in self.entities if all([e.get(k, "") == v for k, v in search.items()])] | ||
|
||
def entities_by_classname(self) -> Dict[str, List[Entity]]: | ||
out = defaultdict(str) | ||
for entity in self.entities: | ||
out[entity.classname].append(entity) | ||
return dict(out) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
__all__ = ["cod4", "quake", "valve"] | ||
|
||
from . import cod4 | ||
from . import quake | ||
from . import valve # version 220 |
Oops, something went wrong.