talespire-encoding 1.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: talespire-encoding
3
+ Version: 1.2.2
4
+ Summary: Encoding and decoding tools for TaleSpire data
5
+ Author: Baldrax
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Baldrax/TaleSpire-Encoding-Python
8
+ Project-URL: Repository, https://github.com/Baldrax/TaleSpire-Encoding-Python
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == "dev"
13
+
14
+ # TaleSpire-Encoding-Python
15
+ Encoding/Decoding tools for TaleSpire
16
+
17
+ This repository is just getting started.
18
+
19
+ It currently contains two encoding types:
20
+ - Slabs (v1, v2)
21
+ - Creature Blueprints (v1, v2)
22
+
23
+ ## Installation:
24
+ With pip you can install specific releases.
25
+ ```
26
+ pip install git+https://github.com/Baldrax/TaleSpire-Encoding-Python.git@vX.X.X
27
+ ```
28
+ Replace the version with the version you want to install:
29
+ - [Latest Releases](https://github.com/Baldrax/TaleSpire-Encoding-Python/releases)
30
+
31
+ ## Slabs Example usage:
32
+ ```python
33
+ from ts_encoding.slab import TSSlab
34
+
35
+ example_slab_code = ("H4sIAAAAAAAACjv369xFJgZGBgYGgUWHGX9Pme/S4z7T7pZ"
36
+ "doRonUGwCSIKhgRFMS0L4DTwMDCcYwOIsDBDADOZLQsRB8g"
37
+ "xQPgOcDwD7jJ0vaAAAAA==")
38
+
39
+ slab = TSSlab()
40
+ slab.decode_slab(example_slab_code)
41
+
42
+ # The slab contents are now in a dictionary `slab.data`
43
+ from pprint import pprint
44
+ pprint(slab.data)
45
+
46
+ """
47
+ {'layout_count': 1,
48
+ 'layouts': [{'instance_count': 9,
49
+ 'instances': [{'degrees': 90.0,
50
+ 'pos_x': 0.0,
51
+ 'pos_y': 0.0,
52
+ 'pos_z': 4.0},
53
+ {'degrees': 0.0,
54
+ 'pos_x': 4.0,
55
+ 'pos_y': 0.0,
56
+ 'pos_z': 4.0},
57
+ {'degrees': 0.0,
58
+ 'pos_x': 2.0,
59
+ 'pos_y': 0.0,
60
+ 'pos_z': 4.0},
61
+ {'degrees': 270.0,
62
+ 'pos_x': 0.0,
63
+ 'pos_y': 0.0,
64
+ 'pos_z': 2.0},
65
+ {'degrees': 180.0,
66
+ 'pos_x': 0.0,
67
+ 'pos_y': 0.0,
68
+ 'pos_z': 0.0},
69
+ {'degrees': 0.0,
70
+ 'pos_x': 4.0,
71
+ 'pos_y': 0.0,
72
+ 'pos_z': 2.0},
73
+ {'degrees': 0.0,
74
+ 'pos_x': 2.0,
75
+ 'pos_y': 0.0,
76
+ 'pos_z': 2.0},
77
+ {'degrees': 0.0,
78
+ 'pos_x': 4.0,
79
+ 'pos_y': 0.0,
80
+ 'pos_z': 0.0},
81
+ {'degrees': 0.0,
82
+ 'pos_x': 2.0,
83
+ 'pos_y': 0.0,
84
+ 'pos_z': 0.0}],
85
+ 'reserved': 0,
86
+ 'uuid': '01c3a210-94fb-449f-8c47-993eda3e7126'}],
87
+ 'magic_num': 3520002766,
88
+ 'num_creatures': 0,
89
+ 'version': 2}
90
+ """
91
+
92
+ # To encode the data
93
+ new_slab_code = slab.encode_slab()
94
+ # The new_slab_code can be pasted into TaleSpire
95
+
96
+ # To create the data start a new TSSlab and edit the data dictionary.
97
+ # Each uuid is a new layout in the dictionary it is currently up to you
98
+ # to format the dictionary properly so that it encodes correctly.
99
+ # In future versions there may be tools to help build the layouts.
100
+
101
+ # Here is an example of creating a single grass tile
102
+ new_slab = TSSlab()
103
+ new_slab.data["layout_count"] = 1
104
+ grass_layout = {
105
+ "instance_count": 1, # The number of grass tiles
106
+ "instances": [{
107
+ "degrees": 0.0, # Yaw rotation in degrees 0-360 it is up to you to give valid values
108
+ "pos_x": 0.0, # X position within slab
109
+ "pos_y": 0.0, # Y position within slab
110
+ "pos_z": 0.0}], # Z position within slab
111
+ "reserved": 0, # Always set to 0
112
+ "uuid": "01c3a210-94fb-449f-8c47-993eda3e7126" # Asset UUID
113
+ }
114
+ new_slab.data["layouts"].append(grass_layout)
115
+ new_slab_code = new_slab.encode_slab()
116
+
117
+ # You can paste that new_slab_code into TaleSpire to see your grass tile
118
+ ```
119
+
120
+ ## Creature Blueprint Example usage:
121
+ ```python
122
+ from ts_encoding.creature_bp import TSCreature
123
+ from pprint import pprint
124
+
125
+ url_from_TS = ("talespire://creature-blueprint/"
126
+ "AgAMV2hpdGUgTWVlcGxlAQAAACMAYnI6MDAwMDAwMDAwMDAwMDAwMD"
127
+ "AwMDAwMDAwMDQ3MDQxMDABAAAAAI49u_vNJQRDrZctETEJKR8ABAAA"
128
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgQQAAIEEAACBBAAAgQQ"
129
+ "AAIEEAACBBAAAgQQAAIEEAACBBAAAgQQAAIEEAACBBAAAgQQAAIEEA"
130
+ "ACBBAAAgQQAAIEEAACBBAAAA")
131
+
132
+ bp = TSCreature()
133
+ bp.decode_url(url_from_TS)
134
+ pprint(bp.data)
135
+
136
+ # You can change any of the data in `bp.data`
137
+ bp.data["name"] = "New Name"
138
+ bp.data["stats"][0] = {"max": 100.0, "value": 100.0} # Changes hp to 100
139
+
140
+ # To re-encode the data
141
+ new_url = bp.encode_url()
142
+
143
+ print(new_url) # You can copy/paste this new URL into TaleSpire
144
+ ```
@@ -0,0 +1,11 @@
1
+ ts_encoding/__init__.py,sha256=9V8f47HbY_y2d0P7ilE4n0chgE4e2UKuOuL9MDwwzVs,322
2
+ ts_encoding/assets.py,sha256=7jtDLbvZcpj0Ka0Jng0SmiN07vaTAPuSTT5Vbz0-b4A,5216
3
+ ts_encoding/common.py,sha256=vTNXa2gYU7ib0yXtu1oO_aIwMmLTk9KequcwzLSGiTk,6745
4
+ ts_encoding/creature_bp.py,sha256=_fISg6jayCxrkX6eW038Gn5c9hLgNO9Qkh5lYd3eFlU,13745
5
+ ts_encoding/exceptions.py,sha256=E13sbDOaK8iUBUjMsn_oKPPyYOVjeevY9qu8iihgwwQ,892
6
+ ts_encoding/slab.py,sha256=uv6f-9owrPm4Y2f-tcDBrIv5eIIEomrMPGU7lgqyDFA,9708
7
+ ts_encoding/slab_testing.py,sha256=sumuW0h9PzpUZfOOMpIy118_EJJNwE0QXfIPEif8klM,18552
8
+ talespire_encoding-1.2.2.dist-info/METADATA,sha256=01fx0BOEwVDiluN_9VNOs5-w6h3QLPkQrew6yHXh3PE,5186
9
+ talespire_encoding-1.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ talespire_encoding-1.2.2.dist-info/top_level.txt,sha256=CrhA5IN_Ee5Brotf1rSdvULycalNETRGm2coN5quVbA,12
11
+ talespire_encoding-1.2.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ts_encoding
@@ -0,0 +1,17 @@
1
+ __version__ = "1.2.2"
2
+
3
+ from .exceptions import (
4
+ SlabExceedsSizeLimit,
5
+ BadSlabCode,
6
+ UnsupportedSlabVersion,
7
+ InvalidTaleSpireDirectory,
8
+ InvalidAssetType
9
+ )
10
+
11
+ __all__ = [
12
+ "SlabExceedsSizeLimit",
13
+ "BadSlabCode",
14
+ "UnsupportedSlabVersion",
15
+ "InvalidTaleSpireDirectory",
16
+ "InvalidAssetType"
17
+ ]
ts_encoding/assets.py ADDED
@@ -0,0 +1,152 @@
1
+ """
2
+ Tools for reading in TaleSpire assets.
3
+ These require a path to a valid TaleSpire install.
4
+ It utilizes the `index.json` files within the installed location to get asset information.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ from pathlib import Path
11
+
12
+ from ts_encoding import InvalidTaleSpireDirectory, InvalidAssetType
13
+
14
+
15
+ def get_asset_index_paths(ts_basedir: Path | str) -> list[Path]:
16
+ """
17
+ Given the base TaleSpire Directory get a list of all the paths to index.json files.
18
+
19
+ Args:
20
+ ts_basedir: The base directory that TaleSpire is installed in.
21
+ """
22
+ ts_base_path = Path(str(ts_basedir))
23
+ taleweaver_dir = ts_base_path / "Taleweaver"
24
+
25
+ if taleweaver_dir.is_dir():
26
+ return list(taleweaver_dir.rglob("index.json"))
27
+ else:
28
+ raise InvalidTaleSpireDirectory(f"The supplied TaleSpire directory is not valid:\n\t{ts_base_path}")
29
+
30
+
31
+ def read_index_file(index_file: Path | str) -> dict:
32
+ """
33
+ Read in the given index file and return it as a dictionary.
34
+
35
+ Args:
36
+ index_file: The TaleSpire index.json file to be read.
37
+ """
38
+ index_file_path = Path(str(index_file))
39
+ with index_file_path.open("r", encoding="utf-8") as f:
40
+ index_dict = json.load(f)
41
+
42
+ return index_dict
43
+
44
+
45
+ def get_index_dicts(ts_basedir: Path | str) -> dict:
46
+ """
47
+ Given the base TaleSpire directory return a dictionary containing
48
+ all the content pack asset indexes as dictionaries.
49
+
50
+ Args:
51
+ ts_basedir: The base directory that TaleSpire is installed in.
52
+ """
53
+ index_files = get_asset_index_paths(ts_basedir)
54
+
55
+ index_dicts = {}
56
+
57
+ for index_file in index_files:
58
+ index_dict = read_index_file(index_file)
59
+ index_name = index_dict["Name"]
60
+ index_dicts[index_name] = {"path": str(index_file), "index": index_dict}
61
+
62
+ return index_dicts
63
+
64
+
65
+ class TSAssetLib:
66
+
67
+ # This is the default list of asset loaded as well as the valid types excepted.
68
+ default_asset_filter = ["Tiles", "Props", "Creatures", "Music"]
69
+
70
+ def __init__(self, ts_basedir, asset_filter: list[str] | None = None):
71
+ """
72
+ TaleSpire Asset Library
73
+ This reads in all the TaleSpire assets and stores them both in index form and by UUID.
74
+
75
+ A filter can be set to limit what types of assets are stored.
76
+ Valid types are: ["Tiles", "Props", "Creatures", "Music"]
77
+ The default is all asset types.
78
+
79
+ Args:
80
+ ts_basedir: The base directory that TaleSpire is installed in.
81
+ asset_filter: A list of asset types to use as a filter.
82
+ """
83
+ self.index_dicts = get_index_dicts(ts_basedir)
84
+ self.asset_filter = asset_filter if asset_filter else self.default_asset_filter
85
+ self.index_names = list(self.index_dicts.keys())
86
+ self.asset_uuid_dict: dict[str, TSAsset] = {}
87
+ self._build_asset_uuid_dict()
88
+
89
+ def _build_asset_uuid_dict(self) -> None:
90
+ for index_name in self.index_names:
91
+ index_path = Path(self.index_dicts[index_name]["path"])
92
+ index_dict = self.index_dicts[index_name]["index"]
93
+
94
+ icon_atlases = []
95
+ for atlas_entry in index_dict["IconsAtlases"]:
96
+ icon_atlases.append(atlas_entry["Path"])
97
+
98
+ for asset_type in self.asset_filter:
99
+ asset_type = asset_type.title()
100
+ if asset_type not in self.default_asset_filter:
101
+ raise InvalidAssetType(
102
+ f"Invalid Asset Filter: {asset_type}\nValid types are: {self.default_asset_filter}"
103
+ )
104
+
105
+ for asset_dict in self.index_dicts[index_name]["index"][asset_type]:
106
+ if "Icon" in asset_dict:
107
+ asset = TSIconAsset(asset_dict, asset_type)
108
+ atlas_index = asset_dict["Icon"]["AtlasIndex"]
109
+ asset.icon_atlas = str(index_path.parent / icon_atlases[atlas_index])
110
+ else:
111
+ asset = TSAsset(asset_dict, asset_type)
112
+
113
+ self.asset_uuid_dict[asset.id] = asset
114
+
115
+ def asset(self, asset_uuid: str) -> TSAsset:
116
+ """
117
+ Given a UUID return the asset.
118
+
119
+ Args:
120
+ asset_uuid: The TaleSpire asset UUID
121
+ """
122
+ asset = self.asset_uuid_dict.get(asset_uuid, None)
123
+ return asset
124
+
125
+ def assets(self) -> list[TSAsset]:
126
+ """Returns a list of all the assets in the library."""
127
+ return list(self.asset_uuid_dict.values())
128
+
129
+
130
+ class TSAsset:
131
+
132
+ def __init__(self, asset_dict, asset_type):
133
+ self.asset_dict = asset_dict
134
+ self.asset_type = asset_type
135
+ self.id = asset_dict["Id"].lower()
136
+ self.name = asset_dict["Name"]
137
+ self.deprecated = asset_dict["IsDeprecated"] == 1
138
+
139
+
140
+ class TSIconAsset(TSAsset):
141
+
142
+ def __init__(self, asset_dict, asset_type):
143
+ super().__init__(asset_dict, asset_type)
144
+ self.icon_atlas_index = asset_dict["Icon"]["AtlasIndex"]
145
+ self.icon_atlas = ""
146
+ atlas_region = asset_dict["Icon"]["Region"]
147
+ self.atlas_region = (
148
+ atlas_region["x"],
149
+ atlas_region["y"],
150
+ atlas_region["width"],
151
+ atlas_region["height"]
152
+ )
ts_encoding/common.py ADDED
@@ -0,0 +1,210 @@
1
+ """
2
+ Base TS Encoding classes and functions for use in the specific encoders.
3
+
4
+ URL Schemes are documented here:
5
+ https://talespire.com/url-scheme
6
+
7
+ Standard Byte order is little-endian
8
+ """
9
+ import base64
10
+ import struct
11
+ import uuid
12
+
13
+
14
+ class TSCodingBase:
15
+ """
16
+ A Base Class to Decode and Encode a TaleSpire content.
17
+
18
+ For Decoding:
19
+ The class stores the binary data and an offset which represents the current position/index we are at in the
20
+ binary data. Each unpack method then increments the offset by the proper amount so we can decode the next
21
+ step without having to feed in the offset index.
22
+ """
23
+
24
+ def __init__(self):
25
+ self.data = {}
26
+ self._init_data()
27
+ self._version = 0
28
+ self._encode_version = 2
29
+ self._code = None
30
+ self._binary_data = None
31
+ self._offset = 0
32
+
33
+ def _init_data(self) -> None:
34
+ """
35
+ This is intended to be overridden by the subclass.
36
+ The data dictionary should be initialized to a default state declaring all the data needed.
37
+ """
38
+ self.data = {
39
+ "version": 1,
40
+ }
41
+
42
+ def _decode(self) -> None:
43
+ """
44
+ Preps self._code into self._binary_data then runs self._decode_steps()
45
+ It is up to the subclass to set self._code
46
+ """
47
+ self._binary_data = base64.b64decode(self._code) # Decode the encoded string into binary data
48
+ self._offset = 0 # Reset the offset index of the binary data
49
+ self._decode_steps()
50
+
51
+ def _decode_steps(self) -> None:
52
+ """
53
+ This is meant to be overridden by the subclass.
54
+ Keep these steps as simple as possible, 1 or 2 lines or break them out into another method.
55
+ When the steps are complete all the binary data should be unpacked into `self.data`
56
+ """
57
+ pass
58
+
59
+ def _encode(self) -> None:
60
+ """
61
+ Resets self._binary_data, runs self._encode_steps and encodes the new binary data to self._code
62
+ It is up to the subclass to reveal self._code to the user or application.
63
+ """
64
+ self._binary_data = bytearray()
65
+ self._encode_steps()
66
+ self._code = base64.b64encode(self._binary_data)
67
+
68
+ def _encode_steps(self) -> None:
69
+ """
70
+ This is meant to be overridden by the subclass.
71
+ Keep these steps as simple as possible, 1 or 2 lines or break them out into another method.
72
+ When the steps are complete everything in `self.data` should be packed into `self._binary_data`
73
+ """
74
+ pass
75
+
76
+ def _unpack_u8(self) -> int:
77
+ """Unpacks a u8 - Unsigned 8-bit integer (1 byte)"""
78
+ result, = struct.unpack_from("<B", self._binary_data, self._offset)
79
+ self._offset += 1
80
+ return result
81
+
82
+ def _unpack_u16(self) -> int:
83
+ """Unpacks a u16 - Unsigned Short Integer (2 bytes)"""
84
+ result, = struct.unpack_from("<H", self._binary_data, self._offset)
85
+ self._offset += 2
86
+ return result
87
+
88
+ def _unpack_u32(self) -> int:
89
+ """Unpacks a u32 - Unsigned 32-bit Integer (4 bytes)"""
90
+ result, = struct.unpack_from("<I", self._binary_data, self._offset)
91
+ self._offset += 4
92
+ return result
93
+
94
+ def _unpack_u64(self) -> int:
95
+ """Unpacks a u64 - Unsigned 64-bit integer (8 bytes)"""
96
+ result, = struct.unpack_from("<Q", self._binary_data, self._offset)
97
+ self._offset += 8
98
+ return result
99
+
100
+ def _unpack_utf8(self, num_bytes: int) -> str:
101
+ """
102
+ Unpacks a UTF-8 string of fixed length.
103
+
104
+ Args:
105
+ num_bytes (int): Number of bytes to read from the stream.
106
+
107
+ Returns:
108
+ str: Decoded UTF-8 string
109
+ """
110
+ result, = struct.unpack_from(f"<{num_bytes}s", self._binary_data, self._offset)
111
+ self._offset += num_bytes
112
+ return result
113
+
114
+ def _unpack_i32(self) -> int:
115
+ """Unpacks an i32 - Signed 32-bit integer (4 bytes)"""
116
+ result, = struct.unpack_from("<i", self._binary_data, self._offset)
117
+ self._offset += 4
118
+ return result
119
+
120
+ def _unpack_uuid(self) -> str:
121
+ """Unpacks a UUID - 128-bit identifier (16 bytes)"""
122
+ result, = struct.unpack_from(f"16s", self._binary_data, self._offset)
123
+ self._offset += 16
124
+ return str(uuid.UUID(bytes=result))
125
+
126
+ def _unpack_slab_uuid(self) -> str:
127
+ """Unpacks a slab UUID - 128-bit identifier (16 bytes, mixed-endian layout)"""
128
+ fields = struct.unpack_from("<IHH8B", self._binary_data, self._offset)
129
+ self._offset += 16
130
+ asset_uuid = uuid.UUID(
131
+ fields=(
132
+ fields[0],
133
+ fields[1],
134
+ fields[2],
135
+ fields[3],
136
+ fields[4],
137
+ int.from_bytes(fields[5:], byteorder="big")
138
+ )
139
+ )
140
+ return str(asset_uuid)
141
+
142
+ def _pack_u8(self, value: int) -> None:
143
+ """
144
+ Packs a u8 - Unsigned 8-bit integer (1 byte)
145
+
146
+ Args:
147
+ value: The integer value to pack.
148
+ """
149
+ self._binary_data.extend(struct.pack("<B", value))
150
+
151
+ def _pack_u16(self, value: int) -> None:
152
+ """
153
+ Packs a u16 - Unsigned 16-bit integer (2 bytes)
154
+
155
+ Args:
156
+ value: The integer value to pack.
157
+ """
158
+ self._binary_data.extend(struct.pack("<H", value))
159
+
160
+ def _pack_u32(self, value: int) -> None:
161
+ """
162
+ Packs a u32 - Unsigned 32-bit integer (4 bytes)
163
+
164
+ Args:
165
+ value: The integer value to pack.
166
+ """
167
+ self._binary_data.extend(struct.pack("<I", value))
168
+
169
+ def _pack_u64(self, value: int) -> None:
170
+ """
171
+ Packs a u64 - Unsigned 64-bit Integer (8 bytes)
172
+
173
+ Args:
174
+ value: The integer value to pack.
175
+ """
176
+ self._binary_data.extend(struct.pack("<Q", value))
177
+
178
+ def _pack_uuid(self, uuid_str: str) -> None:
179
+ """
180
+ Packs a UUID - 128-bit identifier (16 bytes)
181
+
182
+ Args:
183
+ uuid_str: The UUID string.
184
+ """
185
+ self._binary_data.extend(uuid.UUID(uuid_str).bytes)
186
+
187
+ def _pack_slab_uuid(self, uuid_str: str):
188
+ """
189
+ Packs a Slab UUID - 128-bit identifier (16 bytes, mixed-endian layout)
190
+
191
+ Args:
192
+ uuid_str: The UUID String.
193
+ """
194
+ fields = uuid.UUID(uuid_str).fields
195
+
196
+ node_bytes = fields[5].to_bytes(6, byteorder="big")
197
+
198
+ self._binary_data.extend(struct.pack(
199
+ "<IHH8B",
200
+ fields[0], fields[1], fields[2], fields[3], fields[4], *node_bytes
201
+ ))
202
+
203
+ def _pack_i32(self, value: int):
204
+ """
205
+ Packs an i32 - Signed 32-bit integer (4 bytes)
206
+
207
+ Args:
208
+ value: The integer to pack.
209
+ """
210
+ self._binary_data.extend(struct.pack("<i", value))