1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 5 * License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" 10 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language 11 * governing permissions and limitations under the License. 12 ******************************************************************************/ 13 14 package com.badlogic.gdx.tiledmappacker; 15 16 import java.awt.Graphics; 17 import java.awt.image.BufferedImage; 18 import java.io.File; 19 import java.io.FilenameFilter; 20 import java.io.IOException; 21 import java.util.HashMap; 22 import java.util.Iterator; 23 24 import javax.xml.parsers.DocumentBuilder; 25 import javax.xml.parsers.DocumentBuilderFactory; 26 import javax.xml.parsers.ParserConfigurationException; 27 import javax.xml.transform.Transformer; 28 import javax.xml.transform.TransformerConfigurationException; 29 import javax.xml.transform.TransformerException; 30 import javax.xml.transform.TransformerFactory; 31 import javax.xml.transform.dom.DOMSource; 32 import javax.xml.transform.stream.StreamResult; 33 34 import org.w3c.dom.Attr; 35 import org.w3c.dom.Document; 36 import org.w3c.dom.NamedNodeMap; 37 import org.w3c.dom.Node; 38 import org.w3c.dom.NodeList; 39 import org.xml.sax.SAXException; 40 41 import com.badlogic.gdx.ApplicationListener; 42 import com.badlogic.gdx.Gdx; 43 import com.badlogic.gdx.assets.loaders.resolvers.AbsoluteFileHandleResolver; 44 import com.badlogic.gdx.backends.lwjgl.LwjglApplication; 45 import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; 46 import com.badlogic.gdx.files.FileHandle; 47 import com.badlogic.gdx.graphics.g2d.TextureAtlas; 48 import com.badlogic.gdx.maps.MapLayer; 49 import com.badlogic.gdx.maps.tiled.TiledMap; 50 import com.badlogic.gdx.maps.tiled.TiledMapTile; 51 import com.badlogic.gdx.maps.tiled.TiledMapTileLayer; 52 import com.badlogic.gdx.maps.tiled.TiledMapTileSet; 53 import com.badlogic.gdx.maps.tiled.TmxMapLoader; 54 import com.badlogic.gdx.maps.tiled.tiles.AnimatedTiledMapTile; 55 import com.badlogic.gdx.maps.tiled.tiles.StaticTiledMapTile; 56 import com.badlogic.gdx.math.Vector2; 57 import com.badlogic.gdx.tools.texturepacker.TexturePacker; 58 import com.badlogic.gdx.tools.texturepacker.TexturePacker.Settings; 59 import com.badlogic.gdx.utils.GdxRuntimeException; 60 import com.badlogic.gdx.utils.IntArray; 61 import com.badlogic.gdx.utils.ObjectMap; 62 63 /** Given one or more TMX tilemaps, packs all tileset resources used across the maps, or the resources used per map, into a single, 64 * or multiple (one per map), {@link TextureAtlas} and produces a new TMX file to be loaded with an AtlasTiledMapLoader loader. 65 * Optionally, it can keep track of unused tiles and omit them from the generated atlas, reducing the resource size. 66 * 67 * The original TMX map file will be parsed by using the {@link TmxMapLoader} loader, thus access to a valid OpenGL context is 68 * <b>required</b>, that's why an LwjglApplication is created by this preprocessor. 69 * 70 * The new TMX map file will contains a new property, namely "atlas", whose value will enable the AtlasTiledMapLoader to correctly 71 * read the associated TextureAtlas representing the tileset. 72 * 73 * @author David Fraska and others (initial implementation, tell me who you are!) 74 * @author Manuel Bua */ 75 public class TiledMapPacker { 76 private TexturePacker packer; 77 private TiledMap map; 78 79 private TmxMapLoader mapLoader = new TmxMapLoader(new AbsoluteFileHandleResolver()); 80 private TiledMapPackerSettings settings; 81 82 private static final String TilesetsOutputDir = "tileset"; 83 static String AtlasOutputName = "packed"; 84 85 private HashMap<String, IntArray> tilesetUsedIds = new HashMap<String, IntArray>(); 86 private ObjectMap<String, TiledMapTileSet> tilesetsToPack; 87 88 static File inputDir; 89 static File outputDir; 90 private FileHandle currentDir; 91 92 private static class TmxFilter implements FilenameFilter { TmxFilter()93 public TmxFilter () { 94 } 95 96 @Override accept(File dir, String name)97 public boolean accept (File dir, String name) { 98 return (name.endsWith(".tmx")); 99 } 100 } 101 102 private static class DirFilter implements FilenameFilter { DirFilter()103 public DirFilter () { 104 } 105 106 @Override accept(File f, String s)107 public boolean accept (File f, String s) { 108 return (new File(f, s).isDirectory()); 109 } 110 } 111 112 /** Constructs a new preprocessor by using the default packing settings */ TiledMapPacker()113 public TiledMapPacker () { 114 this(new TiledMapPackerSettings()); 115 } 116 117 /** Constructs a new preprocessor by using the specified packing settings */ TiledMapPacker(TiledMapPackerSettings settings)118 public TiledMapPacker (TiledMapPackerSettings settings) { 119 this.settings = settings; 120 } 121 122 /** You can either run the {@link TiledMapPacker#main(String[])} method or reference this class in your own project and call 123 * this method. If working with libGDX sources, you can also run this file to create a run configuration then export it as a 124 * Runnable Jar. To run from a nightly build: 125 * 126 * <code> <br><br> 127 * Linux / OS X <br> 128 java -cp gdx.jar:gdx-natives.jar:gdx-backend-lwjgl.jar:gdx-backend-lwjgl-natives.jar:gdx-tiled-preprocessor.jar:extensions/gdx-tools/gdx-tools.jar 129 com.badlogic.gdx.tiledmappacker.TiledMapPacker inputDir [outputDir] [--strip-unused] [--combine-tilesets] [-v] 130 * <br><br> 131 * 132 * Windows <br> 133 java -cp gdx.jar;gdx-natives.jar;gdx-backend-lwjgl.jar;gdx-backend-lwjgl-natives.jar;gdx-tiled-preprocessor.jar;extensions/gdx-tools/gdx-tools.jar 134 com.badlogic.gdx.tiledmappacker.TiledMapPacker inputDir [outputDir] [--strip-unused] [--combine-tilesets] [-v] 135 * <br><br> </code> 136 * 137 * Keep in mind that this preprocessor will need to load the maps by using the {@link TmxMapLoader} loader and this in turn 138 * will need a valid OpenGL context to work. 139 * 140 * Process a directory containing TMX map files representing Tiled maps and produce multiple, or a single, TextureAtlas as well 141 * as new processed TMX map files, correctly referencing the generated {@link TextureAtlas} by using the "atlas" custom map 142 * property. */ processInputDir(Settings texturePackerSettings)143 public void processInputDir (Settings texturePackerSettings) throws IOException { 144 FileHandle inputDirHandle = new FileHandle(inputDir.getCanonicalPath()); 145 File[] mapFilesInCurrentDir = inputDir.listFiles(new TmxFilter()); 146 tilesetsToPack = new ObjectMap<String, TiledMapTileSet>(); 147 148 // Processes the maps inside inputDir 149 for (File mapFile : mapFilesInCurrentDir) { 150 processSingleMap(mapFile, inputDirHandle, texturePackerSettings); 151 } 152 153 processSubdirectories(inputDirHandle, texturePackerSettings); 154 155 boolean combineTilesets = this.settings.combineTilesets; 156 if (combineTilesets == true) { 157 packTilesets(inputDirHandle, texturePackerSettings); 158 } 159 } 160 161 /** Looks for subdirectories inside parentHandle, processes maps in subdirectory, repeat. 162 * @param currentDir The directory to look for maps and other directories 163 * @throws IOException */ processSubdirectories(FileHandle currentDir, Settings texturePackerSettings)164 private void processSubdirectories (FileHandle currentDir, Settings texturePackerSettings) throws IOException { 165 File parentPath = new File(currentDir.path()); 166 File[] directories = parentPath.listFiles(new DirFilter()); 167 168 for (File directory : directories) { 169 currentDir = new FileHandle(directory.getCanonicalPath()); 170 File[] mapFilesInCurrentDir = directory.listFiles(new TmxFilter()); 171 172 for (File mapFile : mapFilesInCurrentDir) { 173 processSingleMap(mapFile, currentDir, texturePackerSettings); 174 } 175 176 processSubdirectories(currentDir, texturePackerSettings); 177 } 178 } 179 processSingleMap(File mapFile, FileHandle dirHandle, Settings texturePackerSettings)180 private void processSingleMap (File mapFile, FileHandle dirHandle, Settings texturePackerSettings) throws IOException { 181 boolean combineTilesets = this.settings.combineTilesets; 182 if (combineTilesets == false) { 183 tilesetUsedIds = new HashMap<String, IntArray>(); 184 tilesetsToPack = new ObjectMap<String, TiledMapTileSet>(); 185 } 186 187 map = mapLoader.load(mapFile.getCanonicalPath()); 188 189 // if enabled, build a list of used tileids for the tileset used by this map 190 boolean stripUnusedTiles = this.settings.stripUnusedTiles; 191 if (stripUnusedTiles) { 192 stripUnusedTiles(); 193 } else { 194 for (TiledMapTileSet tileset : map.getTileSets()) { 195 String tilesetName = tileset.getName(); 196 if (!tilesetsToPack.containsKey(tilesetName)) { 197 tilesetsToPack.put(tilesetName, tileset); 198 } 199 } 200 } 201 202 if (combineTilesets == false) { 203 FileHandle tmpHandle = new FileHandle(mapFile.getName()); 204 this.settings.atlasOutputName = tmpHandle.nameWithoutExtension(); 205 206 packTilesets(dirHandle, texturePackerSettings); 207 } 208 209 FileHandle tmxFile = new FileHandle(mapFile.getCanonicalPath()); 210 writeUpdatedTMX(map, tmxFile); 211 } 212 stripUnusedTiles()213 private void stripUnusedTiles () { 214 int mapWidth = map.getProperties().get("width", Integer.class); 215 int mapHeight = map.getProperties().get("height", Integer.class); 216 int numlayers = map.getLayers().getCount(); 217 int bucketSize = mapWidth * mapHeight * numlayers; 218 219 Iterator<MapLayer> it = map.getLayers().iterator(); 220 while (it.hasNext()) { 221 MapLayer layer = it.next(); 222 223 // some layers can be plain MapLayer instances (ie. object groups), just ignore them 224 if (layer instanceof TiledMapTileLayer) { 225 TiledMapTileLayer tlayer = (TiledMapTileLayer)layer; 226 227 for (int y = 0; y < mapHeight; ++y) { 228 for (int x = 0; x < mapWidth; ++x) { 229 if (tlayer.getCell(x, y) != null) { 230 TiledMapTile tile = tlayer.getCell(x, y).getTile(); 231 if (tile instanceof AnimatedTiledMapTile) { 232 AnimatedTiledMapTile aTile = (AnimatedTiledMapTile)tile; 233 for (StaticTiledMapTile t : aTile.getFrameTiles()) { 234 addTile(t, bucketSize); 235 } 236 } 237 // Adds non-animated tiles and the base animated tile 238 addTile(tile, bucketSize); 239 } 240 } 241 } 242 } 243 } 244 } 245 addTile(TiledMapTile tile, int bucketSize)246 private void addTile (TiledMapTile tile, int bucketSize) { 247 int tileid = tile.getId() & ~0xE0000000; 248 String tilesetName = tilesetNameFromTileId(map, tileid); 249 IntArray usedIds = getUsedIdsBucket(tilesetName, bucketSize); 250 usedIds.add(tileid); 251 252 // track this tileset to be packed if not already tracked 253 if (!tilesetsToPack.containsKey(tilesetName)) { 254 tilesetsToPack.put(tilesetName, map.getTileSets().getTileSet(tilesetName)); 255 } 256 } 257 tilesetNameFromTileId(TiledMap map, int tileid)258 private String tilesetNameFromTileId (TiledMap map, int tileid) { 259 String name = ""; 260 if (tileid == 0) { 261 return ""; 262 } 263 264 for (TiledMapTileSet tileset : map.getTileSets()) { 265 int firstgid = tileset.getProperties().get("firstgid", -1, Integer.class); 266 if (firstgid == -1) continue; // skip this tileset 267 if (tileid >= firstgid) { 268 name = tileset.getName(); 269 } else { 270 return name; 271 } 272 } 273 274 return name; 275 } 276 277 /** Returns the usedIds bucket for the given tileset name. If it doesn't exist one will be created with the specified size if 278 * its > 0, else null will be returned. 279 * 280 * @param size The size to use to create a new bucket if it doesn't exist, else specify 0 or lower to return null instead 281 * @return a bucket */ getUsedIdsBucket(String tilesetName, int size)282 private IntArray getUsedIdsBucket (String tilesetName, int size) { 283 if (tilesetUsedIds.containsKey(tilesetName)) { 284 return tilesetUsedIds.get(tilesetName); 285 } 286 287 if (size <= 0) { 288 return null; 289 } 290 291 IntArray bucket = new IntArray(size); 292 tilesetUsedIds.put(tilesetName, bucket); 293 return bucket; 294 } 295 296 /** Traverse the specified tilesets, optionally lookup the used ids and pass every tile image to the {@link TexturePacker}, 297 * optionally ignoring unused tile ids */ packTilesets(FileHandle inputDirHandle, Settings texturePackerSettings)298 private void packTilesets (FileHandle inputDirHandle, Settings texturePackerSettings) throws IOException { 299 BufferedImage tile; 300 Vector2 tileLocation; 301 Graphics g; 302 303 packer = new TexturePacker(texturePackerSettings); 304 305 for (TiledMapTileSet set : tilesetsToPack.values()) { 306 String tilesetName = set.getName(); 307 System.out.println("Processing tileset " + tilesetName); 308 309 IntArray usedIds = this.settings.stripUnusedTiles ? getUsedIdsBucket(tilesetName, -1) : null; 310 311 int tileWidth = set.getProperties().get("tilewidth", Integer.class); 312 int tileHeight = set.getProperties().get("tileheight", Integer.class); 313 int firstgid = set.getProperties().get("firstgid", Integer.class); 314 String imageName = set.getProperties().get("imagesource", String.class); 315 316 TileSetLayout layout = new TileSetLayout(firstgid, set, inputDirHandle); 317 318 for (int gid = layout.firstgid, i = 0; i < layout.numTiles; gid++, i++) { 319 boolean verbose = this.settings.verbose; 320 321 if (usedIds != null && !usedIds.contains(gid)) { 322 if (verbose) { 323 System.out.println("Stripped id #" + gid + " from tileset \"" + tilesetName + "\""); 324 } 325 continue; 326 } 327 328 tileLocation = layout.getLocation(gid); 329 tile = new BufferedImage(tileWidth, tileHeight, BufferedImage.TYPE_4BYTE_ABGR); 330 331 g = tile.createGraphics(); 332 g.drawImage(layout.image, 0, 0, tileWidth, tileHeight, (int)tileLocation.x, (int)tileLocation.y, (int)tileLocation.x 333 + tileWidth, (int)tileLocation.y + tileHeight, null); 334 335 if (verbose) { 336 System.out.println("Adding " + tileWidth + "x" + tileHeight + " (" + (int)tileLocation.x + ", " 337 + (int)tileLocation.y + ")"); 338 } 339 // AtlasTmxMapLoader expects every tileset's index to begin at zero for the first tile in every tileset. 340 // so the region's adjusted gid is (gid - layout.firstgid). firstgid will be added back in AtlasTmxMapLoader on load 341 int adjustedGid = gid - layout.firstgid; 342 final String separator = "_"; 343 String regionName = tilesetName + separator + adjustedGid; 344 345 packer.addImage(tile, regionName); 346 } 347 } 348 String tilesetOutputDir = outputDir.toString() + "/" + this.settings.tilesetOutputDirectory; 349 File relativeTilesetOutputDir = new File(tilesetOutputDir); 350 File outputDirTilesets = new File(relativeTilesetOutputDir.getCanonicalPath()); 351 352 outputDirTilesets.mkdirs(); 353 packer.pack(outputDirTilesets, this.settings.atlasOutputName + ".atlas"); 354 } 355 writeUpdatedTMX(TiledMap tiledMap, FileHandle tmxFileHandle)356 private void writeUpdatedTMX (TiledMap tiledMap, FileHandle tmxFileHandle) throws IOException { 357 Document doc; 358 DocumentBuilder docBuilder; 359 DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); 360 361 try { 362 docBuilder = docFactory.newDocumentBuilder(); 363 doc = docBuilder.parse(tmxFileHandle.read()); 364 365 Node map = doc.getFirstChild(); 366 while (map.getNodeType() != Node.ELEMENT_NODE || map.getNodeName() != "map") { 367 if ((map = map.getNextSibling()) == null) { 368 throw new GdxRuntimeException("Couldn't find map node!"); 369 } 370 } 371 372 setProperty(doc, map, "atlas", settings.tilesetOutputDirectory + "/" + settings.atlasOutputName + ".atlas"); 373 374 TransformerFactory transformerFactory = TransformerFactory.newInstance(); 375 Transformer transformer = transformerFactory.newTransformer(); 376 DOMSource source = new DOMSource(doc); 377 378 outputDir.mkdirs(); 379 StreamResult result = new StreamResult(new File(outputDir, tmxFileHandle.name())); 380 transformer.transform(source, result); 381 382 } catch (ParserConfigurationException e) { 383 throw new RuntimeException("ParserConfigurationException: " + e.getMessage()); 384 } catch (SAXException e) { 385 throw new RuntimeException("SAXException: " + e.getMessage()); 386 } catch (TransformerConfigurationException e) { 387 throw new RuntimeException("TransformerConfigurationException: " + e.getMessage()); 388 } catch (TransformerException e) { 389 throw new RuntimeException("TransformerException: " + e.getMessage()); 390 } 391 } 392 setProperty(Document doc, Node parent, String name, String value)393 private static void setProperty (Document doc, Node parent, String name, String value) { 394 Node properties = getFirstChildNodeByName(parent, "properties"); 395 Node property = getFirstChildByNameAttrValue(properties, "property", "name", name); 396 397 NamedNodeMap attributes = property.getAttributes(); 398 Node valueNode = attributes.getNamedItem("value"); 399 if (valueNode == null) { 400 valueNode = doc.createAttribute("value"); 401 valueNode.setNodeValue(value); 402 attributes.setNamedItem(valueNode); 403 } else { 404 valueNode.setNodeValue(value); 405 } 406 } 407 408 /** If the child node doesn't exist, it is created. */ getFirstChildNodeByName(Node parent, String child)409 private static Node getFirstChildNodeByName (Node parent, String child) { 410 NodeList childNodes = parent.getChildNodes(); 411 for (int i = 0; i < childNodes.getLength(); i++) { 412 if (childNodes.item(i).getNodeName().equals(child)) { 413 return childNodes.item(i); 414 } 415 } 416 417 Node newNode = parent.getOwnerDocument().createElement(child); 418 419 if (childNodes.item(0) != null) 420 return parent.insertBefore(newNode, childNodes.item(0)); 421 else 422 return parent.appendChild(newNode); 423 } 424 425 /** If the child node or attribute doesn't exist, it is created. Usage example: Node property = 426 * getFirstChildByAttrValue(properties, "property", "name"); */ getFirstChildByNameAttrValue(Node node, String childName, String attr, String value)427 private static Node getFirstChildByNameAttrValue (Node node, String childName, String attr, String value) { 428 NodeList childNodes = node.getChildNodes(); 429 for (int i = 0; i < childNodes.getLength(); i++) { 430 if (childNodes.item(i).getNodeName().equals(childName)) { 431 NamedNodeMap attributes = childNodes.item(i).getAttributes(); 432 Node attribute = attributes.getNamedItem(attr); 433 if (attribute.getNodeValue().equals(value)) return childNodes.item(i); 434 } 435 } 436 437 Node newNode = node.getOwnerDocument().createElement(childName); 438 NamedNodeMap attributes = newNode.getAttributes(); 439 440 Attr nodeAttr = node.getOwnerDocument().createAttribute(attr); 441 nodeAttr.setNodeValue(value); 442 attributes.setNamedItem(nodeAttr); 443 444 if (childNodes.item(0) != null) { 445 return node.insertBefore(newNode, childNodes.item(0)); 446 } else { 447 return node.appendChild(newNode); 448 } 449 } 450 451 /** Processes a directory of Tile Maps, compressing each tile set contained in any map once. 452 * 453 * @param args args[0]: the input directory containing the tmx files (and tile sets, relative to the path listed in the tmx 454 * file). args[1]: The output directory for the tmx files, should be empty before running. args[2-4] options */ main(String[] args)455 public static void main (String[] args) { 456 final Settings texturePackerSettings = new Settings(); 457 texturePackerSettings.paddingX = 2; 458 texturePackerSettings.paddingY = 2; 459 texturePackerSettings.edgePadding = true; 460 texturePackerSettings.duplicatePadding = true; 461 texturePackerSettings.bleed = true; 462 texturePackerSettings.alias = true; 463 texturePackerSettings.useIndexes = true; 464 465 final TiledMapPackerSettings packerSettings = new TiledMapPackerSettings(); 466 467 if (args.length == 0) { 468 printUsage(); 469 System.exit(0); 470 } else if (args.length == 1) { 471 inputDir = new File(args[0]); 472 outputDir = new File(inputDir, "../output/"); 473 } else if (args.length == 2) { 474 inputDir = new File(args[0]); 475 outputDir = new File(args[1]); 476 } else { 477 inputDir = new File(args[0]); 478 outputDir = new File(args[1]); 479 processExtraArgs(args, packerSettings); 480 } 481 482 TiledMapPacker packer = new TiledMapPacker(packerSettings); 483 LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); 484 config.forceExit = false; 485 config.width = 100; 486 config.height = 50; 487 config.title = "TiledMapPacker"; 488 new LwjglApplication(new ApplicationListener() { 489 490 @Override 491 public void resume () { 492 } 493 494 @Override 495 public void resize (int width, int height) { 496 } 497 498 @Override 499 public void render () { 500 } 501 502 @Override 503 public void pause () { 504 } 505 506 @Override 507 public void dispose () { 508 } 509 510 @Override 511 public void create () { 512 TiledMapPacker packer = new TiledMapPacker(packerSettings); 513 514 if (!inputDir.exists()) { 515 System.out.println(inputDir.getAbsolutePath()); 516 throw new RuntimeException("Input directory does not exist: " + inputDir); 517 } 518 519 try { 520 packer.processInputDir(texturePackerSettings); 521 } catch (IOException e) { 522 throw new RuntimeException("Error processing map: " + e.getMessage()); 523 } 524 System.out.println("Finished processing."); 525 Gdx.app.exit(); 526 } 527 }, config); 528 } 529 processExtraArgs(String[] args, TiledMapPackerSettings packerSettings)530 private static void processExtraArgs (String[] args, TiledMapPackerSettings packerSettings) { 531 String stripUnused = "--strip-unused"; 532 String combineTilesets = "--combine-tilesets"; 533 String verbose = "-v"; 534 535 int length = args.length - 2; 536 String[] argsNotDir = new String[length]; 537 System.arraycopy(args, 2, argsNotDir, 0, length); 538 539 for (String string : argsNotDir) { 540 if (stripUnused.equals(string)) { 541 packerSettings.stripUnusedTiles = true; 542 543 } else if (combineTilesets.equals(string)) { 544 packerSettings.combineTilesets = true; 545 546 } else if (verbose.equals(string)) { 547 packerSettings.verbose = true; 548 549 } else { 550 System.out.println("\nOption \"" + string + "\" not recognized.\n"); 551 printUsage(); 552 System.exit(0); 553 } 554 } 555 } 556 printUsage()557 private static void printUsage () { 558 System.out.println("Usage: INPUTDIR [OUTPUTDIR] [--strip-unused] [--combine-tilesets] [-v]"); 559 System.out.println("Processes a directory of Tiled .tmx maps. Unable to process maps with XML"); 560 System.out.println("tile layer format."); 561 System.out.println(" --strip-unused omits all tiles that are not used. Speeds up"); 562 System.out.println(" the processing. Smaller tilesets."); 563 System.out.println(" --combine-tilesets instead of creating a tileset for each map,"); 564 System.out.println(" this combines the tilesets into some kind"); 565 System.out.println(" of monster tileset. Has problems with tileset"); 566 System.out.println(" location. Has problems with nested folders."); 567 System.out.println(" Not recommended."); 568 System.out.println(" -v outputs which tiles are stripped and included"); 569 System.out.println(); 570 } 571 572 public static class TiledMapPackerSettings { 573 public boolean stripUnusedTiles = false; 574 public boolean combineTilesets = false; 575 public boolean verbose = false; 576 public String tilesetOutputDirectory = TilesetsOutputDir; 577 public String atlasOutputName = AtlasOutputName; 578 } 579 } 580