-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathxp_loader.py
243 lines (186 loc) · 9.25 KB
/
xp_loader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# Changed slightly to be compatible with Python 3
from sys import platform
if platform == 'darwin':
import tcod as libtcod
elif platform == 'linux':
import libtcodpy_local as libtcod
else:
import libtcodpy as libtcod
import binascii
##################################
# In-memory XP format is as follows:
# Returned structure is a dictionary with the keys version, layers, width, height, and layer_data
## Version is stored in case it's useful for someone, but as mentioned in the format description it probably won't be unless format changes happen
## Layers is a full 32 bit int, though right now REXPaint only exports or manages up to 4 layers
## Width and height are extracted from the layer with largest width and height - this value will hold true for all layers for now as per the format description
## layer_data is a list of individual layers, which are stored in the following format
### Each layer is a dictionary with keys width, height (see above), and cells.
### Cells is a row major 2d array of, again, dictionaries with the values 'keycode' (ascii keycode), 'fore_r/g/b', and 'back_r/g/b' (technically ints but in value 0-255)
##################################
##################################
# Used primarily internally to parse the data, feel free to reference them externally if it's useful.
# Changing these programattically will, of course, screw up the parsing (unless the format changes and you're using an old copy of this file)
##################################
version_bytes = 4
layer_count_bytes = 4
layer_width_bytes = 4
layer_height_bytes = 4
layer_keycode_bytes = 4
layer_fore_rgb_bytes = 3
layer_back_rgb_bytes = 3
layer_cell_bytes = layer_keycode_bytes + layer_fore_rgb_bytes + layer_back_rgb_bytes
##################################
# REXPaint color key for transparent background colors. Not directly used here, but you should reference this when calling libtcod's console_set_key_color on offscreen consoles.
##################################
transparent_cell_back_r = 255
transparent_cell_back_g = 0
transparent_cell_back_b = 255
####################################################################
# START LIBTCOD SPECIFIC CODE
##################################
# Used primarily internally to parse the data, feel free to reference them externally if it's useful.
# Changing these programattically will, of course, screw up the parsing (unless the format changes and you're using an old copy of this file)
##################################
#the solid square character
poskey_tile_character = 219
#some or all of the below may appear in libtcod's color definitions; and in fact, you can use libtcod colors as you please for position keys.
#These are merely the colors provided in the accompanying palette.
poskey_color_red = libtcod.Color(255, 0, 0)
poskey_color_lightpurple = libtcod.Color(254, 0, 255) # specifically 254 as 255, 0, 255 is considered a transparent key color in REXPaint
poskey_color_orange = libtcod.Color(255, 128, 0)
poskey_color_pink = libtcod.Color(255, 0, 128)
poskey_color_green = libtcod.Color(0, 255, 0)
poskey_color_teal = libtcod.Color(0, 255, 255)
poskey_color_yellow = libtcod.Color(255, 255, 0)
poskey_color_blue = libtcod.Color(0, 0, 255)
poskey_color_lightblue = libtcod.Color(0, 128, 255)
poskey_color_purple = libtcod.Color(128, 0, 255)
poskey_color_white = libtcod.Color(255, 255, 255)
##################################
# please note - this function writes the contents of transparent cells to the provided console.
# If you're building an offscreen console and want to use the default (or some other) color for transparency, please call libtcod's console.set_key_color(color)
##################################
def load_layer_to_console(console, xp_file_layer):
if not xp_file_layer['width'] or not xp_file_layer['height']:
raise AttributeError('Attempted to call load_layer_to_console on data that didn\'t have a width or height key, check your data')
for x in range(xp_file_layer['width']):
for y in range(xp_file_layer['height']):
cell_data = xp_file_layer['cells'][x][y]
fore_color = libtcod.Color(cell_data['fore_r'], cell_data['fore_g'], cell_data['fore_b'])
back_color = libtcod.Color(cell_data['back_r'], cell_data['back_g'], cell_data['back_b'])
libtcod.console_put_char_ex(console, x, y, cell_data['keycode'], fore_color, back_color)
def get_position_key_xy(xp_file_layer, poskey_color):
for x in range(xp_file_layer['width']):
for y in range(xp_file_layer['height']):
cell_data = xp_file_layer['cells'][x][y]
if cell_data['keycode'] == poskey_tile_character:
fore_color_matches = cell_data['fore_r'] == poskey_color.r and cell_data['fore_g'] == poskey_color.g and cell_data['fore_b'] == poskey_color.b
back_color_matches = cell_data['back_r'] == poskey_color.r and cell_data['back_g'] == poskey_color.g and cell_data['back_b'] == poskey_color.b
if fore_color_matches or back_color_matches:
return (x, y)
raise LookupError('No position key was specified for color ' + str(poskey_color) + ', check your .xp file and/or the input color')
# END LIBTCOD SPECIFIC CODE
####################################################################
##################################
# loads in an xp file from an unzipped string (gained from opening a .xp file with gzip and calling .read())
# reverse_endian controls whether the slices containing data for things like layer width, height, number of layers, etc. is reversed
# so far as I can tell Python is doing int conversions in big-endian, while the .xp format stores them in little-endian
# I may just not be aware of it being unneeded, but have it there in case
##################################
def load_xp_string(file_string, reverse_endian=True):
offset = 0
version = file_string[offset : offset + version_bytes]
offset += version_bytes
layer_count = file_string[offset : offset + layer_count_bytes]
offset += layer_count_bytes
if reverse_endian:
version = version[::-1]
layer_count = layer_count[::-1]
# hex-encodes the numbers then converts them to an int
version = int(binascii.b2a_hex(version), 16)
layer_count = int(binascii.b2a_hex(layer_count), 16)
layers = []
current_largest_width = 0
current_largest_height = 0
for layer in range(layer_count):
#slight lookahead to figure out how much data to feed load_layer
this_layer_width = file_string[offset:offset + layer_width_bytes]
this_layer_height = file_string[offset + layer_width_bytes:offset + layer_width_bytes + layer_height_bytes]
if reverse_endian:
this_layer_width = this_layer_width[::-1]
this_layer_height = this_layer_height[::-1]
this_layer_width = int(binascii.b2a_hex(this_layer_width), 16)
this_layer_height = int(binascii.b2a_hex(this_layer_height), 16)
current_largest_width = max(current_largest_width, this_layer_width)
current_largest_height = max(current_largest_height, this_layer_height)
layer_data_size = layer_width_bytes + layer_height_bytes + (layer_cell_bytes * this_layer_width * this_layer_height)
layer_data_raw = file_string[offset:offset + layer_data_size]
layer_data = parse_layer(file_string[offset:offset + layer_data_size], reverse_endian)
layers.append(layer_data)
offset += layer_data_size
return {
'version':version,
'layer_count':layer_count,
'width':current_largest_width,
'height':current_largest_height,
'layer_data':layers
}
##################################
# Takes a single layer's data and returns the format listed at the top of the file for a single layer.
##################################
def parse_layer(layer_string, reverse_endian=True):
offset = 0
width = layer_string[offset:offset + layer_width_bytes]
offset += layer_width_bytes
height = layer_string[offset:offset + layer_height_bytes]
offset += layer_height_bytes
if reverse_endian:
width = width[::-1]
height = height[::-1]
width = int(binascii.b2a_hex(width), 16)
height = int(binascii.b2a_hex(height), 16)
cells = []
for x in range(width):
row = []
for y in range(height):
cell_data_raw = layer_string[offset:offset + layer_cell_bytes]
cell_data = parse_individual_cell(cell_data_raw, reverse_endian)
row.append(cell_data)
offset += layer_cell_bytes
cells.append(row)
return {
'width':width,
'height':height,
'cells':cells
}
##################################
# Pulls out the keycode and the foreground/background RGB values from a single cell's data, returning them in the format listed at the top of this file for a single cell.
##################################
def parse_individual_cell(cell_string, reverse_endian=True):
offset = 0
keycode = cell_string[offset:offset + layer_keycode_bytes]
if reverse_endian:
keycode = keycode[::-1]
keycode = int(binascii.b2a_hex(keycode), 16)
offset += layer_keycode_bytes
fore_r = int(binascii.b2a_hex(cell_string[offset:offset+1]), 16)
offset += 1
fore_g = int(binascii.b2a_hex(cell_string[offset:offset+1]), 16)
offset += 1
fore_b = int(binascii.b2a_hex(cell_string[offset:offset+1]), 16)
offset += 1
back_r = int(binascii.b2a_hex(cell_string[offset:offset+1]), 16)
offset += 1
back_g = int(binascii.b2a_hex(cell_string[offset:offset+1]), 16)
offset += 1
back_b = int(binascii.b2a_hex(cell_string[offset:offset+1]), 16)
offset += 1
return {
'keycode':keycode,
'fore_r':fore_r,
'fore_g':fore_g,
'fore_b':fore_b,
'back_r':back_r,
'back_g':back_g,
'back_b':back_b,
}