Skip to content

Commit 609a0dd

Browse files
authored
Merge pull request #16 from krummja/release-1.3.0
Release 1.3.0
2 parents a13a640 + f63d2c8 commit 609a0dd

28 files changed

+1438
-466
lines changed

.flake8

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[flake8]
2+
3+
ignore =
4+
E203, # whitespace before :
5+
E265, # block comment should start with #
6+
F401, # module imported but unused
7+
F403, # 'from module import *' unused; unable to detect undefined names
8+
F405, # name may be undefined, or defined from star imports
9+
10+
max-line-length =
11+
120

.github/workflows/main.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
name: Tests
2+
23
on: [push]
4+
35
jobs:
46
build:
57
runs-on: ubuntu-latest

pecs_framework/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .engine import Engine
44
from .loader import Loader
55
from .events import EntityEvent
6+
from .base_system import BaseSystem
67

78

89
__all__ = [
@@ -12,4 +13,5 @@
1213
'Engine',
1314
'Loader',
1415
'EntityEvent',
16+
'BaseSystem',
1517
]

pecs_framework/base_system.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
2-
from beartype.typing import *
3-
from typing import TypeAlias
4-
from abc import ABC, abstractmethod
2+
from beartype.typing import TYPE_CHECKING
53

64
if TYPE_CHECKING:
7-
from pecs_framework.query import Query, ComponentQuery
85
from pecs_framework.domain import Domain
96
from pecs_framework.engine import Engine
7+
from pecs_framework.query import ComponentQuery
8+
from pecs_framework.query import Query
9+
10+
from abc import ABC, abstractmethod
1011

1112

1213
class Loop(ABC):
@@ -28,7 +29,7 @@ def teardown(self) -> None:
2829
def pre_update(self) -> None:
2930
raise NotImplementedError("Method has no implementation")
3031

31-
@abstractmethod
32+
@abstractmethod
3233
def update(self) -> None:
3334
raise NotImplementedError("Method has no implementation")
3435

@@ -45,18 +46,18 @@ def __init__(self, loop: Loop) -> None:
4546
self.initialize()
4647

4748
def query(
48-
self,
49-
key: str,
49+
self,
50+
key: str,
5051
all_of: ComponentQuery | None = None,
51-
any_of: ComponentQuery | None = None,
52+
any_of: ComponentQuery | None = None,
5253
none_of: ComponentQuery | None = None,
5354
) -> None:
5455
all_of = all_of if all_of else []
5556
any_of = any_of if any_of else []
5657
none_of = none_of if none_of else []
5758
self._queries[key] = self.loop.domain.create_query(
58-
all_of,
59-
any_of,
59+
all_of,
60+
any_of,
6061
none_of,
6162
)
6263

pecs_framework/component.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import annotations
22
from beartype.typing import TypeVar
3+
from beartype.typing import TypedDict
4+
from beartype.typing import Any
35

46
from pecs_framework._types import Bases, Namespace
57
from pecs_framework.events import EntityEvent
68

7-
import json
89
import sys
910
import traceback
1011

@@ -14,19 +15,19 @@ class ComponentMeta(type):
1415
comp_id: str
1516
cbit: int
1617
_entity_id: str
17-
18+
1819
def __new__(
19-
cls: type[ComponentMeta],
20-
clsname: str,
21-
bases: Bases,
20+
cls: type[ComponentMeta],
21+
clsname: str,
22+
bases: Bases,
2223
namespace: Namespace,
2324
) -> ComponentMeta:
2425
clsobj = super().__new__(cls, clsname, bases, namespace)
2526
clsobj.comp_id = clsname.upper()
2627
clsobj.cbit = 0
2728
return clsobj
28-
29-
29+
30+
3031
class Component(metaclass=ComponentMeta):
3132
"""Root Component class that all components extend."""
3233
_entity_id: str
@@ -49,7 +50,7 @@ def handle_event(self, evt: EntityEvent):
4950

5051
def on_event(self, evt: EntityEvent) -> EntityEvent | None:
5152
pass
52-
53+
5354

5455
# Type variable ranging over Component instances
5556
CT = TypeVar("CT", bound=Component)

pecs_framework/domain.py

+129-19
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
11
from __future__ import annotations
2-
from beartype.typing import *
2+
from beartype.typing import TYPE_CHECKING
3+
from beartype.typing import Any
4+
from beartype.typing import TypedDict
35

46
from uuid import uuid1
57
from collections import OrderedDict
8+
import json
69

710
if TYPE_CHECKING:
11+
from pecs_framework.component import Component
812
from pecs_framework.engine import Engine
9-
from pecs_framework.query import ComponentQuery
1013
from pecs_framework.prefab import EntityTemplate
14+
from pecs_framework.query import ComponentQuery
1115

12-
from pecs_framework.entities import Entity
16+
from pathlib import Path
17+
from pecs_framework.entity import Entity
1318
from pecs_framework.query import Query
1419
from rich.console import Console
15-
from rich import inspect
1620

1721

1822
console = Console()
1923

24+
class ComponentDict(TypedDict):
25+
comp_id: str
26+
cbit: int
27+
data: dict[str, Any]
28+
29+
30+
class EntityDict(TypedDict):
31+
alias: str | None
32+
components: list[ComponentDict]
33+
2034

2135
class EntityRegistry:
2236

2337
def __init__(self, domain: Domain) -> None:
2438
self.domain = domain
25-
self.aliases = OrderedDict()
39+
self.alias_to_eid = OrderedDict()
40+
self.eid_to_alias = OrderedDict()
2641
self._map: dict[str, Entity] = OrderedDict()
2742

2843
def __getitem__(self, key: str) -> Entity:
@@ -31,12 +46,15 @@ def __getitem__(self, key: str) -> Entity:
3146
def __setitem__(self, key: str, entity: Entity) -> None:
3247
self._map[key] = entity
3348

49+
def __iter__(self):
50+
return iter(self._map.values())
51+
3452
def keys(self):
3553
return self._map.keys()
3654

3755
def values(self):
3856
return self._map.values()
39-
57+
4058
def get_entity_id(self, entity_or_alias: Entity | str) -> str:
4159
"""
4260
Get an Entity's identifier either by passing the entity itself or its
@@ -52,12 +70,17 @@ def get_entity_id(self, entity_or_alias: Entity | str) -> str:
5270
The Entity's unique identifier
5371
"""
5472
if isinstance(entity_or_alias, str):
55-
id_: str = self.aliases.get(entity_or_alias, '')
73+
id_: str = self.alias_to_eid.get(entity_or_alias, '')
5674
else:
5775
id_: str = entity_or_alias.eid
5876
return id_
59-
60-
def create(self, alias: str | None = None) -> Entity:
77+
78+
def create(
79+
self,
80+
alias: str | None = None,
81+
*,
82+
entity_id: str | None = None,
83+
) -> Entity:
6184
"""
6285
Create a new Entity in the engine's Entity registry.
6386
@@ -70,13 +93,17 @@ def create(self, alias: str | None = None) -> Entity:
7093
-------
7194
The newly created Entity instance
7295
"""
73-
entity = Entity(self.domain, self.domain.create_uid())
96+
entity = Entity(
97+
self.domain,
98+
entity_id if entity_id else self.domain.create_uid(),
99+
)
74100
self._map[entity.eid] = entity
75-
101+
76102
if alias:
77-
if alias in self.aliases.keys():
103+
if alias in self.alias_to_eid:
78104
raise KeyError(f"Entity already exists with alias {alias}")
79-
self.aliases[alias] = entity.eid
105+
self.alias_to_eid[alias] = entity.eid
106+
self.eid_to_alias[entity.eid] = alias
80107

81108
return entity
82109

@@ -108,7 +135,7 @@ def create_from_prefab(
108135
# props from the process above.
109136
for name, props in comp_props.items():
110137
self.domain.engine.components.attach(entity, name, props)
111-
138+
112139
return entity
113140

114141
def get_by_alias(self, alias: str) -> Entity:
@@ -124,7 +151,7 @@ def get_by_alias(self, alias: str) -> Entity:
124151
-------
125152
The Entity corresponding to the passed alias.
126153
"""
127-
entity_id: str = self.aliases.get(alias, '')
154+
entity_id: str = self.alias_to_eid.get(alias, '')
128155
if entity_id:
129156
return self.get_by_id(entity_id)
130157
else:
@@ -134,25 +161,35 @@ def get_by_alias(self, alias: str) -> Entity:
134161
def get_by_id(self, entity_id: str) -> Entity:
135162
return self._map[entity_id]
136163

164+
def get_alias_for_entity(self, entity_or_eid: Entity | str) -> str | None:
165+
if isinstance(entity_or_eid, str):
166+
return self.eid_to_alias.get(entity_or_eid, None)
167+
return self.eid_to_alias.get(entity_or_eid.eid, None)
168+
137169
def remove_entity_by_id(self, entity_id: str) -> None:
138170
self._map[entity_id]._on_entity_destroyed()
139171
del self._map[entity_id]
140172

141173
def remove_entity_by_alias(self, alias: str) -> None:
142174
entity_id = self.get_entity_id(alias)
175+
if alias in self.alias_to_eid:
176+
del self.alias_to_eid[alias]
143177
self.remove_entity_by_id(entity_id)
144178

145179

146180
class Domain:
147-
181+
148182
@staticmethod
149183
def create_uid() -> str:
150184
return str(uuid1())
151-
185+
152186
def __init__(self, engine: Engine) -> None:
153187
self.engine = engine
188+
self.reset()
189+
190+
def reset(self) -> None:
154191
self.entities = EntityRegistry(self)
155-
self.queries: List[Query] = []
192+
self.queries: list[Query] = []
156193

157194
def destroy_entity(self, entity: Entity | str) -> None:
158195
if isinstance(entity, str):
@@ -161,18 +198,91 @@ def destroy_entity(self, entity: Entity | str) -> None:
161198
else:
162199
self.entities.remove_entity_by_alias(entity)
163200
else:
201+
# if entity.eid in self.entities.aliases.values():
202+
for k, v in self.entities.alias_to_eid.items():
203+
if v == entity.eid:
204+
alias = k
205+
self.entities.remove_entity_by_alias(alias)
206+
return
164207
self.entities.remove_entity_by_id(entity.eid)
165208

166209
def create_query(
167210
self,
168211
all_of: ComponentQuery | None = None,
169212
any_of: ComponentQuery | None = None,
170213
none_of: ComponentQuery | None = None,
171-
) -> Query:
214+
) -> Query:
172215
query = Query(self, all_of, any_of, none_of)
173216
self.queries.append(query)
174217
return query
175218

176219
def candidate(self, entity: Entity) -> None:
177220
for query in self.queries:
178221
query.candidate(entity)
222+
223+
def save(self, directory: Path, filename: str) -> None:
224+
output: dict[str, EntityDict] = {}
225+
226+
for entity in self.entities:
227+
output[entity.eid] = {
228+
"alias": self.entities.get_alias_for_entity(entity.eid),
229+
"components": [],
230+
}
231+
232+
for component in entity.components.values():
233+
component_data = serialize_component(component)
234+
output[entity.eid]["components"].append(component_data)
235+
236+
write_to_file(directory, filename, output)
237+
238+
def load(self, directory: Path, filename: str) -> None:
239+
if loaded_data := load_from_file(directory, filename):
240+
for eid, entity_data in loaded_data.items():
241+
alias = entity_data["alias"]
242+
component_data = entity_data["components"]
243+
244+
entity = self.entities.create(alias=alias, entity_id=eid)
245+
246+
for component_datum in component_data:
247+
self.engine.components.attach(
248+
entity,
249+
component_datum["comp_id"],
250+
{k: v for k, v in component_datum["data"].items()},
251+
)
252+
253+
254+
def write_to_file(
255+
directory: Path,
256+
filename: str,
257+
data_dict: dict[str, EntityDict],
258+
) -> None:
259+
if not filename.endswith(".json"):
260+
filename = filename + ".json"
261+
with open(Path(directory, filename), "+w") as file:
262+
file.write(json.dumps(data_dict, indent=4))
263+
264+
265+
def load_from_file(directory: Path, filename: str) -> dict[str, EntityDict]:
266+
if not (filename.endswith(".json")):
267+
filename = filename + ".json"
268+
with open(Path(directory, filename), "+r") as file:
269+
return json.loads(file.read())
270+
271+
272+
def serialize_component(component: Component) -> ComponentDict:
273+
"""
274+
Serialization should provide all of the necessary information needed to
275+
rebuild what is set up in `setup_ecs` anew, with the state that the
276+
components were in when they were serialized.
277+
"""
278+
comp_id = component.__class__.comp_id
279+
cbit = component.__class__.cbit
280+
instance_data = vars(component)
281+
282+
del instance_data["_entity_id"]
283+
284+
return {
285+
"comp_id": comp_id,
286+
"cbit": cbit,
287+
"data": instance_data,
288+
}

0 commit comments

Comments
 (0)