diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df9f7ba..2b26f69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,8 @@ jobs: - run: | python -m pip install --upgrade pip pip install -r requirements.txt + - run: | + python -m ss14_tiled.test - run: | TMP=$(mktemp -d) git clone https://github.com/space-wizards/space-station-14 $TMP diff --git a/requirements.txt b/requirements.txt index 391ae3b..c65c94c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ autopep8 +deepdiff opencv-python pylint pyyaml diff --git a/ss14_tiled/__main__.py b/ss14_tiled/__main__.py index d6b7e96..c2d09b9 100644 --- a/ss14_tiled/__main__.py +++ b/ss14_tiled/__main__.py @@ -1,4 +1,6 @@ """Main module. To be split up on a later day.""" +import copy +import shutil import sys import xml.etree.ElementTree as ET from pathlib import Path @@ -14,7 +16,7 @@ def create_tiles(root: Path, out: Path): resources_dir = root / "Resources" yml_dir = resources_dir / "Prototypes/Tiles" - files = [x for x in yml_dir.glob("**/*") if x.is_file()] + files = [x for x in yml_dir.glob("**/*.yml") if x.is_file()] root_element = ET.Element("tileset", name="Tiles") tile_id = 0 @@ -54,7 +56,7 @@ def create_decals(root: Path, out: Path, name: str = "", color: str = "#FFF"): resources_dir = root / "Resources" yml_dir = resources_dir / "Prototypes/Decals" - files = [x for x in yml_dir.glob("**/*") if x.is_file()] + files = [x for x in yml_dir.glob("**/*.yml") if x.is_file()] root_element = ET.Element("tileset", name="Decals") if name: @@ -96,6 +98,130 @@ def create_decals(root: Path, out: Path, name: str = "", color: str = "#FFF"): encoding="UTF-8", xml_declaration=True) +def merge_entity(child: dict, parent: dict) -> dict: + """Merge entities.""" + out = copy.deepcopy(parent) + for (key, value) in child.items(): + if key == "components": + continue + out[key] = value + + if "abstract" in child and child["abstract"]: + out["abstract"] = True + elif "abstract" in out: + del out["abstract"] + + if "components" in child: + if not "components" in out: + out["components"] = child["components"] + else: + for child_comp in child["components"]: + found = False + for i, out_comp in enumerate(out["components"]): + if child_comp["type"] != out_comp["type"]: + continue + found = True + for (key, value) in child_comp.items(): + out_comp[key] = value + out["components"][i] = out_comp + if not found: + out["components"].append(child_comp) + + return out + + +def find_entities(root: Path) -> list[dict]: + """Find and return all entities.""" + + # Some bases are outside the "Entities" directory, + # so we have to go over everything. + yml_dir = root / "Resources/Prototypes" + files = [x for x in yml_dir.glob("**/*.yml") if x.is_file()] + + children = [] + adults = {} + for file in files: + for entity in yaml.load(file.read_text("UTF-8"), Loader=SafeLoadIgnoreUnknown) or []: + if entity["type"] != "entity": + continue # alias? + if "parent" in entity: + children.append(entity) + else: + adults[entity["id"]] = entity + + while len(children) > 0: + still_children = [] + for child in children: + parents = child["parent"] + if isinstance(parents, str): + parents = [parents] + + if all(parent in adults for parent in parents): + merged = adults[parents[0]] + for parent in parents[1:]: + merged = merge_entity(adults[parent], merged) + adults[child["id"]] = merge_entity(child, merged) + else: + still_children.append(child) + + children = still_children + + return adults + + +def filter_entities(entities: dict) -> dict: + """Filter out some of the entities.""" + entities = {k: v for k, v in entities.items() + if "abstract" not in v} + entities = {k: v for k, v in entities.items() + if "Sprite" in [x["type"] for x in v["components"]]} + entities = {k: v for k, v in entities.items() + if "TimedDespawn" not in [x["type"] for x in v["components"]]} + entities = {k: v for k, v in entities.items() + if "suffix" not in v or "DEBUG" not in str(v["suffix"])} + entities = {k: v for k, v in entities.items() + if "suffix" not in v or "Admeme" not in str(v["suffix"])} + entities = {k: v for k, v in entities.items() + if "suffix" not in v or "DO NOT MAP" not in str(v["suffix"])} + entities = {k: v for k, v in entities.items() + if "categories" not in v or "HideSpawnMenu" not in v["categories"]} + entities = {k: v for k, v in entities.items() + if "Input" not in [x["type"] for x in v["components"]]} + entities = {k: v for k, v in entities.items() + if "RandomHumanoidSpawner" not in [x["type"] for x in v["components"]]} + + return entities + + +def group_entities(entities: dict) -> list[tuple[str, dict]]: + """Split entities into groups.""" + # TODO: implement + return [("All", entities)] + + +def create_entities(root: Path, out: Path): + """Create the "entities"-tiles.""" + tiles_out = out / ".images" / "entities" + tiles_out.mkdir(parents=True, exist_ok=True) + + entities = find_entities(root) + entities = filter_entities(entities) + groups = group_entities(entities) + # TODO: Continue + + +class SafeLoadIgnoreUnknown(yaml.SafeLoader): + """YAML-Loader that ignores unknown constructors.""" + + def ignore_unknown(self, _node): + """Returns None no matter the node.""" + return None + + +SafeLoadIgnoreUnknown.add_constructor( + None, SafeLoadIgnoreUnknown.ignore_unknown) + + def parse_hex(color: str): """Parse a hex string to RGBA uint8.""" if len(color) == 4: @@ -162,9 +288,12 @@ def get_colors(root: Path) -> list[(str, str)]: def setup(root: Path): """Create tile-sets for Tiled.""" out = Path("dist") - out.mkdir(exist_ok=True) + if out.exists(): + shutil.rmtree(out) + out.mkdir() create_tiles(root, out) + create_entities(root, out) create_decals(root, out) for (name, color) in get_colors(root): create_decals(root, out, name, color) diff --git a/ss14_tiled/test.py b/ss14_tiled/test.py new file mode 100644 index 0000000..354ed9d --- /dev/null +++ b/ss14_tiled/test.py @@ -0,0 +1,118 @@ +"""Some tests.""" +import unittest + +from deepdiff import DeepDiff + +from . import __main__ + + +class TestMergeEntity(unittest.TestCase): + """Tests to see if merging entities works.""" + def test_basalt(self): + """From /Resources/Prototypes/Entities/Tiles/basalt.yml""" + child = { + "type": "entity", + "id": "BasaltTwo", + "parent": "BasaltOne", + "placement": { + "mode": "SnapgridCenter" + }, + "components": [{ + "type": "Sprite", + "layers": [{ + "state": "basalt2", + "shader": "unshaded" + }] + }] + } + parent = { + "type": "entity", + "id": "BasaltOne", + "description": "Rock.", + "placement": { + "mode": "SnapgridCenter" + }, + "components": [{ + "type": "Clickable", + }, { + "type": "Sprite", + "sprite": "/Textures/Tiles/Planet/basalt.rsi", + "layers": [{ + "state": "basalt1", + "shader": "unshaded" + }] + }, { + "type": "SyncSprite", + }, { + "type": "RequiresTile", + }, { + "type": "Transform", + "anchored": True + }, { + "type": "Tag", + "tags": ["HideContextMenu"] + }] + } + expected = { + "type": "entity", + "id": "BasaltTwo", + "parent": "BasaltOne", + "description": "Rock.", + "placement": { + "mode": "SnapgridCenter" + }, + "components": [{ + "type": "Clickable", + }, { + "type": "Sprite", + "sprite": "/Textures/Tiles/Planet/basalt.rsi", + "layers": [{ + "state": "basalt2", + "shader": "unshaded" + }] + }, { + "type": "SyncSprite", + }, { + "type": "RequiresTile", + }, { + "type": "Transform", + "anchored": True + }, { + "type": "Tag", + "tags": ["HideContextMenu"] + }] + } + actual = __main__.merge_entity(child, parent) + diff = DeepDiff(actual, expected, ignore_order=True) + assert not diff + + def test_new_component(self): + """If the child has a new component.""" + child = { + "id": "B", + "parent": "A", + "components": [{ + "type": "test_2" + }] + } + parent = { + "id": "A", + "components": [{ + "type": "test_1" + }] + } + expected = { + "id": "B", + "parent": "A", + "components": [{ + "type": "test_1" + }, { + "type": "test_2" + }] + } + actual = __main__.merge_entity(child, parent) + diff = DeepDiff(actual, expected, ignore_order=True) + assert not diff + +if __name__ == "__main__": + unittest.main()