359 lines
14 KiB
Java
359 lines
14 KiB
Java
package org.dynmap;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import org.bukkit.World;
|
|
import org.bukkit.Location;
|
|
import org.dynmap.debug.Debug;
|
|
import org.dynmap.kzedmap.KzedMap;
|
|
import org.dynmap.kzedmap.KzedMap.KzedBufferedImage;
|
|
import org.dynmap.utils.FileLockManager;
|
|
import org.dynmap.utils.MapChunkCache;
|
|
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.File;
|
|
import java.io.FilenameFilter;
|
|
import java.io.IOException;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
|
public class DynmapWorld {
|
|
public enum AutoGenerateOption {
|
|
NONE,
|
|
FORMAPONLY,
|
|
PERMANENT
|
|
}
|
|
public World world;
|
|
public List<MapType> maps = new ArrayList<MapType>();
|
|
public UpdateQueue updates = new UpdateQueue();
|
|
public ConfigurationNode configuration;
|
|
public List<Location> seedloc;
|
|
public List<MapChunkCache.VisibilityLimit> visibility_limits;
|
|
public AutoGenerateOption do_autogenerate;
|
|
public MapChunkCache.HiddenChunkStyle hiddenchunkstyle;
|
|
public int servertime;
|
|
public boolean sendposition;
|
|
public boolean sendhealth;
|
|
public boolean bigworld; /* If true, deeper directory hierarchy */
|
|
private int extrazoomoutlevels; /* Number of additional zoom out levels to generate */
|
|
public File worldtilepath;
|
|
private Object lock = new Object();
|
|
private HashSet<String> zoomoutupdates[];
|
|
private boolean checkts = true; /* Check timestamps on first run with new configuration */
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public void setExtraZoomOutLevels(int lvl) {
|
|
extrazoomoutlevels = lvl;
|
|
zoomoutupdates = new HashSet[lvl];
|
|
for(int i = 0; i < lvl; i++)
|
|
zoomoutupdates[i] = new HashSet<String>();
|
|
checkts = true;
|
|
}
|
|
public int getExtraZoomOutLevels() { return extrazoomoutlevels; }
|
|
|
|
public void enqueueZoomOutUpdate(File f) {
|
|
enqueueZoomOutUpdate(f, 0);
|
|
}
|
|
|
|
private void enqueueZoomOutUpdate(File f, int level) {
|
|
if(level >= extrazoomoutlevels)
|
|
return;
|
|
synchronized(lock) {
|
|
zoomoutupdates[level].add(f.getPath());
|
|
}
|
|
}
|
|
|
|
private boolean popQueuedUpdate(File f, int level) {
|
|
if(level >= extrazoomoutlevels)
|
|
return false;
|
|
synchronized(lock) {
|
|
return zoomoutupdates[level].remove(f.getPath());
|
|
}
|
|
}
|
|
|
|
private static class DirFilter implements FilenameFilter {
|
|
public boolean accept(File f, String n) {
|
|
if(!n.equals("..") && !n.equals(".")) {
|
|
File fn = new File(f, n);
|
|
return fn.isDirectory();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static class PNGFileFilter implements FilenameFilter {
|
|
String prefix;
|
|
public PNGFileFilter(String pre) { prefix = pre; }
|
|
public boolean accept(File f, String n) {
|
|
if(n.endsWith(".png") && n.startsWith(prefix)) {
|
|
File fn = new File(f, n);
|
|
return fn.isFile();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void freshenZoomOutFiles() {
|
|
for(int i = 0; i < extrazoomoutlevels; i++) {
|
|
freshenZoomOutFilesByLevel(i);
|
|
}
|
|
checkts = false; /* Just handle queued updates after first scan */
|
|
}
|
|
|
|
private static class PrefixData {
|
|
int stepsize;
|
|
int[] stepseq;
|
|
boolean neg_step_x;
|
|
String baseprefix;
|
|
int zoomlevel;
|
|
String zoomprefix;
|
|
String fnprefix;
|
|
String zfnprefix;
|
|
int bigworldshift;
|
|
}
|
|
|
|
public void freshenZoomOutFilesByLevel(int zoomlevel) {
|
|
int cnt = 0;
|
|
Debug.debug("freshenZoomOutFiles(" + world.getName() + "," + zoomlevel + ")");
|
|
if(worldtilepath.exists() == false) /* Quit if not found */
|
|
return;
|
|
HashMap<String, PrefixData> maptab = buildPrefixData(zoomlevel);
|
|
|
|
if(bigworld) { /* If big world, next directories are map name specific */
|
|
DirFilter df = new DirFilter();
|
|
for(String pfx : maptab.keySet()) { /* Walk through prefixes, as directories */
|
|
PrefixData pd = maptab.get(pfx);
|
|
File dname = new File(worldtilepath, pfx);
|
|
/* Now, go through subdirectories under this one, and process them */
|
|
String[] subdir = dname.list(df);
|
|
if(subdir == null) continue;
|
|
for(String s : subdir) {
|
|
File sdname = new File(dname, s);
|
|
cnt += processZoomDirectory(sdname, pd);
|
|
}
|
|
}
|
|
}
|
|
else { /* Else, classic file layout */
|
|
for(String pfx : maptab.keySet()) { /* Walk through prefixes, as directories */
|
|
cnt += processZoomDirectory(worldtilepath, maptab.get(pfx));
|
|
}
|
|
}
|
|
Debug.debug("freshenZoomOutFiles(" + world.getName() + "," + zoomlevel + ") - done (" + cnt + " updated files)");
|
|
}
|
|
|
|
private HashMap<String, PrefixData> buildPrefixData(int zoomlevel) {
|
|
HashMap<String, PrefixData> maptab = new HashMap<String, PrefixData>();
|
|
/* Build table of file prefixes and step sizes */
|
|
for(MapType mt : maps) {
|
|
List<String> pfx = mt.baseZoomFilePrefixes();
|
|
int stepsize = mt.baseZoomFileStepSize();
|
|
int bigworldshift = mt.getBigWorldShift();
|
|
boolean neg_step_x = false;
|
|
if(stepsize < 0) {
|
|
stepsize = -stepsize;
|
|
neg_step_x = true;
|
|
}
|
|
int[] stepseq = mt.zoomFileStepSequence();
|
|
for(String p : pfx) {
|
|
PrefixData pd = new PrefixData();
|
|
pd.stepsize = stepsize;
|
|
pd.neg_step_x = neg_step_x;
|
|
pd.stepseq = stepseq;
|
|
pd.baseprefix = p;
|
|
pd.zoomlevel = zoomlevel;
|
|
pd.zoomprefix = "zzzzzzzzzzzz".substring(0, zoomlevel);
|
|
pd.bigworldshift = bigworldshift;
|
|
if(bigworld) {
|
|
if(zoomlevel > 0) {
|
|
pd.zoomprefix += "_";
|
|
pd.zfnprefix = "z" + pd.zoomprefix;
|
|
}
|
|
else {
|
|
pd.zfnprefix = "z_";
|
|
}
|
|
pd.fnprefix = pd.zoomprefix;
|
|
}
|
|
else {
|
|
pd.fnprefix = pd.zoomprefix + pd.baseprefix;
|
|
pd.zfnprefix = "z" + pd.fnprefix;
|
|
}
|
|
|
|
maptab.put(p, pd);
|
|
}
|
|
}
|
|
return maptab;
|
|
}
|
|
|
|
private static class ProcessTileRec {
|
|
File zf;
|
|
String zfname;
|
|
int x, y;
|
|
}
|
|
|
|
private String makeFilePath(PrefixData pd, int x, int y, boolean zoomed) {
|
|
if(bigworld)
|
|
return pd.baseprefix + "/" + (x >> pd.bigworldshift) + "_" + (y >> pd.bigworldshift) + "/" + (zoomed?pd.zfnprefix:pd.fnprefix) + x + "_" + y + ".png";
|
|
else
|
|
return (zoomed?pd.zfnprefix:pd.fnprefix) + "_" + x + "_" + y + ".png";
|
|
}
|
|
|
|
private int processZoomDirectory(File dir, PrefixData pd) {
|
|
Debug.debug("processZoomDirectory(" + dir.getPath() + "," + pd.baseprefix + ")");
|
|
HashMap<String, ProcessTileRec> toprocess = new HashMap<String, ProcessTileRec>();
|
|
String[] files = dir.list(new PNGFileFilter(pd.fnprefix));
|
|
if(files == null)
|
|
return 0;
|
|
for(String fn : files) {
|
|
ProcessTileRec tr = processZoomFile(new File(dir, fn), pd);
|
|
if(tr != null) {
|
|
String zfpath = tr.zf.getPath();
|
|
if(!toprocess.containsKey(zfpath)) {
|
|
toprocess.put(zfpath, tr);
|
|
}
|
|
}
|
|
}
|
|
int cnt = 0;
|
|
/* Do processing */
|
|
for(ProcessTileRec s : toprocess.values()) {
|
|
processZoomTile(pd, dir, s.zf, s.zfname, s.x, s.y);
|
|
cnt++;
|
|
}
|
|
Debug.debug("processZoomDirectory(" + dir.getPath() + "," + pd.baseprefix + ") - done (" + cnt + " files)");
|
|
return cnt;
|
|
}
|
|
|
|
private ProcessTileRec processZoomFile(File f, PrefixData pd) {
|
|
/* If not checking timstamp, we're out if nothing queued for this file */
|
|
if(!checkts) {
|
|
if(!popQueuedUpdate(f, pd.zoomlevel))
|
|
return null;
|
|
}
|
|
int step = pd.stepsize << pd.zoomlevel;
|
|
String fn = f.getName();
|
|
/* Parse filename to predict zoomed out file */
|
|
fn = fn.substring(0, fn.lastIndexOf('.')); /* Strip off extension */
|
|
String[] tok = fn.split("_"); /* Split by underscores */
|
|
int x = 0;
|
|
int y = 0;
|
|
boolean parsed = false;
|
|
if(tok.length >= 2) {
|
|
try {
|
|
x = Integer.parseInt(tok[tok.length-2]);
|
|
y = Integer.parseInt(tok[tok.length-1]);
|
|
parsed = true;
|
|
} catch (NumberFormatException nfx) {
|
|
}
|
|
}
|
|
if(!parsed)
|
|
return null;
|
|
if(pd.neg_step_x) x = -x;
|
|
if(x >= 0)
|
|
x = x - (x % (2*step));
|
|
else
|
|
x = x + (x % (2*step));
|
|
if(pd.neg_step_x) x = -x;
|
|
if(y >= 0)
|
|
y = y - (y % (2*step));
|
|
else
|
|
y = y + (y % (2*step));
|
|
/* Make name of corresponding zoomed tile */
|
|
String zfname = makeFilePath(pd, x, y, true);
|
|
File zf = new File(worldtilepath, zfname);
|
|
if(checkts) { /* If checking timestamp, see if we need update based on enqueued update OR old file time */
|
|
/* If we're not updated, and zoom file exists and is older than our file, nothing to do */
|
|
if((!popQueuedUpdate(f, pd.zoomlevel)) && zf.exists() && (zf.lastModified() >= f.lastModified())) {
|
|
return null;
|
|
}
|
|
}
|
|
ProcessTileRec rec = new ProcessTileRec();
|
|
rec.zf = zf;
|
|
rec.x = x;
|
|
rec.y = y;
|
|
rec.zfname = zfname;
|
|
Debug.debug("Process " + zf.getPath() + " due to " + f.getPath());
|
|
return rec;
|
|
}
|
|
|
|
private void processZoomTile(PrefixData pd, File dir, File zf, String zfname, int tx, int ty) {
|
|
Debug.debug("processZoomFile(" + pd.baseprefix + "," + dir.getPath() + "," + zf.getPath() + "," + tx + "," + ty + ")");
|
|
int width = 128, height = 128;
|
|
BufferedImage zIm = null;
|
|
KzedBufferedImage kzIm = null;
|
|
int[] argb = new int[width*height];
|
|
int step = pd.stepsize << pd.zoomlevel;
|
|
int ztx = tx;
|
|
tx = tx - (pd.neg_step_x?step:0); /* Adjust for negative step */
|
|
|
|
/* create image buffer */
|
|
kzIm = KzedMap.allocateBufferedImage(width, height);
|
|
zIm = kzIm.buf_img;
|
|
|
|
for(int i = 0; i < 4; i++) {
|
|
File f = new File(worldtilepath, makeFilePath(pd, (tx + step*(1&pd.stepseq[i])), (ty + step*(pd.stepseq[i]>>1)), false));
|
|
if(f.exists()) {
|
|
BufferedImage im = null;
|
|
FileLockManager.getReadLock(f);
|
|
popQueuedUpdate(f, pd.zoomlevel);
|
|
try {
|
|
im = ImageIO.read(f);
|
|
} catch (IOException e) {
|
|
} catch (IndexOutOfBoundsException e) {
|
|
}
|
|
FileLockManager.releaseReadLock(f);
|
|
if(im != null) {
|
|
im.getRGB(0, 0, width, height, argb, 0, width); /* Read data */
|
|
im.flush();
|
|
/* Do binlinear scale to 64x64 */
|
|
Color c1 = new Color();
|
|
for(int y = 0; y < height; y += 2) {
|
|
for(int x = 0; x < width; x += 2) {
|
|
int red = 0;
|
|
int green = 0;
|
|
int blue = 0;
|
|
int alpha = 0;
|
|
for(int yy = y; yy < y+2; yy++) {
|
|
for(int xx = x; xx < x+2; xx++) {
|
|
c1.setARGB(argb[(yy*width)+xx]);
|
|
red += c1.getRed();
|
|
green += c1.getGreen();
|
|
blue += c1.getBlue();
|
|
alpha += c1.getAlpha();
|
|
}
|
|
}
|
|
c1.setRGBA(red>>2, green>>2, blue>>2, alpha>>2);
|
|
argb[(y*width/2) + (x/2)] = c1.getARGB();
|
|
}
|
|
}
|
|
/* blit scaled rendered tile onto zoom-out tile */
|
|
zIm.setRGB(((i>>1) != 0)?0:width/2, (i & 1) * height/2, width/2, height/2, argb, 0, width);
|
|
}
|
|
}
|
|
}
|
|
FileLockManager.getWriteLock(zf);
|
|
TileHashManager hashman = MapManager.mapman.hashman;
|
|
long crc = hashman.calculateTileHash(kzIm.argb_buf); /* Get hash of tile */
|
|
int tilex = ztx/step/2;
|
|
int tiley = ty/step/2;
|
|
String key = world.getName()+".z"+pd.zoomprefix+pd.baseprefix;
|
|
if((!zf.exists()) || (crc != MapManager.mapman.hashman.getImageHashCode(key, null, tilex, tiley))) {
|
|
try {
|
|
if(!zf.getParentFile().exists())
|
|
zf.getParentFile().mkdirs();
|
|
FileLockManager.imageIOWrite(zIm, "png", zf);
|
|
Debug.debug("Saved zoom-out tile at " + zf.getPath());
|
|
} catch (IOException e) {
|
|
Debug.error("Failed to save zoom-out tile: " + zf.getName(), e);
|
|
} catch (java.lang.NullPointerException e) {
|
|
Debug.error("Failed to save zoom-out tile (NullPointerException): " + zf.getName(), e);
|
|
}
|
|
hashman.updateHashCode(key, null, tilex, tiley, crc);
|
|
MapManager.mapman.pushUpdate(this.world, new Client.Tile(zfname));
|
|
enqueueZoomOutUpdate(zf, pd.zoomlevel+1);
|
|
}
|
|
FileLockManager.releaseWriteLock(zf);
|
|
KzedMap.freeBufferedImage(kzIm);
|
|
}
|
|
}
|