/** | |
* Copyright (c) 2010-2011, Vincent Vollers and Christopher J. Kucera | |
* All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without | |
* modification, are permitted provided that the following conditions are met: | |
* * Redistributions of source code must retain the above copyright | |
* notice, this list of conditions and the following disclaimer. | |
* * Redistributions in binary form must reproduce the above copyright | |
* notice, this list of conditions and the following disclaimer in the | |
* documentation and/or other materials provided with the distribution. | |
* * Neither the name of the Minecraft X-Ray team nor the | |
* names of its contributors may be used to endorse or promote products | |
* derived from this software without specific prior written permission. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
* DISCLAIMED. IN NO EVENT SHALL VINCENT VOLLERS OR CJ KUCERA BE LIABLE FOR ANY | |
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
*/ | |
package com.plusminus.craft; | |
import com.plusminus.craft.WorldInfo; | |
import java.awt.Color; | |
import java.awt.Rectangle; | |
import java.awt.Graphics2D; | |
import java.awt.AlphaComposite; | |
import java.awt.RenderingHints; | |
import java.awt.image.BufferedImage; | |
import java.io.File; | |
import java.io.FileReader; | |
import java.io.FileInputStream; | |
import java.io.DataInputStream; | |
import java.io.LineNumberReader; | |
import java.io.FileNotFoundException; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.zip.GZIPInputStream; | |
import java.util.zip.ZipFile; | |
import java.util.zip.ZipException; | |
import java.util.jar.JarFile; | |
import java.util.zip.ZipEntry; | |
import javax.imageio.ImageIO; | |
/*** | |
* Utility class which has convenience methods to access the | |
* files of the current minecraft installation | |
* @author Vincent Vollers | |
*/ | |
public class MineCraftEnvironment { | |
public static enum OS {XP, Vista, MacOS, Linux, NotSupported}; | |
public static OS os; | |
public static File baseDir; | |
public static File xrayBaseDir; | |
static { | |
String os = System.getProperty( "os.name" ); | |
HashMap<String,OS> osData = new HashMap<String,OS>(); | |
/* Full list of os.name strings | |
AIX | |
Digital Unix | |
FreeBSD | |
HP UX | |
Irix | |
Linux | |
Mac OS | |
Mac OS X | |
MPE/iX | |
Netware 4.11 | |
OS/2 | |
Solaris | |
Windows 2000 | |
Windows 7 | |
Windows 95 | |
Windows 98 | |
Windows NT | |
Windows Vista | |
Windows XP | |
*/ | |
osData.put("FreeBSD", OS.Linux); | |
osData.put("HP UX", OS.Linux); | |
osData.put("Linux", OS.Linux); | |
osData.put("Mac OS", OS.MacOS); | |
osData.put("Mac OS X", OS.MacOS); | |
osData.put("Windows", OS.XP); | |
osData.put("Windows 7", OS.Vista); | |
osData.put("Windows XP", OS.XP); | |
osData.put("Windows 2003", OS.XP); | |
osData.put("Windows 2000", OS.XP); | |
osData.put("Windows Vista", OS.Vista); | |
if(!osData.containsKey(os)) { | |
MineCraftEnvironment.os = OS.NotSupported; | |
} else { | |
MineCraftEnvironment.os = osData.get(os); | |
} | |
switch(MineCraftEnvironment.os) { | |
case Vista: | |
MineCraftEnvironment.baseDir = new File(System.getenv("APPDATA"), ".minecraft"); | |
MineCraftEnvironment.xrayBaseDir = new File(System.getenv("APPDATA"), ".minecraft_xray"); | |
break; | |
case Linux: | |
MineCraftEnvironment.baseDir = new File(System.getProperty("user.home"), ".minecraft"); | |
MineCraftEnvironment.xrayBaseDir = new File(System.getProperty("user.home"), ".minecraft_xray"); | |
break; | |
case XP: | |
MineCraftEnvironment.baseDir = new File(System.getenv("APPDATA"), ".minecraft"); // untested | |
MineCraftEnvironment.xrayBaseDir = new File(System.getenv("APPDATA"), ".minecraft_xray"); | |
break; | |
case MacOS: | |
// damn macs ;p | |
File dotMinecraftEnv = new File(System.getProperty("user.home"), "Library/Application Support/.minecraft"); | |
if(dotMinecraftEnv.exists()) { | |
MineCraftEnvironment.baseDir = dotMinecraftEnv; | |
MineCraftEnvironment.xrayBaseDir = new File(System.getProperty("user.home"), "Library/Application Support/.minecraft_xray"); | |
} else { | |
MineCraftEnvironment.baseDir = new File(System.getProperty("user.home"), "Library/Application Support/minecraft"); // untested | |
MineCraftEnvironment.xrayBaseDir = new File(System.getProperty("user.home"), "Library/Application Support/minecraft_xray"); // untested | |
} | |
break; | |
default: | |
MineCraftEnvironment.baseDir = null; | |
MineCraftEnvironment.xrayBaseDir = null; | |
} | |
System.out.println(MineCraftEnvironment.baseDir.getAbsolutePath()); | |
} | |
/*** | |
* Returns a list of WorldInfo objects, corresponding to available worlds | |
* @return | |
*/ | |
public static ArrayList<WorldInfo> getAvailableWorlds() { | |
ArrayList<WorldInfo> worlds = new ArrayList<WorldInfo>(); | |
for(int i=0;i<10;i++) { | |
File worldDir = getWorldDirectory(i); | |
if(worldDir.exists() && worldDir.canRead()) { | |
try | |
{ | |
// First snatch up the overworld | |
WorldInfo info = new WorldInfo(worldDir.getCanonicalPath(), i); | |
worlds.add(info); | |
// Now see if there's an associated Nether world we can add. | |
WorldInfo netherinfo = info.getNetherInfo(); | |
if (netherinfo != null) | |
{ | |
worlds.add(netherinfo); | |
} | |
} | |
catch (IOException e) | |
{ | |
// Nothing; guess we'll ignore it. | |
} | |
} | |
} | |
return worlds; | |
} | |
/*** | |
* Returns a file handle to a chunk file in a world | |
* @param world | |
* @param x | |
* @param z | |
* @return | |
*/ | |
public static DataInputStream getChunkInputStream(WorldInfo world, int x, int z) { | |
if (world.has_region_data) | |
{ | |
RegionFile rf = RegionFileCache.getRegionFile(new File(world.getBasePath()), x, z); | |
if (rf != null) | |
{ | |
DataInputStream chunk = rf.getChunkDataInputStream(x, z); | |
if (chunk != null) | |
{ | |
System.out.println("Read chunk (" + x + ", " + z + ") from Region file"); | |
return chunk; | |
} | |
} | |
} | |
if (!world.is_beta_1_3_level) | |
{ | |
int xx = x % 64; | |
if(xx<0) xx = 64+xx; | |
int zz = z % 64; | |
if(zz<0) zz = 64+zz; | |
String firstFolder = Integer.toString(xx, 36); | |
String secondFolder = Integer.toString(zz, 36); | |
String filename = "c." + Integer.toString(x, 36) + "." + Integer.toString(z, 36) + ".dat"; | |
File chunk = new File(world.getBasePath(), firstFolder + "/" + secondFolder + "/" + filename); | |
if (chunk.exists()) | |
{ | |
// There's some code duplication here from DTFReader.readDTFFile() | |
try { | |
return new DataInputStream(new GZIPInputStream(new FileInputStream(chunk))); | |
} catch (FileNotFoundException e) { | |
// TODO Auto-generated catch block | |
e.printStackTrace(); | |
} catch (IOException e) { | |
// TODO Auto-generated catch block | |
e.printStackTrace(); | |
} | |
} | |
} | |
return null; | |
} | |
/*** | |
* Returns a file handle to a base world directory | |
* @param world | |
* @return | |
*/ | |
public static File getWorldDirectory(int world) { | |
return new File(baseDir, "saves/World" + world); | |
} | |
/*** | |
* Returns a file handle to the base minecraft directory | |
* @return | |
*/ | |
public static File getMinecraftDirectory() { | |
return MineCraftEnvironment.baseDir; | |
} | |
/** | |
* Returns a file handle to our own data directory | |
* @return | |
*/ | |
public static File getXrayDirectory() { | |
return MineCraftEnvironment.xrayBaseDir; | |
} | |
/** | |
* Returns a file handle to our config file; will create the | |
* directory if needed. | |
* @return | |
*/ | |
public static File getXrayConfigFile() { | |
if (MineCraftEnvironment.xrayBaseDir.exists()) | |
{ | |
if (!MineCraftEnvironment.xrayBaseDir.isDirectory()) | |
{ | |
return null; | |
} | |
} | |
else | |
{ | |
if (!MineCraftEnvironment.xrayBaseDir.mkdir()) | |
{ | |
return null; | |
} | |
} | |
return new File(MineCraftEnvironment.xrayBaseDir, "xray.properties"); | |
} | |
/*** | |
* Returns a stream to an arbitrary file either from the main jar, or from the user-specified | |
* texture pack. | |
* | |
* @return | |
*/ | |
public static InputStream getMinecraftTexturepackData(String filename) { | |
// First check the options.txt file to see if we should be using the defined | |
// texture pack. | |
File optionsFile = new File(baseDir, "options.txt"); | |
String texturepack = null; | |
if (optionsFile.exists()) | |
{ | |
LineNumberReader reader = null; | |
try | |
{ | |
reader = new LineNumberReader(new FileReader(optionsFile)); | |
String line = null; | |
String[] parts; | |
while ((line = reader.readLine()) != null) | |
{ | |
parts = line.split(":", 2); | |
if (parts.length == 2) | |
{ | |
if (parts[0].equalsIgnoreCase("skin")) | |
{ | |
if (parts[1].equalsIgnoreCase("Default")) | |
{ | |
// Default skin, just break and | |
break; | |
} | |
else | |
{ | |
// Use the specified texture pack | |
texturepack = parts[1]; | |
break; | |
} | |
} | |
} | |
} | |
} | |
catch (FileNotFoundException e) | |
{ | |
// Just ignore it and load the default terrain.png | |
} | |
catch (IOException e) | |
{ | |
// Ditto, just ignore | |
} | |
if (reader != null) | |
{ | |
try | |
{ | |
reader.close(); | |
} | |
catch (IOException e) | |
{ | |
// do nothing | |
} | |
} | |
} | |
// Attempt to load in the texture pack | |
if (texturepack != null) | |
{ | |
File packFile = new File(baseDir, "texturepacks/" + texturepack); | |
if (packFile.exists()) | |
{ | |
ZipFile zf = null; | |
try | |
{ | |
zf = new ZipFile(packFile); | |
ZipEntry entry = zf.getEntry(filename); | |
if (entry != null) | |
{ | |
return zf.getInputStream(entry); | |
} | |
} | |
catch (ZipException e) | |
{ | |
// Do nothing. | |
} | |
catch (IOException e) | |
{ | |
// Do nothing | |
} | |
if (zf != null) | |
{ | |
try | |
{ | |
zf.close(); | |
} | |
catch (IOException e) | |
{ | |
// do nothing | |
} | |
} | |
} | |
} | |
// If we got here, just do what we've always done. | |
return getMinecraftFile(filename); | |
} | |
/*** | |
* Returns a stream to the texture data (overrides in the directory are handled) | |
* @return | |
*/ | |
public static InputStream getMinecraftTextureData() { | |
return getMinecraftTexturepackData("terrain.png"); | |
} | |
/*** | |
* Returns a stream to the water texture data | |
* @return | |
*/ | |
public static InputStream getMinecraftWaterData() { | |
return getMinecraftTexturepackData("misc/water.png"); | |
} | |
/*** | |
* Returns a stream to the water texture data | |
* @return | |
*/ | |
public static InputStream getMinecraftParticleData() { | |
return getMinecraftTexturepackData("particles.png"); | |
} | |
/** | |
* Returns a stream to the painting data | |
* @return | |
*/ | |
public static InputStream getMinecraftPaintingData() { | |
return getMinecraftTexturepackData("art/kz.png"); | |
} | |
/*** | |
* Returns a stream to the font data (overrides in the directory are handled) | |
* @return | |
*/ | |
public static InputStream getMinecraftFontData() { | |
return getMinecraftFile("default.png"); | |
} | |
/*** | |
* Attempts to create a bufferedImage for a stream | |
* @param i | |
* @return | |
*/ | |
private static BufferedImage buildImageFromInput(InputStream i) { | |
try { | |
return ImageIO.read(i); | |
} catch (IOException e) { | |
// TODO Auto-generated catch block | |
e.printStackTrace(); | |
return null; | |
} | |
} | |
/** | |
* Attempts to create a bufferedImage containing our painting sheet | |
* @return | |
*/ | |
public static BufferedImage getMinecraftPaintings() { | |
return buildImageFromInput(getMinecraftPaintingData()); | |
} | |
/*** | |
* Attempts to create a bufferedImage containing the texture sprite sheet. This does | |
* some munging to make things a little more useful for us. Namely: | |
* | |
* 1) It will attempt to colorize any biome-ready skin by first checking for green | |
* pixels in the texture, and then doing some blending if it looks greyscale. | |
* 2) We also copy in the water texture from misc/water.png, because many third-party | |
* skins don't actually have a water graphic in the same place as the default skin | |
* 3) Then we attempt to construct a passable "fire" texture from the particles file. | |
* 4) Lastly, we duplicate the texture with a green tint, immediately below the | |
* main texture group. We do this to support our "explored" highlighting - the | |
* tinting can be done easily via OpenGL itself, but there were pretty severe | |
* performance issues when I tried that on my laptop. If we just modify the texture | |
* and use offsets instead, there's no FPS drop on there. This DOES have the | |
* unfortunate downside that, when specifying texture coordinates with glTexCoord2f(), | |
* we can no longer think of the textures as perfectly "square." The Y offsets must | |
* be half of what we're used to. Perhaps it would make sense to double the X axis | |
* here as well, so that we could avoid some confusion; for now I'll leave it though. | |
* @return | |
*/ | |
public static BufferedImage getMinecraftTexture() { | |
BufferedImage bi = buildImageFromInput(getMinecraftTextureData()); | |
Graphics2D g2d = bi.createGraphics(); | |
// Figure out our square size, and then check to see if the grass tile is | |
// grayscale or not; we do this by examining the middle row of pixels. If | |
// it *is* grayscale, then colorize it. | |
int square_width = bi.getWidth()/16; | |
int[] pixels = new int[square_width]; | |
int i; | |
int r, g, b; | |
boolean grayscale = true; | |
bi.getRGB(0, square_width/2, square_width, 1, pixels, 0, square_width); | |
for (i=0; i<square_width; i++) | |
{ | |
//a = (pixels[i] & 0xFF000000) >> 24; | |
r = (pixels[i] & 0x00FF0000) >> 16; | |
g = (pixels[i] & 0x0000FF00) >> 8; | |
b = (pixels[i] & 0x000000FF); | |
//System.out.println("Pixel " + i + ": " + r + ", " + g + ", " + b + ", " + a); | |
if (g > r || g > b) | |
{ | |
grayscale = false; | |
break; | |
} | |
} | |
// Now do the coloring if we have to. | |
if (grayscale) | |
{ | |
AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.3f); | |
// First greenify grass | |
Rectangle rect = new Rectangle(0, 0, square_width, square_width); | |
g2d.setComposite(ac); | |
g2d.setColor(Color.green); | |
g2d.fill(rect); | |
g2d.drawImage(bi, null, 0, 0); | |
// Now greenify leaves | |
rect = new Rectangle(4*square_width, 3*square_width, 2*square_width, square_width); | |
g2d.setComposite(ac); | |
g2d.setColor(Color.green); | |
g2d.fill(rect); | |
g2d.drawImage(bi, null, 0, 0); | |
} | |
// Load in the water texture separately and pretend it's a part of the main texture pack. | |
BufferedImage bi2 = buildImageFromInput(getMinecraftWaterData()); | |
int water_width = bi2.getWidth(); | |
g2d.setComposite(AlphaComposite.Src); | |
if (square_width < water_width) | |
{ | |
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); | |
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); | |
} | |
else | |
{ | |
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); | |
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); | |
} | |
g2d.drawImage(bi2, 15*square_width, 12*square_width, square_width, square_width, null); | |
// Also create a fake sort of "fire" graphic to use | |
bi2 = buildImageFromInput(getMinecraftParticleData()); | |
int particle_width = bi2.getWidth()/16; | |
int fire_x = 15; | |
int fire_y = 1; | |
int flame_x = 0; | |
int flame_y = 3; | |
int start_fire_x = fire_x*square_width; | |
int start_fire_y = fire_y*square_width; | |
int start_flame_x = flame_x*particle_width; | |
int start_flame_y = flame_y*particle_width; | |
g2d.setComposite(AlphaComposite.Src); | |
g2d.setColor(new Color(0f, 0f, 0f, 0f)); | |
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); | |
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); | |
g2d.fillRect(fire_x*square_width, fire_y*square_width, square_width, square_width); | |
if (square_width < (particle_width*2)) | |
{ | |
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); | |
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); | |
} | |
g2d.drawImage(bi2, | |
start_fire_x, start_fire_y, start_fire_x+(square_width/2), start_fire_y+(square_width/2), | |
start_flame_x, start_flame_y, start_flame_x+particle_width, start_flame_y+particle_width, | |
null); | |
g2d.drawImage(bi2, | |
start_fire_x+(square_width/2), start_fire_y, start_fire_x+square_width, start_fire_y+(square_width/2), | |
start_flame_x, start_flame_y, start_flame_x+particle_width, start_flame_y+particle_width, | |
null); | |
g2d.drawImage(bi2, | |
start_fire_x, start_fire_y+(square_width/2), start_fire_x+(square_width/2), start_fire_y+square_width, | |
start_flame_x, start_flame_y, start_flame_x+particle_width, start_flame_y+particle_width, | |
null); | |
g2d.drawImage(bi2, | |
start_fire_x+(square_width/2), start_fire_y+(square_width/2), start_fire_x+square_width, start_fire_y+square_width, | |
start_flame_x, start_flame_y, start_flame_x+particle_width, start_flame_y+particle_width, | |
null); | |
// Duplicate the texture underneath, tinted for our "explored" areas | |
bi2 = new BufferedImage(bi.getWidth(), bi.getHeight()*2, BufferedImage.TYPE_INT_ARGB); | |
g2d = bi2.createGraphics(); | |
g2d.setComposite(AlphaComposite.Src); | |
g2d.drawImage(bi, 0, 0, bi.getWidth(), bi.getHeight(), null); | |
g2d.drawImage(bi, 0, bi.getHeight(), bi.getWidth(), bi.getHeight(), null); | |
g2d.setComposite(AlphaComposite.SrcAtop); | |
g2d.setColor(new Color(0f, 1f, 0f, .2f)); | |
g2d.fillRect(0, bi.getHeight(), bi.getWidth(), bi.getHeight()); | |
return bi2; | |
} | |
/*** | |
* Attempts to create a bufferedImage containing the font | |
* @return | |
*/ | |
public static BufferedImage getMinecraftFont() { | |
return buildImageFromInput(getMinecraftFontData()); | |
} | |
/*** | |
* Creates an Inputstream to a file in the bin/ directory in the minecraft directory. | |
* This handles overrides. If a file exists in the bin/ directory it will load that. | |
* Otherwise it will look in the .jar file of minecraft | |
* @param fileName | |
* @return | |
*/ | |
public static InputStream getMinecraftFile(String fileName){ | |
File overrideFile = new File(baseDir, "bin/" + fileName); | |
if(overrideFile.exists()) { | |
try { | |
return new FileInputStream(overrideFile); | |
} catch (FileNotFoundException e) { | |
// TODO Auto-generated catch block | |
e.printStackTrace(); | |
return null; | |
} | |
} | |
File minecraftDataFile = new File(baseDir, "bin/minecraft.jar"); | |
if(!minecraftDataFile.exists()) { | |
return null; | |
} | |
try { | |
JarFile jf = new JarFile(minecraftDataFile); | |
ZipEntry zipEntry = jf.getEntry(fileName); | |
if(zipEntry == null) { | |
return null; | |
} | |
return jf.getInputStream(zipEntry); | |
} catch (IOException e) { | |
System.out.println(e.toString()); | |
return null; | |
} | |
} | |
} |