Skip to content

Commit 8eef1da

Browse files
committed
Initial commit
1 parent 3d1973a commit 8eef1da

22 files changed

+3198
-499
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.idea
12
# Byte-compiled / optimized / DLL files
23
__pycache__/
34
*.py[cod]

LICENSE

Lines changed: 159 additions & 498 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,55 @@
11
# genieutils-py
2-
Python implementation of genieutils
2+
3+
Python implementation of [genieutils](https://github.com/Tapsa/genieutils).
4+
5+
This library can be used to read and write `empires2_x2_p1.dat` files for Age of Empires II Definitive Edition.
6+
7+
8+
## Supported dat versions
9+
10+
Currently, only the latest version used in Age of Empires II Definitive Edition is supported (`GV_LatestDE2`/`GV_C20`).
11+
12+
13+
## Installation
14+
15+
```shell
16+
pip install genieutils-py
17+
```
18+
19+
## Usage examples
20+
21+
### Dump the whole dat file as json
22+
23+
The package comes with a handy command line tool that does that for you.
24+
25+
```shell
26+
dat-to-json path/to/empires2_x2_p1.dat
27+
```
28+
29+
30+
### Change cost of Loom to 69 Gold
31+
32+
```python
33+
from genieutils.datfile import DatFile
34+
35+
data = DatFile.parse('path/to/empires2_x2_p1.dat')
36+
data.techs[22].resource_costs[0].amount = 69
37+
data.save('path/to/modded/empires2_x2_p1.dat')
38+
```
39+
40+
### Prevent Kings from garrisoning
41+
42+
```python
43+
from genieutils.datfile import DatFile
44+
45+
data = DatFile.parse('path/to/empires2_x2_p1.dat')
46+
for civ in data.civs:
47+
civ.units[434].bird.task_size -= 1
48+
civ.units[434].bird.tasks.pop()
49+
data.save('path/to/modded/empires2_x2_p1.dat')
50+
```
51+
52+
53+
## Authors
54+
55+
[HSZemi](https://github.com/hszemi) - Original Author

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[tool.hatch.build.targets.wheel]
6+
packages = ["src/genieutils"]
7+
8+
[project]
9+
name = "genieutils-py"
10+
version = "0.0.1"
11+
authors = [
12+
{ name = "SiegeEngineers", email = "[email protected]" },
13+
]
14+
description = "Re-implementation of genieutils in Python"
15+
readme = "README.md"
16+
requires-python = ">=3.11"
17+
classifiers = [
18+
"Programming Language :: Python :: 3",
19+
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
20+
"Operating System :: OS Independent",
21+
]
22+
23+
[project.urls]
24+
Homepage = "https://github.com/SiegeEngineers/genieutils-py"
25+
Issues = "https://github.com/SiegeEngineers/genieutils-py/issues"
26+
27+
[project.scripts]
28+
dat-to-json = "genieutils.scripts:dat_to_json"

src/genieutils/__init__.py

Whitespace-only changes.

src/genieutils/civ.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from dataclasses import dataclass
2+
3+
from genieutils.common import ByteHandler, GenieClass
4+
from genieutils.unit import Unit
5+
6+
7+
@dataclass
8+
class Civ(GenieClass):
9+
player_type: int
10+
name: str
11+
resources_size: int
12+
tech_tree_id: int
13+
team_bonus_id: int
14+
resources: list[float]
15+
icon_set: int
16+
units_size: int
17+
unit_pointers: list[int]
18+
units: list[Unit | None]
19+
20+
@classmethod
21+
def from_bytes(cls, content: ByteHandler) -> 'Civ':
22+
player_type = content.read_int_8()
23+
name = content.read_debug_string()
24+
resources_size = content.read_int_16()
25+
tech_tree_id = content.read_int_16()
26+
team_bonus_id = content.read_int_16()
27+
resources = content.read_float_array(resources_size)
28+
icon_set = content.read_int_8()
29+
units_size = content.read_int_16()
30+
unit_pointers = content.read_int_32_array(units_size)
31+
units = content.read_class_array_with_pointers(Unit, units_size, unit_pointers)
32+
return cls(
33+
player_type=player_type,
34+
name=name,
35+
resources_size=resources_size,
36+
tech_tree_id=tech_tree_id,
37+
team_bonus_id=team_bonus_id,
38+
resources=resources,
39+
icon_set=icon_set,
40+
units_size=units_size,
41+
unit_pointers=unit_pointers,
42+
units=units,
43+
)
44+
45+
def to_bytes(self) -> bytes:
46+
return b''.join([
47+
self.write_int_8(self.player_type),
48+
self.write_debug_string(self.name),
49+
self.write_int_16(self.resources_size),
50+
self.write_int_16(self.tech_tree_id),
51+
self.write_int_16(self.team_bonus_id),
52+
self.write_float_array(self.resources),
53+
self.write_int_8(self.icon_set),
54+
self.write_int_16(self.units_size),
55+
self.write_int_32_array(self.unit_pointers),
56+
self.write_class_array_with_pointers(self.unit_pointers, self.units),
57+
])

src/genieutils/common.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from abc import ABC
2+
from enum import IntEnum
3+
from typing import TypeVar
4+
5+
from genieutils.datatypes import Int, Float, String
6+
7+
TILE_TYPE_COUNT = 19
8+
TERRAIN_COUNT = 200
9+
TERRAIN_UNITS_SIZE = 30
10+
11+
12+
class UnitType(IntEnum):
13+
EyeCandy = 10
14+
Trees = 15
15+
Flag = 20
16+
DeadFish = 30
17+
Bird = 40
18+
Combatant = 50
19+
Projectile = 60
20+
Creatable = 70
21+
Building = 80
22+
AoeTrees = 90
23+
24+
25+
class GenieClass(ABC):
26+
@classmethod
27+
def from_bytes(cls, data: 'ByteHandler'):
28+
raise NotImplementedError
29+
30+
@classmethod
31+
def from_bytes_with_count(cls, data: 'ByteHandler', terrains_used_1: int):
32+
raise NotImplementedError
33+
34+
def to_bytes(self) -> bytes:
35+
raise NotImplementedError
36+
37+
def write_debug_string(self, value: str) -> bytes:
38+
return (self.write_int_16(0x0A60, signed=False)
39+
+ self.write_int_16(len(value), signed=False)
40+
+ value.encode('utf-8'))
41+
42+
def write_string(self, length: int, value: str) -> bytes:
43+
return String.to_bytes(value, length)
44+
45+
def write_int_8(self, value: int) -> bytes:
46+
return Int.to_bytes(value, length=1, signed=False)
47+
48+
def write_int_8_array(self, value: list[int]) -> bytes:
49+
return b''.join(self.write_int_8(v) for v in value)
50+
51+
def write_int_16(self, value: int, signed=True) -> bytes:
52+
return Int.to_bytes(value, length=2, signed=signed)
53+
54+
def write_int_16_array(self, value: list[int]) -> bytes:
55+
return b''.join(self.write_int_16(v) for v in value)
56+
57+
def write_int_32(self, value: int, signed=True) -> bytes:
58+
return Int.to_bytes(value, length=4, signed=signed)
59+
60+
def write_int_32_array(self, value: list[int]) -> bytes:
61+
return b''.join(self.write_int_32(v) for v in value)
62+
63+
def write_float(self, value: float) -> bytes:
64+
return Float.to_bytes(value)
65+
66+
def write_float_array(self, value: list[float]) -> bytes:
67+
return b''.join(self.write_float(v) for v in value)
68+
69+
def write_class(self, value: 'GenieClass') -> bytes:
70+
retval = value.to_bytes()
71+
if retval:
72+
return retval
73+
return b''
74+
75+
def write_class_array(self, value: list['GenieClass']) -> bytes:
76+
retval = b''.join(self.write_class(v) for v in value)
77+
if retval:
78+
return retval
79+
return b''
80+
81+
def write_class_array_with_pointers(self, pointers: list[int], value: list['GenieClass']) -> bytes:
82+
retval = b''.join(self.write_class(v) for i, v in enumerate(value) if pointers[i])
83+
if retval:
84+
return retval
85+
return b''
86+
87+
88+
C = TypeVar('C', bound=GenieClass)
89+
90+
91+
class ByteHandler:
92+
def __init__(self, content: memoryview):
93+
self.content = content
94+
self.offset = 0
95+
96+
def consume_range(self, length: int) -> memoryview:
97+
start = self.offset
98+
end = start + length
99+
self.offset = end
100+
return self.content[start:end]
101+
102+
def read_debug_string(self) -> str:
103+
tmp_size = self.read_int_16(signed=False)
104+
assert tmp_size == 0x0A60
105+
size = self.read_int_16(signed=False)
106+
return String.from_bytes(self.consume_range(size))
107+
108+
def read_string(self, length: int) -> str:
109+
return String.from_bytes(self.consume_range(length))
110+
111+
def read_int_8(self) -> int:
112+
return Int.from_bytes(self.consume_range(1), signed=False)
113+
114+
def read_int_8_array(self, size: int) -> list[int]:
115+
elements = []
116+
for i in range(size):
117+
elements.append(self.read_int_8())
118+
return elements
119+
120+
def read_int_16(self, signed=True) -> int:
121+
return Int.from_bytes(self.consume_range(2), signed=signed)
122+
123+
def read_int_16_array(self, size: int) -> list[int]:
124+
elements = []
125+
for i in range(size):
126+
elements.append(self.read_int_16())
127+
return elements
128+
129+
def read_int_32(self, signed=True) -> int:
130+
return Int.from_bytes(self.consume_range(4), signed=signed)
131+
132+
def read_int_32_array(self, size: int) -> list[int]:
133+
elements = []
134+
for i in range(size):
135+
elements.append(self.read_int_32())
136+
return elements
137+
138+
def read_float(self) -> float:
139+
return Float.from_bytes(self.consume_range(4))
140+
141+
def read_float_array(self, size: int) -> list[float]:
142+
elements = []
143+
for i in range(size):
144+
elements.append(self.read_float())
145+
return elements
146+
147+
def read_class(self, class_: type[C]) -> C:
148+
element = class_.from_bytes(self)
149+
return element
150+
151+
def read_class_array(self, class_: type[C], size: int) -> list[C]:
152+
elements = []
153+
for i in range(size):
154+
element = class_.from_bytes(self)
155+
elements.append(element)
156+
return elements
157+
158+
def read_class_array_with_pointers(self, class_: type[C], size: int, pointers: list[int]) -> list[C | None]:
159+
elements = []
160+
for i in range(size):
161+
element = None
162+
if pointers[i]:
163+
element = class_.from_bytes(self)
164+
elements.append(element)
165+
return elements
166+
167+
def read_class_array_with_param(self, class_: type[C], size: int, terrains_used_1: int) -> list[C]:
168+
elements = []
169+
for i in range(size):
170+
terrain_restriction = class_.from_bytes_with_count(self, terrains_used_1)
171+
elements.append(terrain_restriction)
172+
return elements

src/genieutils/datatypes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import struct
2+
3+
4+
class String:
5+
@staticmethod
6+
def from_bytes(content: memoryview) -> str:
7+
return bytes(content).rstrip(b'\0').decode()
8+
9+
@staticmethod
10+
def to_bytes(content: str, length=None) -> bytes:
11+
encoded = content.encode()
12+
if not length:
13+
length = len(encoded) + 1
14+
zfill = length - len(encoded)
15+
return encoded + (b'\0' * zfill)
16+
17+
18+
class Int:
19+
@staticmethod
20+
def from_bytes(content: memoryview, signed=True) -> int:
21+
return int.from_bytes(content, byteorder='little', signed=signed)
22+
23+
@staticmethod
24+
def to_bytes(content: int, length=2, signed=True) -> bytes:
25+
return content.to_bytes(length, 'little', signed=signed)
26+
27+
28+
class Float:
29+
@staticmethod
30+
def from_bytes(content: memoryview) -> float:
31+
return struct.unpack('f', content)[0]
32+
33+
@staticmethod
34+
def to_bytes(content: float) -> bytes:
35+
return struct.pack('f', content)

0 commit comments

Comments
 (0)