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