1 /* $Id$ */
2 /***************************************************************************
3  *                   (C) Copyright 2003-2010 - Stendhal                    *
4  ***************************************************************************
5  ***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or modify  *
8  *   it under the terms of the GNU General Public License as published by  *
9  *   the Free Software Foundation; either version 2 of the License, or     *
10  *   (at your option) any later version.                                   *
11  *                                                                         *
12  ***************************************************************************/
13 package games.stendhal.tools;
14 
15 import java.io.BufferedReader;
16 import java.io.File;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.Iterator;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24 
25 import tiled.core.Map;
26 import tiled.core.MapLayer;
27 import tiled.core.Tile;
28 import tiled.core.TileLayer;
29 import tiled.core.TileSet;
30 import tiled.io.TMXMapReader;
31 import tiled.io.TMXMapWriter;
32 import tiled.util.BasicTileCutter;
33 
34 /**
35  * A tool for converting tileset mappings.
36  *
37  * The new mapping is lines in format:
38  * <p>
39  * [oldtilesetpath]:[tilenumber]:[newtilesetpath]:[tilenumber]
40  * <p>
41  * These are read from the standard input.
42  * <p>
43  * Description of the process:
44  * <p>
45  * <ol>
46  * <li>Loads the map
47  * <li>Adds any new tilesets defined in the mapping if needed
48  * <li>Converts the old tileset mappings to new
49  * <li>Removes any unused tilesets from the map
50  * <li>Saves the map
51  * </ol>
52  */
53 public class TilesetConverter {
54 	private Mapping mapping = new Mapping();
55 	/**
56 	 * For quick lookup by tileset name
57 	 */
58 	private HashMap<String, TileSet> setByName = new HashMap<String, TileSet>();
59 
60 	/**
61 	 * Helper to make <code>namePattern</code> construction a bit more readable.
62 	 */
63 	private String sep =  Pattern.quote(File.separator);
64 	/**
65 	 * A pattern for picking the name of the tileset from the image name.
66 	 * The trailing "dir/image" without ".png"
67 	 */
68 	Pattern namePattern = Pattern.compile(".*" + sep + "([^" + sep + "]+"
69 			+ sep + "[^" + sep + "]+)\\.png$");
70 
71 	/**
72 	 * For returning the translated tile information.
73 	 */
74 	private static class TileInfo {
75 		public String file;
76 		public int index;
77 
TileInfo(String file, int index)78 		public TileInfo(String file, int index) {
79 			this.file = file;
80 			this.index = index;
81 		}
82 	}
83 
84 	/**
85 	 * A class for keeping the tile translation information
86 	 */
87 	private static class Mapping {
88 		private HashMap<String, HashMap<Integer, TileInfo>> mappings = new HashMap<String, HashMap<Integer, TileInfo>>();
89 		private HashSet<String> newTilesets = new HashSet<String>();
90 
91 		/**
92 		 * Add a new translation mapping.
93 		 *
94 		 * @param oldImg path to the old image file
95 		 * @param oldIndex index of the tile to be translated
96 		 * @param newImg path to the new image file
97 		 * @param newIndex index of the translated tile
98 		 */
addMapping(String oldImg, int oldIndex, String newImg, int newIndex)99 		public void addMapping(String oldImg, int oldIndex, String newImg, int newIndex) {
100 			newTilesets.add(newImg);
101 			HashMap<Integer, TileInfo> mapping = mappings.get(oldImg);
102 			if (mapping == null) {
103 				mapping = new HashMap<Integer, TileInfo>();
104 				mappings.put(oldImg, mapping);
105 			}
106 			mapping.put(oldIndex, new TileInfo(newImg, newIndex));
107 		}
108 
109 		/**
110 		 * Get a translated tile corresponding to an old tile.
111 		 *
112 		 * @param oldImg path to the old image file
113 		 * @param index index of the tile in the image
114 		 * @return new tile information, or <code>null</code>
115 		 * if the old tile should be kept
116 		 */
getTile(String oldImg, int index)117 		public TileInfo getTile(String oldImg, int index) {
118 			TileInfo result = null;
119 			HashMap<Integer, TileInfo> mapping = mappings.get(oldImg);
120 			if (mapping != null) {
121 				result = mapping.get(index);
122 			}
123 			return result;
124 		}
125 
126 		/**
127 		 * Get the new tilesets the translation adds to the map.
128 		 *
129 		 * @return an iterable set of image paths
130 		 */
getNewSets()131 		public Iterable<String> getNewSets() {
132 			return newTilesets;
133 		}
134 	}
135 
136 	/**
137 	 * Check whether a tileset is in use by a map.
138 	 *
139 	 * @param map the map to be checked
140 	 * @param tileset the tileset to be checked
141 	 * @return true iff the tileset is in use
142 	 */
isUsedTileset(final Map map, final TileSet tileset)143 	private boolean isUsedTileset(final Map map, final TileSet tileset) {
144 		for (final Iterator< ? > tiles = tileset.iterator(); tiles.hasNext();) {
145 			final Tile tile = (Tile) tiles.next();
146 
147 			for (final MapLayer layer : map) {
148 				if ((layer instanceof TileLayer) && (((TileLayer) layer).isUsed(tile))) {
149 					return true;
150 				}
151 			}
152 		}
153 
154 		return false;
155 	}
156 
157 	/**
158 	 * Remove any tilesets in a map that are not actually in use.
159 	 *
160 	 * @param map the map to be broomed
161 	 */
removeUnusedTilesets(final Map map)162 	private void removeUnusedTilesets(final Map map) {
163 		for (final Iterator< ? > sets = map.getTileSets().iterator(); sets.hasNext();) {
164 			final TileSet tileset = (TileSet) sets.next();
165 
166 			if (!isUsedTileset(map, tileset)) {
167 				sets.remove();
168 			}
169 		}
170 	}
171 
172 	/**
173 	 * Construct a nice name for a tileset based on the image name.
174 	 * The substring used for the name is specified in <code>namePattern</code>
175 	 *
176 	 * @param name image path
177 	 * @return a human readable tileset name
178 	 */
constructTilesetName(String name)179 	private String constructTilesetName(String name) {
180 		Matcher matcher = namePattern.matcher(name);
181 
182 		if (matcher.find()) {
183 			name = matcher.group(1);
184 		}
185 		return name;
186 	}
187 
188 	/**
189 	 * Add all the tilesets that the translation mapping uses to a map.
190 	 *
191 	 * @param map the map to add the tilesets to
192 	 * @throws IOException
193 	 */
addNewTilesets(Map map)194 	private void addNewTilesets(Map map) throws IOException {
195 		// First build up the mapping of old sets
196 		for (TileSet set : map.getTileSets()) {
197 			setByName.put(set.getTilebmpFile(), set);
198 		}
199 
200 		// then add all missing new sets
201 		for (String name : mapping.getNewSets()) {
202 			if (name.equals("")) {
203 				continue;
204 			}
205 
206 			if (!setByName.containsKey(name)) {
207 				// The tileset's not yet included. Add it to the map
208 				TileSet set = new TileSet();
209 				set.setName(constructTilesetName(name));
210 				BasicTileCutter cutter = new BasicTileCutter(32, 32, 0, 0);
211 				set.importTileBitmap(name, cutter);
212 
213 				setByName.put(name, set);
214 				map.addTileset(set);
215 			}
216 		}
217 	}
218 
219 	/**
220 	 * Find the translated tile that corresponds to a tile
221 	 * in the original tile mapping.
222 	 *
223 	 * @param tile The tile to be translated
224 	 * @return Translated tile
225 	 */
translateTile(Tile tile)226 	Tile translateTile(Tile tile) {
227 		int id = tile.getId();
228 		TileSet set = tile.getTileSet();
229 		TileInfo info = mapping.getTile(set.getTilebmpFile(), id);
230 		if (info != null) {
231 			TileSet newSet = setByName.get(info.file);
232 			tile = newSet.getTile(info.index);
233 		}
234 
235 		return tile;
236 	}
237 
238 	/**
239 	 * Translate all the tiles of a layer.
240 	 *
241 	 * @param layer the layer to be translated
242 	 */
translateLayer(MapLayer layer)243 	private void translateLayer(MapLayer layer) {
244 		if (!(layer instanceof TileLayer)) {
245 			return;
246 		}
247 		TileLayer tileLayer = (TileLayer) layer;
248 		for (int y = 0; y < tileLayer.getHeight(); y++) {
249 			for (int x = 0; x < tileLayer.getWidth(); x++) {
250 				Tile tile = tileLayer.getTileAt(x, y);
251 				if (tile != null) {
252 					tile = translateTile(tile);
253 					tileLayer.setTileAt(x, y, tile);
254 				}
255 			}
256 		}
257 	}
258 
259 	/**
260 	 * Translate all the layers of a map.
261 	 *
262 	 * @param map the map to be converted
263 	 */
translateMap(Map map)264 	private void translateMap(Map map) {
265 		for (MapLayer layer : map) {
266 			translateLayer(layer);
267 		}
268 	}
269 
270 	/**
271 	 * Converts a map file according to the tile mapping.
272 	 *
273 	 * @param tmxFile the map to be converted
274 	 * @throws Exception
275 	 */
convert(final String tmxFile)276 	private void convert(final String tmxFile) throws Exception {
277 		final File file = new File(tmxFile);
278 
279 		final String filename = file.getAbsolutePath();
280 		final Map map = new TMXMapReader().readMap(filename);
281 		addNewTilesets(map);
282 		translateMap(map);
283 		removeUnusedTilesets(map);
284 		new TMXMapWriter().writeMap(map, filename);
285 	}
286 
287 	/**
288 	 * Load tile mapping information from the standard input.
289 	 *
290 	 * @param path The path of the <b>map</b>. Needed for proper
291 	 * conversion of the tileset paths.
292 	 * @throws IOException
293 	 */
loadMapping(String path)294 	private void loadMapping(String path) throws IOException {
295 		BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
296 
297 		// needed for constructing the full path of the tilesets
298 		File f = new File(path);
299 		String dir = f.getParent();
300 
301 		String line;
302 		while ((line = input.readLine()) != null) {
303 			String[] elements = line.split(":", -1);
304 			if (elements.length != 4) {
305 				System.err.println("Invalid line: '" + line + "'");
306 			} else {
307 				int newIndex = 0;
308 				if (!"".equals(elements[3])) {
309 					newIndex = Integer.parseInt(elements[3]);
310 				}
311 
312 				/*
313 				 * Oh, yay. Tiled likes to translate the filenames internally
314 				 * to full paths.
315 				 * Great fun with java to compare the paths when the system
316 				 * allows no playing with directories whatsoever. We can't rely
317 				 * on the current directory being the same as that of the map.
318 				 * Building the full path from scratch, and hope for the best.
319 				 */
320 				String path1 = (new File(dir + File.separator + elements[0])).getCanonicalPath();
321 				String path2 = (new File(dir + File.separator + elements[2])).getCanonicalPath();
322 
323 				mapping.addMapping(path1, Integer.parseInt(elements[1]), path2, newIndex);
324 			}
325 		}
326 	}
327 
main(final String[] args)328 	public static void main(final String[] args) throws Exception {
329 		if (args.length < 1) {
330 			System.out.println("usage: java games.stendhal.tools.TilesetConverter <tmx file>");
331 			return;
332 		}
333 
334 		final TilesetConverter converter = new TilesetConverter();
335 		converter.loadMapping(args[0]);
336 
337 		converter.convert(args[0]);
338 	}
339 }
340