package micropolisj.build_tool; import micropolisj.engine.TileSpec; import micropolisj.graphics.*; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.nio.charset.Charset; import java.util.*; import javax.imageio.*; import javax.swing.ImageIcon; import javax.xml.stream.*; import static micropolisj.engine.TileSpec.generateTileNames; import static micropolisj.graphics.TileImage.*; import static micropolisj.XML_Helper.*; public class MakeTiles { static HashMap tileData = new HashMap(); static final Charset UTF8 = Charset.forName("UTF-8"); static int SKIP_TILES = 0; static int COUNT_TILES = -1; static int TILE_SIZE = STD_SIZE; public static void main(String [] args) throws Exception { if (args.length != 2) { throw new Exception("Wrong number of arguments"); } if (System.getProperty("tile_size") != null) { TILE_SIZE = Integer.parseInt(System.getProperty("tile_size")); } if (System.getProperty("skip_tiles") != null) { SKIP_TILES = Integer.parseInt(System.getProperty("skip_tiles")); } if (System.getProperty("tile_count") != null) { COUNT_TILES = Integer.parseInt(System.getProperty("tile_count")); } File recipeFile = new File(args[0]); File outputDir = new File(args[1]); generateFromRecipe(recipeFile, outputDir); } static class TileMapping { String tileName; TileImage ref; TileImage dest; TileMapping(String tileName, TileImage ref, TileImage dest) { this.tileName = tileName; this.ref = ref; this.dest = dest; } } static class ComposeFrame extends TileImageSprite { TileImage refImage; ComposeFrame(ComposeBuffer parentBuffer, TileImage refImage) { super(parentBuffer); this.refImage = refImage; } } static class ComposeBuffer extends TileImage { File outFile; String fileName; boolean useAlpha; int maxWidth; int nextOffsetY; BufferedImage buf; Graphics2D gr; ComposeBuffer(File outputDir, String fileName, boolean useAlpha) { this.outFile = new File(outputDir, fileName); this.fileName = fileName; this.useAlpha = useAlpha; } TileImageSprite prepareTile(Dimension size, TileImage refImage) { TileImageSprite s = new ComposeFrame(this, refImage); s.offsetY = this.nextOffsetY + size.height - TILE_SIZE; this.nextOffsetY += size.height; this.maxWidth = Math.max(maxWidth, size.width); return s; } void createBuffer() { this.buf = new BufferedImage(maxWidth,nextOffsetY, useAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); this.gr = buf.createGraphics(); } void writeFile() throws IOException { System.out.println("Generating tiles array: "+outFile); ImageIO.write(buf, "png", outFile); } @Override public void drawFragment(Graphics2D gr, int destX, int destY, int srcX, int srcY) { throw new UnsupportedOperationException(); } } static class Composer { ComposeBuffer stanTiles; Composer(File outputDir) { this.stanTiles = new ComposeBuffer(outputDir, "tiles.png", false); } TileImageSprite prepareTile(TileImage refImage) { Dimension size = new Dimension(TILE_SIZE, TILE_SIZE); return stanTiles.prepareTile(size, refImage); } void createBuffers() { stanTiles.createBuffer(); } Graphics2D getGr(TileImageSprite s) { ComposeBuffer cb = (ComposeBuffer) s.source; return cb.gr; } void writeFiles() throws IOException { stanTiles.writeFile(); } } static void generateFromRecipe(File recipeFile, File outputDir) throws IOException { Properties recipe = new Properties(); recipe.load( new InputStreamReader( new FileInputStream(recipeFile), UTF8 )); // count number of images String [] tileNames = generateTileNames(recipe); int ntiles = COUNT_TILES == -1 ? tileNames.length : COUNT_TILES; // prepare mapping data Composer c = new Composer(outputDir); ArrayList mappings = new ArrayList(); for (int i = 0; i < ntiles; i++) { int tileNumber = SKIP_TILES + i; if (!(tileNumber >= 0 && tileNumber < tileNames.length)) { continue; } String tileName = tileNames[tileNumber]; String rawSpec = recipe.getProperty(tileName); assert rawSpec != null; TileSpec tileSpec = TileSpec.parse(tileNumber, tileName, rawSpec, recipe); TileImage ref = parseFrameSpec(tileSpec); if (ref == null) { // tile is defined, but it has no images continue; } TileImage dest = prepareFrames(ref, c); TileMapping m = new TileMapping(tileName, ref, dest); mappings.add(m); } // actually assemble the image c.createBuffers(); for (TileMapping m : mappings) { drawFrames(m.dest, c); } // make parent directories if necessary outputDir.mkdirs(); // output the composed images c.writeFiles(); // output an index of all tile names and their offset into // the composed tile array File indexFile = new File(outputDir, "tiles.idx"); System.out.println("Generating tiles index: "+indexFile); writeIndexFile(mappings, indexFile); } static TileImage prepareFrames(TileImage ref, Composer c) { if (ref instanceof Animation) { Animation mc = (Animation) ref; Animation dest = new Animation(); for (Animation.Frame f : mc.frames) { TileImage s = prepareFrames(f.frame, c); dest.addFrame(s, f.duration); } return dest; } else { TileImageSprite s = c.prepareTile(ref); return s; } } static void drawFrames(TileImage dest, Composer c) { if (dest instanceof Animation) { Animation ani = (Animation) dest; for (Animation.Frame f : ani.frames) { drawFrames(f.frame, c); } } else { assert dest instanceof ComposeFrame; ComposeFrame f = (ComposeFrame) dest; f.refImage.drawTo(c.getGr(f), f.offsetX, f.offsetY); } } static void writeIndexFile(Collection mappings, File indexFile) throws IOException { try { FileOutputStream outStream = new FileOutputStream(indexFile); XMLStreamWriter out = XMLOutputFactory.newInstance().createXMLStreamWriter(outStream, "UTF-8"); out.writeStartDocument(); out.writeStartElement("micropolis-tiles-index"); for (TileMapping m : mappings) { out.writeStartElement("tile"); out.writeAttribute("name", m.tileName); assert (m.dest instanceof Animation) || (m.dest instanceof TileImageSprite); if (m.dest instanceof Animation) { Animation ani = (Animation) m.dest; out.writeStartElement("animation"); for (Animation.Frame f : ani.frames) { TileImageSprite s = (TileImageSprite) f.frame; out.writeStartElement("frame"); out.writeAttribute("at", String.format("%d,%d", s.offsetX, s.offsetY)); out.writeEndElement(); } out.writeEndElement(); } else { //assume it is a simple sprite TileImageSprite s = (TileImageSprite ) m.dest; out.writeStartElement("image"); out.writeAttribute("at", String.format("%d,%d", s.offsetX, s.offsetY)); out.writeEndElement(); } out.writeEndElement(); } out.writeEndElement(); out.writeEndDocument(); out.close(); outStream.close(); //because XMLStreamWriter does not call it for us } catch (XMLStreamException e) { throw new IOException(e); } } static TileImage parseFrameSpec(TileSpec spec) throws IOException { return parseFrameSpec(spec.getImages()); } static TileImage parseFrameSpec(String rawSpec) throws IOException { String [] parts = rawSpec.split("\\|"); for (int i = 0; i < parts.length; i++) { parts[i] = parts[i].trim(); } return parseFrameSpec(parts); } static TileImage parseFrameSpec(String [] layerStrings) throws IOException { TileImage result = null; for (String layerStr : layerStrings) { TileImage newLayer = parseLayerSpec(layerStr); if (result == null) { result = newLayer; } else { result = new TileImageLayer( result, newLayer); } } return result; } static TileImage parseLayerSpec(String layerStr) throws IOException { String [] parts = layerStr.split("@", 2); TileImage img = loadAnimation(parts[0]); if (parts.length >= 2) { TileImageSprite sprite = new TileImageSprite(img); String offsetInfo = parts[1]; parts = offsetInfo.split(","); if (parts.length >= 1) { sprite.offsetX = Integer.parseInt(parts[0]); } if (parts.length >= 2) { sprite.offsetY = Integer.parseInt(parts[1]); } return sprite; }//endif something given after '@' in image specifier return img; } static File findInkscape() { String exeName = "inkscape"; if (System.getProperty("os.name").startsWith("Windows")) { exeName += ".exe"; } File [] pathsToTry = { new File("/usr/bin"), new File("c:\\Program Files\\Inkscape"), new File("c:\\Program Files (x86)\\Inkscape") }; for (File p : pathsToTry) { File f = new File(p, exeName); if (f.exists()) { return f; } } throw new Error("INKSCAPE not installed (or not found)"); } static File stagingDir = new File("generated"); static File renderSvg(String fileName, File svgFile) throws IOException { File pngFile = new File(stagingDir, fileName+"_"+TILE_SIZE+"x"+TILE_SIZE+".png"); if (pngFile.exists() && pngFile.lastModified() > svgFile.lastModified()) { // looks like the PNG file is already up-to-date return pngFile; } File inkscapeBin = findInkscape(); System.out.println("Generating raster image: "+pngFile); if (pngFile.exists()) { pngFile.delete(); } else { pngFile.getParentFile().mkdirs(); } String [] cmdline = { inkscapeBin.toString(), "--export-dpi="+(TILE_SIZE*90.0/STD_SIZE), "--export-png="+pngFile.toString(), svgFile.toString() }; Process p = Runtime.getRuntime().exec(cmdline); int exit_value; try { exit_value = p.waitFor(); } catch (InterruptedException e) { throw new RuntimeException(e); } if (exit_value != 0) { throw new RuntimeException("Helper exit status: "+exit_value); } if (!pngFile.exists()) { throw new RuntimeException("File not found: "+pngFile); } return pngFile; } static TileImage loadAnimation(String fileName) throws IOException { File f = new File(fileName + ".ani"); if (f.exists()) { return Animation.load(f, loaderContext); } else { return loadImage(fileName); } } static class MyLoaderContext implements LoaderContext { HashMap loadedImages = new HashMap(); //implements LoaderContext public SourceImage getDefaultImage() { throw new UnsupportedOperationException(); } //implements LoaderContext public SourceImage getImage(String fileName) throws IOException { if (!loadedImages.containsKey(fileName)) { loadedImages.put(fileName, loadImageNoCache(fileName)); } return loadedImages.get(fileName); } public TileImage parseFrameSpec(String tmp) throws IOException { return MakeTiles.parseFrameSpec(tmp); } } static MyLoaderContext loaderContext = new MyLoaderContext(); static TileImage loadImage(String fileName) throws IOException { File xmlFile = new File(fileName + ".xml"); if (xmlFile.exists()) { return loadImageXml(xmlFile); } return loaderContext.getImage(fileName); } static SourceImage loadImageReal(File pngFile, int basisSize) throws IOException { BufferedImage bi = ImageIO.read(pngFile); return new ScaledSourceImage( bi, basisSize, TILE_SIZE); } static SourceImage loadImageNoCache(String fileName) throws IOException { File svgFile, pngFile = null; svgFile = new File(fileName+"_"+TILE_SIZE+"x"+TILE_SIZE+".svg"); if (svgFile.exists()) { pngFile = renderSvg(fileName, svgFile); } else { svgFile = new File(fileName+".svg"); if (svgFile.exists()) { pngFile = renderSvg(fileName, svgFile); } } if (pngFile != null && pngFile.exists()) { return loadImageReal(pngFile, TILE_SIZE); } pngFile = new File(fileName+"_"+TILE_SIZE+"x"+TILE_SIZE+".png"); if (pngFile.exists()) { return loadImageReal(pngFile, TILE_SIZE); } if (TILE_SIZE < 128) { pngFile = new File(fileName+"_128x128.png"); if (pngFile.exists()) { return loadImageReal(pngFile, 128); } } pngFile = new File(fileName+".png"); if (pngFile.exists()) { return loadImageReal(pngFile, STD_SIZE); } throw new IOException("File not found: "+fileName+".{svg,png}"); } static TileImage loadImageXml(File xmlFile) throws IOException { FileInputStream inStream = new FileInputStream(xmlFile); try { XMLStreamReader in = XMLInputFactory.newInstance().createXMLStreamReader(inStream, "UTF-8"); in.nextTag(); // get document tag return readTileImage(in, loaderContext); } catch (XMLStreamException e) { throw new IOException("XML Parse error", e); } } }