1 | #!/usr/bin/env python |
---|
2 | |
---|
3 | # This file is part of PARPG. |
---|
4 | |
---|
5 | # PARPG is free software: you can redistribute it and/or modify |
---|
6 | # it under the terms of the GNU General Public License as published by |
---|
7 | # the Free Software Foundation, either version 3 of the License, or |
---|
8 | # (at your option) any later version. |
---|
9 | |
---|
10 | # PARPG is distributed in the hope that it will be useful, |
---|
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
13 | # GNU General Public License for more details. |
---|
14 | |
---|
15 | # You should have received a copy of the GNU General Public License |
---|
16 | # along with PARPG. If not, see <http://www.gnu.org/licenses/>. |
---|
17 | |
---|
18 | import sys,cPickle |
---|
19 | from xml.sax import make_parser |
---|
20 | from xml.sax.handler import ContentHandler |
---|
21 | |
---|
22 | # this code is for building the transition layer for the map |
---|
23 | # the world map is built of two layers: one for the world floor, and the other |
---|
24 | # for the all the objects (including the player and NPC) |
---|
25 | # This program accepts one argument, the original XML map file, |
---|
26 | # and outputs another file with 1 or more added layers: the layers holding |
---|
27 | # the information for the transition tiles that are rendered over the ground |
---|
28 | |
---|
29 | # usage: transition.py mapfile |
---|
30 | # outputs file new.xml, a simple text file that contains ONLY the new layers |
---|
31 | # needed in the mapfile. At the moment you have to splice these in by hand; |
---|
32 | # they must come AFTER the ground layer but BEFORE the building and objects |
---|
33 | # layers. PM maximinus at the PARPG forums if any questions. |
---|
34 | |
---|
35 | # some simple defines for each part of the tile |
---|
36 | TOP = 1 |
---|
37 | RIGHT = 2 |
---|
38 | BOTTOM = 4 |
---|
39 | LEFT = 8 |
---|
40 | TOP_LEFT = 16 |
---|
41 | TOP_RIGHT = 32 |
---|
42 | BOTTOM_RIGHT = 64 |
---|
43 | BOTTOM_LEFT = 128 |
---|
44 | |
---|
45 | # side transition tiles always block corner tiles |
---|
46 | # but which ones? |
---|
47 | TOP_BLOCK = TOP_RIGHT+TOP_LEFT |
---|
48 | RIGHT_BLOCK = TOP_RIGHT+BOTTOM_RIGHT |
---|
49 | BOTTOM_BLOCK = BOTTOM_RIGHT+BOTTOM_LEFT |
---|
50 | LEFT_BLOCK = TOP_LEFT+BOTTOM_LEFT |
---|
51 | NONE = 0 |
---|
52 | |
---|
53 | # now for each of the 15 different possible side variations we |
---|
54 | # can know what corner pieces do not need to be drawn |
---|
55 | # this table stores all of the allowed combinations |
---|
56 | # based on the bit pattern for the side elements |
---|
57 | |
---|
58 | CORNER_LOOKUP = [BOTTOM_BLOCK, LEFT_BLOCK, |
---|
59 | BOTTOM_LEFT, TOP_BLOCK, |
---|
60 | NONE, TOP_LEFT, |
---|
61 | NONE, RIGHT_BLOCK, |
---|
62 | BOTTOM_RIGHT, NONE, |
---|
63 | NONE, TOP_RIGHT, |
---|
64 | NONE, NONE, |
---|
65 | NONE] |
---|
66 | |
---|
67 | class XMLTileData: |
---|
68 | def __init__(self, x, y, z, o, i=None): |
---|
69 | self.x = x |
---|
70 | self.y = y |
---|
71 | self.z = z |
---|
72 | self.object = o |
---|
73 | self.ident = i |
---|
74 | |
---|
75 | class XMLLayerData: |
---|
76 | """Class to store one complete layer""" |
---|
77 | def __init__(self, x, y, name): |
---|
78 | self.x_scale = x |
---|
79 | self.y_scale = y |
---|
80 | self.name = name |
---|
81 | self.tiles = [] |
---|
82 | |
---|
83 | class LocalXMLParser(ContentHandler): |
---|
84 | """Class inherits from ContantHandler, and is used to parse the |
---|
85 | local map data""" |
---|
86 | def __init__(self): |
---|
87 | self.search = "map" |
---|
88 | self.layers = [] |
---|
89 | self.current_layer = False |
---|
90 | self.final = [] |
---|
91 | |
---|
92 | def startElement(self, name, attrs): |
---|
93 | """Called every time we meet a new element""" |
---|
94 | # we are only looking for the 'layer' elements, the rest we ignore |
---|
95 | if(name == "layer"): |
---|
96 | # grab the data and store that as well |
---|
97 | try: |
---|
98 | x = attrs.getValue('x_scale') |
---|
99 | y = attrs.getValue('y_scale') |
---|
100 | name = attrs.getValue('id') |
---|
101 | except(KeyError): |
---|
102 | sys.stderr.write("Error: Layer information invalid") |
---|
103 | sys.exit(False) |
---|
104 | # start a new layer |
---|
105 | self.layers.append(XMLLayerData(x, y, name)) |
---|
106 | self.current_layer = True |
---|
107 | elif(name == "i"): |
---|
108 | # have a current layer? |
---|
109 | if self.current_layer == False: |
---|
110 | sys.stderr.write("Error: item data outside of layer\n") |
---|
111 | sys.exit(False) |
---|
112 | # ok, it's ok, let's parse and add the data |
---|
113 | try: |
---|
114 | x = attrs.getValue('x') |
---|
115 | y = attrs.getValue('y') |
---|
116 | z = attrs.getValue('z') |
---|
117 | o = attrs.getValue('o') |
---|
118 | except(KeyError): |
---|
119 | sys.stderr.write("Error: Data missing in tile definition\n") |
---|
120 | sys.exit(False) |
---|
121 | try: |
---|
122 | i = attrs.getValue('id') |
---|
123 | except(KeyError): |
---|
124 | i = None |
---|
125 | # convert tile co-ords to integers |
---|
126 | x = float(x) |
---|
127 | y = float(y) |
---|
128 | z = float(z) |
---|
129 | # now we have the tile data, save it for later |
---|
130 | self.layers[-1].tiles.append(XMLTileData(x, y, z, o, i)) |
---|
131 | |
---|
132 | def endElement(self, name): |
---|
133 | if(name == "layer"): |
---|
134 | # end of current layer |
---|
135 | self.current_layer=False |
---|
136 | |
---|
137 | class LocalMap: |
---|
138 | def __init__(self): |
---|
139 | self.layers = [] |
---|
140 | self.ttiles = [] |
---|
141 | self.render_tiles =[] |
---|
142 | self.min_x = 0 |
---|
143 | self.max_x = 0 |
---|
144 | self.min_y = 0 |
---|
145 | self.max_y = 0 |
---|
146 | |
---|
147 | def outputTransLayer(self, l_file, l_count): |
---|
148 | if(len(self.render_tiles) == 0): |
---|
149 | return True |
---|
150 | try: |
---|
151 | layer_name = "TransitionLayer" + str(l_count) |
---|
152 | l_file.write(''' <layer x_offset="0.0" pathing="''') |
---|
153 | l_file.write('''cell_edges_and_diagonals" y_offset="0.0"''') |
---|
154 | l_file.write(''' grid_type="square" id="''') |
---|
155 | l_file.write(layer_name + '''"''') |
---|
156 | l_file.write(''' x_scale="1" y_scale="1" rotation="0.0">\n''') |
---|
157 | l_file.write(' <instances>\n') |
---|
158 | for tile in self.render_tiles: |
---|
159 | l_file.write(''' <i x="''') |
---|
160 | l_file.write(str(tile.x)) |
---|
161 | l_file.write('''" o="''') |
---|
162 | l_file.write(tile.object) |
---|
163 | l_file.write('''" y="''') |
---|
164 | l_file.write(str(tile.y)) |
---|
165 | l_file.write('''" r="0" z="0.0"></i>\n''') |
---|
166 | l_file.write(' </instances>\n </layer>\n') |
---|
167 | except(IOError): |
---|
168 | sys.stderr.write("Error: Couldn't write data") |
---|
169 | return False |
---|
170 | return True |
---|
171 | |
---|
172 | def GetSurroundings(self, x, y, search): |
---|
173 | """Function called by buildTransLayer to see if a tile needs to |
---|
174 | display transition graphics over it (drawn on another layer)""" |
---|
175 | # check all of the tiles around the current tile |
---|
176 | value=0 |
---|
177 | if(self.pMatchSearch(x,y+1,search) == True): |
---|
178 | value += RIGHT |
---|
179 | if(self.pMatchSearch(x-1,y+1,search) == True): |
---|
180 | value += BOTTOM_RIGHT |
---|
181 | if(self.pMatchSearch(x-1,y,search) == True): |
---|
182 | value += BOTTOM |
---|
183 | if(self.pMatchSearch(x-1,y-1,search) == True): |
---|
184 | value += BOTTOM_LEFT |
---|
185 | if(self.pMatchSearch(x,y-1,search) == True): |
---|
186 | value += LEFT |
---|
187 | if(self.pMatchSearch(x+1,y-1,search) == True): |
---|
188 | value += TOP_LEFT |
---|
189 | if(self.pMatchSearch(x+1,y,search) == True): |
---|
190 | value += TOP |
---|
191 | if(self.pMatchSearch(x+1,y+1,search) == True): |
---|
192 | value += TOP_RIGHT |
---|
193 | return value |
---|
194 | |
---|
195 | def getTransitionTiles(self, search): |
---|
196 | """Build up and return a list of the tiles that might |
---|
197 | need a transition tiles layed over them""" |
---|
198 | size = len(search) |
---|
199 | tiles = [] |
---|
200 | for t in self.layers[0].tiles: |
---|
201 | # we are only interested in tiles that DON'T have what we are |
---|
202 | # are looking for (because they might need a transition gfx) |
---|
203 | if(t.object != None and t.object[:size] != search): |
---|
204 | # whereas now we we need to check all the tiles around |
---|
205 | # this tile |
---|
206 | trans_value = self.GetSurroundings(t.x, t.y, search) |
---|
207 | if(trans_value != 0): |
---|
208 | # we found an actual real transition |
---|
209 | tiles.append([t.x, t.y, trans_value]) |
---|
210 | return tiles |
---|
211 | |
---|
212 | def getTransitionName(self, base, value, corner=False): |
---|
213 | if(corner == False): |
---|
214 | name = base + "-ts" |
---|
215 | else: |
---|
216 | name = base + "-tc" |
---|
217 | if(value < 10): |
---|
218 | name += "0" |
---|
219 | name += str(value) |
---|
220 | return name |
---|
221 | |
---|
222 | def buildTransLayer(self, search): |
---|
223 | """Build up the data for a transition layer |
---|
224 | search is the string that matches the start of the name of |
---|
225 | each tile that we are looking for""" |
---|
226 | transition_tiles = self.getTransitionTiles(search) |
---|
227 | # now we have all the possible tiles, lets see what they |
---|
228 | # actually need to have rendered |
---|
229 | for t in transition_tiles: |
---|
230 | # first we calculate the side tiles: |
---|
231 | sides = (t[2]&15) |
---|
232 | if(sides != 0): |
---|
233 | # there are some side tiles to be drawn. Now we just |
---|
234 | # need to see if there are any corners to be done |
---|
235 | corners = (t[2]&240)&(CORNER_LOOKUP[sides-1]) |
---|
236 | if(corners != 0): |
---|
237 | # we must add a corner piece as well |
---|
238 | corners = corners/16 |
---|
239 | name = self.getTransitionName(search, corners, True) |
---|
240 | self.ttiles.append(XMLTileData(t[0], t[1], 0, name)) |
---|
241 | # add the side tile pieces |
---|
242 | name = self.getTransitionName(search, sides, False) |
---|
243 | self.ttiles.append(XMLTileData(t[0], t[1], 0, name)) |
---|
244 | else: |
---|
245 | # there are no side tiles, so let's just look at |
---|
246 | # the corners (quite easy): |
---|
247 | corners = (t[2]&240)/16 |
---|
248 | if(corners != 0): |
---|
249 | # there is a corner piece needed |
---|
250 | name = self.getTransitionName(search, corners, True) |
---|
251 | self.ttiles.append(XMLTileData(t[0], t[1], 0, name)) |
---|
252 | |
---|
253 | def loadFromXML(self, filename): |
---|
254 | """Load a map from the XML file used in Fife |
---|
255 | Returns True if it worked, False otherwise""" |
---|
256 | try: |
---|
257 | map_file = open(filename,'rt') |
---|
258 | except(IOError): |
---|
259 | sys.stderr.write("Error: No map given!\n") |
---|
260 | return(False) |
---|
261 | # now open and read the XML file |
---|
262 | parser = make_parser() |
---|
263 | cur_handler = LocalXMLParser() |
---|
264 | parser.setContentHandler(cur_handler) |
---|
265 | parser.parse(map_file) |
---|
266 | map_file.close() |
---|
267 | # make a copy of the layer data |
---|
268 | self.layers = cur_handler.layers |
---|
269 | return True |
---|
270 | |
---|
271 | def getSize(self): |
---|
272 | """getSize stores the size of the grid""" |
---|
273 | for t in self.layers[0].tiles: |
---|
274 | if t.x > self.max_x: |
---|
275 | self.max_x = t.x |
---|
276 | if t.x < self.min_x: |
---|
277 | self.min_x = t.x |
---|
278 | if t.y > self.max_y: |
---|
279 | self.max_y = t.y |
---|
280 | if t.y < self.min_y: |
---|
281 | self.min_y = t.y |
---|
282 | |
---|
283 | def checkRange(self, x, y): |
---|
284 | """Grid co-ords in range?""" |
---|
285 | if((x < self.min_x) or (x > self.max_x) or |
---|
286 | (y < self.min_y) or (y > self.max_y)): |
---|
287 | return False |
---|
288 | return True |
---|
289 | |
---|
290 | def pMatchSearch(self, x, y, search): |
---|
291 | """Brute force method used for matching grid""" |
---|
292 | # is the tile even in range? |
---|
293 | if(self.checkRange(x, y) == False): |
---|
294 | return False |
---|
295 | size = len(search) |
---|
296 | for t in self.layers[0].tiles: |
---|
297 | if((t.x == x) and (t.y == y) and (t.object[:size] == search)): |
---|
298 | return(True) |
---|
299 | # no match |
---|
300 | return False |
---|
301 | |
---|
302 | def coordsMatch(self, x, y, tiles): |
---|
303 | """Helper routine to check wether the list of tiles |
---|
304 | in tiles has any contain the coords x,y""" |
---|
305 | for t in tiles: |
---|
306 | if((t.x == x) and (t.y == y)): |
---|
307 | return True |
---|
308 | # obviously no match |
---|
309 | return False |
---|
310 | |
---|
311 | def saveMap(self, filename): |
---|
312 | """Save the new map""" |
---|
313 | # open the new files for writing |
---|
314 | try: |
---|
315 | map_file = open(filename, 'wt') |
---|
316 | except(IOError): |
---|
317 | sys.stderr.write("Error: Couldn't save map\n") |
---|
318 | return(False) |
---|
319 | # we don't know how many layers we need, let's do that now |
---|
320 | # this is a brute force solution but it does work, and speed |
---|
321 | # is not required in this utility |
---|
322 | layer_count = 0 |
---|
323 | while(self.ttiles != []): |
---|
324 | recycled_tiles = [] |
---|
325 | self.render_tiles = [] |
---|
326 | for t in self.ttiles: |
---|
327 | if(self.coordsMatch(t.x, t.y, self.render_tiles) == False): |
---|
328 | # no matching tile in the grid so far, so add it |
---|
329 | self.render_tiles.append(t) |
---|
330 | else: |
---|
331 | # we must save this for another layer |
---|
332 | recycled_tiles.append(t) |
---|
333 | # render this layer |
---|
334 | if(self.outputTransLayer(map_file, layer_count) == False): |
---|
335 | return False |
---|
336 | layer_count += 1 |
---|
337 | self.ttiles = recycled_tiles |
---|
338 | # phew, that was it |
---|
339 | map_file.close() |
---|
340 | print "Output new file as new.xml" |
---|
341 | print "Had to render", layer_count, "layers" |
---|
342 | return True |
---|
343 | |
---|
344 | def printDetails(self): |
---|
345 | """Debugging routine to output some details about the map |
---|
346 | Used to check the map loaded ok""" |
---|
347 | # display each layer name, then the details |
---|
348 | print "Layer ID's:", |
---|
349 | for l in self.layers: |
---|
350 | print l.name, |
---|
351 | print "\nMap Dimensions: X=", (self.max_x-self.min_x) + 1, |
---|
352 | print " Y=", (self.max_y-self.min_y) + 1 |
---|
353 | |
---|
354 | if __name__=="__main__": |
---|
355 | # pass a map name as the first argument |
---|
356 | if(len(sys.argv) < 2): |
---|
357 | sys.stderr.write("Error: No map given!\n") |
---|
358 | sys.exit(False) |
---|
359 | |
---|
360 | new_map = LocalMap() |
---|
361 | if(new_map.loadFromXML(sys.argv[1]) == True): |
---|
362 | new_map.getSize() |
---|
363 | new_map.buildTransLayer("grass") |
---|
364 | new_map.saveMap("new.xml") |
---|
365 | new_map.printDetails() |
---|
366 | |
---|