[Refactor] Use proper folder structure

master
halotroop2288 2020-02-03 16:57:13 -08:00
parent 72406cd513
commit 9a745dfacc
25 changed files with 950 additions and 229 deletions

View File

@ -1,21 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-12">
<attributes>
<attribute name="module" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,8 +1,8 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=12
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=12
org.eclipse.jdt.core.compiler.compliance=11
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@ -12,4 +12,4 @@ org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=12
org.eclipse.jdt.core.compiler.source=11

View File

@ -1,6 +1,7 @@
### LiteCraft
![GPLv3 or later](https://www.gnu.org/graphics/gplv3-or-later.png "GPLv3-plus Logo")
# LiteCraft
A lightweight, singleplayer only Minecraft clone <br />
Aims to be on-par with Minecraft 1.2.5 (Just before they started using integrated servers), in terms of quality, with thousands of lines less code, and quite a few less bugs. <br />
#### Goals:
- Learn the (old) Minecraft codebase inside and out
@ -8,7 +9,7 @@ Aims to be on-par with Minecraft 1.2.5 (Just before they started using integrate
- Create a clone of Minecraft with as little original code and as many open-source
libraries as possible
- Use a mainly data-driven structure, allowing for easier additions (and removals) of features, saving custom classes for only really unique features. (So all farmable mobs would share the same class, for instance.)
#### Planned Feature Changes from Vanilla Minecraft
- No more multiplayer. (It adds a lot of lines of code, as well as requiring an entirely
different app for a server.)
@ -17,4 +18,8 @@ Aims to be on-par with Minecraft 1.2.5 (Just before they started using integrate
- Pretty much any creature, monster, item, block, dimension that's unique to Minecraft will not
be present either.
- These features will be replaced by our own original ideas, or some more generic ones, instead.
- Only built-in texture packs. This allows us to write fewer lines of code to process custom resources.
- Only built-in texture packs. This allows us to write fewer lines of code to process custom resources
# LICENSE
This project is licensed under the GNU General Public License Version 3.0<br />
For details see [LICENSE](https://github.com/halotroop/LiteCraft/blob/master/LICENSE)

173
pom.xml
View File

@ -1,53 +1,126 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>LiteCraft</groupId>
<artifactId>LiteCraft</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>LiteCraft</name>
<description>Lightweight Minecraft Clone Engine</description>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<lwjgl.version>3.2.3</lwjgl.version>
<joml.version>1.9.17</joml.version>
<gson.version>2.8.5</gson.version>
<lwjgl.natives>natives-windows</lwjgl.natives>
</properties>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>LiteCraft</groupId>
<artifactId>LiteCraft</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>LiteCraft</name>
<description>Lightweight Minecraft Clone Engine</description>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<gson.version>2.8.5</gson.version>
<lwjgl.version>3.2.4-SNAPSHOT</lwjgl.version>
<joml.version>1.9.20</joml.version>
<lwjgl.natives>natives-linux</lwjgl.natives>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-bom</artifactId>
<version>${lwjgl.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>sonatype-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl</artifactId></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-glfw</artifactId></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-openal</artifactId></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-opengl</artifactId></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl</artifactId><classifier>${lwjgl.natives}</classifier></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-glfw</artifactId><classifier>${lwjgl.natives}</classifier></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-openal</artifactId><classifier>${lwjgl.natives}</classifier></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-opengl</artifactId><classifier>${lwjgl.natives}</classifier></dependency>
<dependency><groupId>org.joml</groupId><artifactId>joml</artifactId><version>${joml.version}</version></dependency>
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>${gson.version}</version></dependency>
<dependency><groupId>net.sourceforge.argo</groupId><artifactId>argo</artifactId><version>5.5</version></dependency>
<dependency><groupId>org.spongepowered</groupId><artifactId>noise</artifactId><version>2.0.0-SNAPSHOT</version></dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-bom</artifactId>
<version>${lwjgl.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.Spoutcraft</groupId>
<artifactId>soundsystem</artifactId>
<version>master-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.aeonbits.owner</groupId>
<artifactId>owner</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-glfw</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-opengl</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-glfw</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-opengl</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.joml</groupId>
<artifactId>joml</artifactId>
<version>${joml.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.argo</groupId>
<artifactId>argo</artifactId>
<version>5.5</version>
</dependency>
</dependencies>
</project>

View File

@ -1,6 +0,0 @@
package com.github.halotroop.litecraft.logic;
public interface TickListener
{
void onTick(float deltaTime);
}

View File

@ -1,81 +1,99 @@
package com.github.halotroop.litecraft;
import org.apache.commons.cli.*;
import org.lwjgl.*;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*;
import com.github.halotroop.litecraft.logic.TickListener;
import com.github.halotroop.litecraft.logic.Timer;
import com.github.halotroop.litecraft.logic.Timer.TickListener;
import com.github.halotroop.litecraft.render.Renderer;
public class LiteCraftMain
public class LiteCraftMain implements Runnable
{
public static int maxFPS = 60;
public static int width = 640, height = 480; // Don't change these values. They just initialize it in case we forget to set them later.
public static boolean spamLog, debug;
public String splashText = "";
protected Timer timer;
private int fps, ups, tps;
public static int maxFPS = 100;
private long frameTimer;
private Renderer renderer;
public static int width = 400, height = 300; // Don't change these values. They just initialize it in case we forget to set them later.
private Window window;
private static boolean spamLog;
public static void main(String[] args) throws Exception
{
Options options = new Options();
CommandLineParser parser = new DefaultParser();
HelpFormatter formatter = new HelpFormatter();
CommandLine cmd;
options.addOption(new Option("w", "width", true, "Screen width"));
options.addOption(new Option("h", "height", true, "Screen height"));
options.addOption(new Option("debug", "debug", true, "Use debug features"));
options.addOption(new Option("spam_log","spam_log", true, "Log sanity checks"));
options.addOption(new Option("limit_fps", "limit_fps", true, "Use the FPS limiter"));
options.addOption(new Option("max_fps", "max_fps", true, "The maximum amount of FPS"));
try
{
cmd = parser.parse(options, args);
width = Integer.parseInt(cmd.getOptionValue("width", "640"));
height = Integer.parseInt(cmd.getOptionValue("height", "480"));
debug = Boolean.parseBoolean(cmd.getOptionValue("debug", "false"));
spamLog = Boolean.parseBoolean(cmd.getOptionValue("spam_log", "false"));
maxFPS = Integer.parseInt(cmd.getOptionValue("max_fps", "60"));
}
catch (ParseException e)
{
System.out.println(e.getLocalizedMessage());
formatter.printHelp("utility-name", options);
}
new LiteCraftMain().run();
}
protected TickListener tickListener = new TickListener()
{
@Override
public void onTick(float deltaTime)
{
tps++;
}
{ tps++; }
};
public static void main(String[] args)
{
width = 1600;
height = 900;
spamLog = false;
new LiteCraftMain().run();
}
public void run()
{
System.out.println("Running program.");
System.out.println("LWJGL version: " + Version.getVersion());
init();
frameTimer = System.currentTimeMillis();
// Run the rendering loop until the player has attempted to close the window
while (!GLFW.glfwWindowShouldClose(window.getWindowLong()))
{
loop();
}
destroy();
// Shuts down the game and destroys all the things that are using RAM (so the user doesn't have to restart their computer afterward...)
private void destroy()
{
if (debug) System.out.println("Closing game...");
renderer.cleanUp();
window.destroy();
// Terminate GLFW and free the error callback
GLFW.glfwTerminate();
GLFW.glfwSetErrorCallback(null).free();
if (debug) System.out.println("Game closed successfully.");
}
private void init()
{
System.out.println("Initializing game...");
if (debug) System.out.println("Initializing game...");
// Setup an error callback. The default implementation will print the error message in System.err.
GLFWErrorCallback.createPrint(System.err).set();
// Initialize GLFW. Most GLFW functions will not work before doing this.
if (!GLFW.glfwInit()) throw new IllegalStateException("Unable to initialize GLFW");
// Configure GLFW
window = new Window(width, height);
timer = new Timer(20);
timer.addTickListener(tickListener);
GL.createCapabilities(); // This line is critical for LWJGL's interoperation with GLFW.
renderer = new Renderer();
window.setWindowTitle("LiteCraft - " + "INSERT SPLASH TEXT HERE!");
GL.createCapabilities(); // This line is critical for LWJGL's interoperation with GLFW.
renderer = new Renderer();
if (splashText == "")
{
window.setWindowTitle("LiteCraft - " + "INSERT SPLASH TEXT HERE!");
}
input();
System.out.println("Initialization complete.");
if (debug) System.out.println("Initialization complete.");
}
// Sets up the key inputs for the game (currently just esc for closing the game)
@ -88,21 +106,18 @@ public class LiteCraftMain
GLFW.glfwSetWindowShouldClose(window, true); // We will detect this in the game loop
});
}
// Things that the game should do over and over and over again until it is closed
private void loop()
{
ups++;
// Poll for window events. The key callback above will only be invoked during this call.
GLFW.glfwPollEvents();
timer.tick();
if (fps < maxFPS) render();
if (System.currentTimeMillis() > frameTimer + 1000) // wait for one second
{
window.setWindowTitle("LiteCraft | FPS: " + fps + " | TPS: " + tps + " | UPS: " + ups);
if (debug) window.setWindowTitle("LiteCraft | FPS: " + fps + " | TPS: " + tps + " | UPS: " + ups);
fps = 0;
ups = 0;
tps = 0;
@ -113,25 +128,21 @@ public class LiteCraftMain
public void render()
{
if (spamLog) System.out.println("rendering " + fps);
renderer.render();
window.render();
fps++; // After a successful frame render, increase the frame counter.
}
// Shuts down the game and destroys all the things that are using RAM (so the user doesn't have to restart their computer afterward...)
private void destroy()
public void run()
{
System.out.println("Closing game...");
renderer.cleanUp();
window.destroy();
// Terminate GLFW and free the error callback
GLFW.glfwTerminate();
GLFW.glfwSetErrorCallback(null).free();
System.out.println("Game closed successfully.");
System.out.println("Starting game...");
System.out.println("LWJGL version: " + Version.getVersion());
System.out.println("Resolution: " + width + 'x' + height);
init();
frameTimer = System.currentTimeMillis();
// Run the rendering loop until the player has attempted to close the window
while (!GLFW.glfwWindowShouldClose(window.getWindowLong()))
{ loop(); }
destroy();
}
}

View File

@ -10,16 +10,12 @@ public class Window
private long windowLong = 0;
public long getWindowLong()
{
return windowLong;
}
{ return windowLong; }
private String title;
public String getWindowTitle()
{
return title;
}
{ return title; }
protected void setWindowTitle(String title)
{
@ -31,14 +27,10 @@ public class Window
private int width, height;
public int getHeight()
{
return height;
}
{ return height; }
public int getWidth()
{
return width;
}
{ return width; }
public void setHeight(int height)
{
@ -63,20 +55,18 @@ public class Window
}
public boolean shouldClose()
{
return GLFW.glfwWindowShouldClose(windowLong);
}
{ return GLFW.glfwWindowShouldClose(windowLong); }
private void closeWindow()
{
GLFW.glfwSetWindowShouldClose(windowLong, true);
}
public void closeWindow()
{ GLFW.glfwSetWindowShouldClose(windowLong, true); }
// (Always useful to have simpler inputs, even if you only ever plan on using these once. Can be great for debugging, or just making life easier.
public Window()
{this(1600, 900);}
{ this(1600, 900); }
public Window(int width, int height)
{this(width, height, "LiteCraft");}
{ this(width, height, "LiteCraft"); }
public Window(int width, int height, String title)
{
// Keep these in this order!
@ -106,7 +96,7 @@ public class Window
GLFW.glfwShowWindow(windowLong); // Make the window visible
}
}
public void destroy()
{
Callbacks.glfwFreeCallbacks(windowLong);
@ -114,23 +104,14 @@ public class Window
}
public void render()
{
swapDisplayBuffers();
}
{ swapDisplayBuffers(); }
public void hide()
{
GLFW.glfwHideWindow(windowLong);
}
{ GLFW.glfwHideWindow(windowLong); }
public void show()
{
GLFW.glfwShowWindow(windowLong);
}
{ GLFW.glfwShowWindow(windowLong); }
private void swapDisplayBuffers()
{
GLFW.glfwSwapBuffers(windowLong);
}
{ GLFW.glfwSwapBuffers(windowLong); }
}

View File

@ -3,8 +3,6 @@ package com.github.halotroop.litecraft.logic;
import java.util.HashSet;
import java.util.Set;
import org.lwjgl.glfw.GLFW;
/*
* @author Jack Wilsdon (Stack Exchange)
* https://codereview.stackexchange.com/questions/111855/ticker-for-game-timing
@ -15,31 +13,21 @@ public class Timer
private double nextTick;
private int tickRate;
private Set<TickListener> tickListeners = new HashSet<>();
public Timer(int tickRate)
{
this.tickRate = tickRate;
}
{ this.tickRate = tickRate; }
public void addTickListener(TickListener listener)
{
tickListeners.add(listener);
}
{ tickListeners.add(listener); }
public void removeTickListener(TickListener listener)
{
tickListeners.remove(listener);
}
{ tickListeners.remove(listener); }
public void setTickRate(int tickRate)
{
this.tickRate = tickRate;
}
{ this.tickRate = tickRate; }
public int getTickRate()
{
return tickRate;
}
{ return tickRate; }
public void reset()
{
@ -60,13 +48,16 @@ public class Timer
}
float deltaTime = (float) (currentTime - lastTick) / targetTimeDelta;
for (TickListener listener : tickListeners)
{
listener.onTick(deltaTime);
}
{ listener.onTick(deltaTime); }
lastTick = currentTime;
nextTick = currentTime + targetTimeDelta;
return true;
}
return false;
}
public interface TickListener
{
void onTick(float deltaTime);
}
}

View File

@ -0,0 +1,20 @@
package com.github.halotroop.litecraft.options;
import org.aeonbits.owner.Config;
@Config.Sources("file:~/Documents/LiteCraft.config")
public interface SettingsConfig extends Config
{
@Key("screen_width")
@DefaultValue("640")
public int screenWidth();
@Key("screen_height")
@DefaultValue("480")
public int screenHeight();
@Key("debug_mode")
@DefaultValue("false")
public boolean debugMode();
@Key("spam_log")
@DefaultValue("false")
public boolean spamLog();
}

View File

@ -10,23 +10,20 @@ import com.github.halotroop.litecraft.render.model.Vertex;
public class Renderer
{
private Model model;
public Renderer()
{
init();
model = new Model();
Vertex[] vertices =
{
new Vertex(-1, -1, 0),
new Vertex(1, -1, 0),
new Vertex(0, 1, 0)
};
model.bufferVertices(vertices);
}
public void render()
{
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, model.getVBO());
@ -38,19 +35,14 @@ public class Renderer
}
private void init()
{
prepare();
}
{ prepare(); }
private void prepare()
{
GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Set the background color
GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Set the background color
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); // clear the framebuffer
}
public void cleanUp()
{
}
{}
}

View File

@ -8,32 +8,26 @@ import org.lwjgl.opengl.GL15;
public class Model
{
private int vbo, size;
public Model()
{
vbo = GL15.glGenBuffers();
size = 0;
}
public void bufferVertices(Vertex[] verts)
{
FloatBuffer buffer = BufferUtils.createFloatBuffer(verts.length * Vertex.SIZE);
for (Vertex vertex : verts)
{
buffer.put(vertex.x);
buffer.put(vertex.y);
buffer.put(vertex.z);
}
buffer.flip();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
size = verts.length;
}

View File

@ -5,7 +5,7 @@ import org.joml.Vector3i;
public class Vertex extends Vector3i
{
public static final int SIZE = 3;
public Vertex(int x, int y, int z)
{
this.x = x;

View File

@ -3,7 +3,5 @@ package com.github.halotroop.litecraft.types.gui;
public class MainMenu extends Menu
{
public MainMenu()
{
super();
}
{ super(); }
}

View File

@ -0,0 +1,326 @@
package scaveleous.mcregion;
/*
** Author: Scaveleous (Minecraft Forum)
** (Public domain)
**/
// Interfaces with region files on the disk
/*
Region File Format
Each region file represents a 32x32 group of chunks. The conversion from
chunk number to region number is floor(coord / 32): a chunk at (30, -3)
would be in region (0, -1), and one at (70, -30) would be at (3, -1).
Region files are named "r.x.z.data", where x and z are the region coordinates.
A region file begins with a 4KB header that describes where chunks are stored
in the file. A 4-byte big-endian integer represents sector offsets and sector
counts. The chunk offset for a chunk (x, z) begins at byte 4*(x+z*32) in the
file. The bottom byte of the chunk offset indicates the number of sectors the
chunk takes up, and the top 3 bytes represent the sector number of the chunk.
Given a chunk offset o, the chunk data begins at byte 4096*(o/256) and takes up
at most 4096*(o%256) bytes. A chunk cannot exceed 1MB in size. If a chunk
offset is 0, the corresponding chunk is not stored in the region file.
Chunk data begins with a 4-byte big-endian integer representing the chunk data
length in bytes, not counting the length field. The length must be smaller than
4096 times the number of sectors. The next byte is a version field, to allow
backwards-compatible updates to how chunks are encoded.
A version of 1 represents a gzipped NBT file. The gzipped data is the chunk
length - 1.
A version of 2 represents a deflated (zlib compressed) NBT file. The deflated
data is the chunk length - 1.
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
public class RegionFile
{
/* lets chunk writing be multithreaded by not locking the whole file as a
chunk is serializing -- only writes when serialization is over */
class ChunkBuffer extends ByteArrayOutputStream
{
private int x, z;
public ChunkBuffer(int x, int z)
{
super(8192); // initialize to 8KB
this.x = x;
this.z = z;
}
@Override
public void close()
{ RegionFile.this.write(x, z, buf, count); }
}
static final int CHUNK_HEADER_SIZE = 5;
private static final byte emptySector[] = new byte[4096];
private final File fileName;
private RandomAccessFile file;
private final int offsets[];
private ArrayList<Boolean> sectorFree;
private int sizeDelta;
private long lastModified = 0;
public RegionFile(File path)
{
offsets = new int[1024];
fileName = path;
debugln("REGION LOAD " + fileName);
sizeDelta = 0;
try
{
if (path.exists())
lastModified = path.lastModified();
file = new RandomAccessFile(path, "rw");
if (file.length() < 4096)
{
/* we need to write the chunk offset table */
for (int i = 0; i < 1024; ++i)
file.writeInt(0);
sizeDelta += 4096;
}
if ((file.length() & 0xfff) != 0)
{
/* the file size is not a multiple of 4KB, grow it */
for (int i = 0; i < (file.length() & 0xfff); ++i)
file.write((byte) 0);
}
/* set up the available sector map */
int nSectors = (int) file.length() / 4096;
sectorFree = new ArrayList<Boolean>(nSectors);
for (int i = 0; i < nSectors; ++i)
{ sectorFree.add(true); }
sectorFree.set(0, false); // chunk offset table
file.seek(0);
for (int i = 0; i < 1024; ++i)
{
int offset = file.readInt();
offsets[i] = offset;
if (offset != 0 && (offset >> 8) + (offset & 0xFF) <= sectorFree.size())
{
for (int sectorNum = 0; sectorNum < (offset & 0xFF); ++sectorNum)
{ sectorFree.set((offset >> 8) + sectorNum, false); }
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
public void close() throws IOException
{ file.close(); }
// various small debug printing helpers
private void debug(String in)
{
//System.out.print(in);
}
private void debug(String mode, int x, int z, int count, String in)
{ debug("REGION " + mode + " " + fileName.getName() + "[" + x + "," + z + "] " + count + "B = " + in); }
private void debug(String mode, int x, int z, String in)
{ debug("REGION " + mode + " " + fileName.getName() + "[" + x + "," + z + "] = " + in); }
private void debugln(String in)
{ debug(in + "\n"); }
private void debugln(String mode, int x, int z, String in)
{ debug(mode, x, z, in + "\n"); }
/* gets an (uncompressed) stream representing the chunk data
returns null if the chunk is not found or an error occurs */
public synchronized DataInputStream getChunkDataInputStream(int x, int z)
{
if (outOfBounds(x, z))
{
debugln("READ", x, z, "out of bounds");
return null;
}
try
{
int offset = getOffset(x, z);
if (offset == 0)
{
// debugln("READ", x, z, "miss");
return null;
}
int sectorNumber = offset >> 8;
int numSectors = offset & 0xFF;
if (sectorNumber + numSectors > sectorFree.size())
{
debugln("READ", x, z, "invalid sector");
return null;
}
file.seek(sectorNumber * 4096);
int length = file.readInt();
if (length > 4096 * numSectors)
{
debugln("READ", x, z, "invalid length: " + length + " > 4096 * " + numSectors);
return null;
}
byte version = file.readByte();
if (version == 1)
{
byte[] data = new byte[length - 1];
file.read(data);
DataInputStream ret = new DataInputStream(new GZIPInputStream(
new ByteArrayInputStream(data)));
// debug("READ", x, z, " = found");
return ret;
}
else if (version == 2)
{
byte[] data = new byte[length - 1];
file.read(data);
DataInputStream ret = new DataInputStream(new InflaterInputStream(
new ByteArrayInputStream(data)));
// debug("READ", x, z, " = found");
return ret;
}
debugln("READ", x, z, "unknown version " + version);
return null;
}
catch (IOException e)
{
debugln("READ", x, z, "exception");
return null;
}
}
public DataOutputStream getChunkDataOutputStream(int x, int z)
{
if (outOfBounds(x, z))
return null;
return new DataOutputStream(new DeflaterOutputStream(
new ChunkBuffer(x, z)));
}
private int getOffset(int x, int z) throws IOException
{ return offsets[x + z * 32]; }
/* gets how much the region file has grown since it was last checked */
public synchronized int getSizeDelta()
{
int ret = sizeDelta;
sizeDelta = 0;
return ret;
}
/* the modification date of the region file when it was first opened */
public long lastModified()
{ return lastModified; }
/* is this an invalid chunk coordinate? */
private boolean outOfBounds(int x, int z)
{ return x < 0 || x >= 32 || z < 0 || z >= 32; }
private void setOffset(int x, int z, int offset) throws IOException
{
offsets[x + z * 32] = offset;
file.seek((x + z * 32) * 4);
file.writeInt(offset);
}
/* write a chunk data to the region file at specified sector number */
private void write(int sectorNumber, byte[] data, int length) throws IOException
{
debugln(" " + sectorNumber);
file.seek(sectorNumber * 4096);
file.writeInt(length + 1); // chunk length
file.writeByte(2); // chunk version number
file.write(data, 0, length); // chunk data
}
/* write a chunk at (x,z) with length bytes of data to disk */
protected synchronized void write(int x, int z, byte[] data, int length)
{
try
{
int offset = getOffset(x, z);
int sectorNumber = offset >> 8;
int sectorsAllocated = offset & 0xFF;
int sectorsNeeded = (length + CHUNK_HEADER_SIZE) / 4096 + 1;
if (sectorsNeeded >= 256) // maximum chunk size is 1MB
return;
if (sectorNumber != 0 && sectorsAllocated == sectorsNeeded)
{
/* we can simply overwrite the old sectors */
debug("SAVE", x, z, length, "rewrite");
write(sectorNumber, data, length);
}
else
{
/* we need to allocate new sectors */
/* mark the sectors previously used for this chunk as free */
for (int i = 0; i < sectorsAllocated; ++i)
sectorFree.set(sectorNumber + i, true);
/* scan for a free space large enough to store this chunk */
int runStart = sectorFree.indexOf(true);
int runLength = 0;
if (runStart != -1)
{
for (int i = runStart; i < sectorFree.size(); ++i)
{
if (runLength != 0)
{
if (sectorFree.get(i))
runLength++;
else
runLength = 0;
}
else if (sectorFree.get(i))
{
runStart = i;
runLength = 1;
}
if (runLength >= sectorsNeeded)
break;
}
}
if (runLength >= sectorsNeeded)
{
/* we found a free space large enough */
debug("SAVE", x, z, length, "reuse");
sectorNumber = runStart;
setOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
for (int i = 0; i < sectorsNeeded; ++i)
sectorFree.set(sectorNumber + i, false);
write(sectorNumber, data, length);
}
else
{
/* no free space large enough found -- we need to grow the file */
debug("SAVE", x, z, length, "grow");
file.seek(file.length());
sectorNumber = sectorFree.size();
for (int i = 0; i < sectorsNeeded; ++i)
{
file.write(emptySector);
sectorFree.add(false);
}
sizeDelta += 4096 * sectorsNeeded;
write(sectorNumber, data, length);
setOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
}
}
}
catch (IOException e)
{e.printStackTrace();}
}
}

View File

@ -0,0 +1,73 @@
package scaveleous.mcregion;
/*
** Author: Scaveleous (Minecraft Forum)
** (Public domain)
**/
// A simple cache and wrapper for efficiently multiple RegionFiles simultaneously.
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
public class RegionFileCache
{
private static final Map<File, Reference<RegionFile>> cache = new HashMap<File, Reference<RegionFile>>();
public static synchronized void clear()
{
for (Reference<RegionFile> ref : cache.values())
{
try
{
if (ref.get() != null)
ref.get().close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
cache.clear();
}
public static DataInputStream getChunkDataInputStream(File basePath, int x, int z)
{
RegionFile r = getRegionFile(basePath, x, z);
return r.getChunkDataInputStream(x & 31, z & 31);
}
public static DataOutputStream getChunkDataOutputStream(File basePath, int x, int z)
{
RegionFile r = getRegionFile(basePath, x, z);
return r.getChunkDataOutputStream(x & 31, z & 31);
}
public static synchronized RegionFile getRegionFile(File basePath, int x, int z)
{
File regionDir = new File(basePath, "region");
File file = new File(regionDir, "r." + (x >> 5) + "." + (z >> 5) + ".data");
Reference<RegionFile> ref = cache.get(file);
if (ref != null && ref.get() != null)
return ref.get();
if (!regionDir.exists())
regionDir.mkdirs();
RegionFile reg = new RegionFile(file);
cache.put(file, new SoftReference<RegionFile>(reg));
return reg;
}
public static int getSizeDelta(File basePath, int x, int z)
{
RegionFile r = getRegionFile(basePath, x, z);
return r.getSizeDelta();
}
private RegionFileCache()
{}
}

View File

@ -0,0 +1,264 @@
package scaveleous.mcregion;
/*
** Author: Scaveleous (Minecraft Forum)
** (Public domain)
**/
// A tool to convert to and from chunk/region files
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
class RegionTool
{
static private boolean isConsole = false;
/* copies all files from one directory to another, except for files in the skip set
does not copy empty directories */
private static void copyDir(File srcDir, File dstDir, Set<File> skip)
{
byte buf[] = new byte[4096];
for (File child : srcDir.listFiles())
{
if (child.isDirectory())
copyDir(child, new File(dstDir, child.getName()), skip);
else
{
if (!skip.contains(child))
{
try
{
File dstfile = new File(dstDir, child.getName());
dstDir.mkdirs();
FileOutputStream out = new FileOutputStream(dstfile);
FileInputStream in = new FileInputStream(child);
int len = 0;
while (len != -1)
{
out.write(buf, 0, len);
len = in.read(buf);
}
out.close();
in.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
}
private static void exit(String message)
{
System.err.println(message);
System.exit(1);
}
private static void exitUsage()
{ exit("regionTool: converts between chunks and regions\n" +
"usage: java -jar RegionTool.jar [un]pack <world directory> [target directory]"); }
public static void main(String[] args)
{
if (args.length != 2 && args.length != 3)
exitUsage();
if (System.console() != null)
isConsole = true;
int mode = 0;
if (args[0].equalsIgnoreCase("unpack"))
mode = 1;
else if (args[0].equalsIgnoreCase("pack"))
mode = 2;
if (mode == 0)
exitUsage();
File worldDir = new File(args[1]);
if (!worldDir.exists() || !worldDir.isDirectory())
exit("error: " + worldDir.getPath() + " is not a directory");
File targetDir = worldDir;
if (args.length == 3)
{
targetDir = new File(args[2]);
if (!targetDir.isDirectory())
{ targetDir.mkdirs(); }
}
if (mode == 1)
unpack(worldDir, targetDir);
else if (mode == 2)
pack(worldDir, targetDir);
}
private static void pack(File worldDir, File targetDir)
{
Set<File> processedFiles = null;
if (worldDir != targetDir)
processedFiles = new HashSet<File>();
Pattern chunkFilePattern = Pattern.compile("c\\.(-?[0-9a-z]+)\\.(-?[0-9a-z]+).dat");
Pattern chunkFolderPattern = Pattern.compile("[0-9a-z]|1[0-9a-r]");
int chunksPacked = 0;
int chunksSkipped = 0;
for (File dir1 : worldDir.listFiles())
{
if (!dir1.isDirectory())
continue;
if (chunkFolderPattern.matcher(dir1.getName()).matches())
{
for (File dir2 : dir1.listFiles())
{
if (!dir2.isDirectory())
continue;
if (chunkFolderPattern.matcher(dir2.getName()).matches())
{
for (File chunkFile : dir2.listFiles())
{
Matcher m = chunkFilePattern.matcher(chunkFile.getName());
if (m.matches())
{
if (packChunk(targetDir, chunkFile, m))
chunksPacked++;
else
chunksSkipped++;
if (processedFiles != null)
processedFiles.add(chunkFile);
}
if (isConsole)
System.out.print("\rpacked " + chunksPacked + " chunks" +
(chunksSkipped > 0 ? ", skipped " + chunksSkipped + " older ones" : ""));
}
}
}
}
}
if (isConsole)
System.out.print("\r");
System.out.println("packed " + chunksPacked + " chunks" +
(chunksSkipped > 0 ? ", skipped " + chunksSkipped + " older ones" : ""));
if (processedFiles != null)
copyDir(worldDir, targetDir, processedFiles);
}
private static boolean packChunk(File worldDir, File chunkFile, Matcher m)
{
int x = Integer.parseInt(m.group(1), 36);
int z = Integer.parseInt(m.group(2), 36);
RegionFile region = RegionFileCache.getRegionFile(worldDir, x, z);
if (region.lastModified() > chunkFile.lastModified())
return false;
byte buf[] = new byte[4096];
int len = 0;
try
{
DataInputStream istream = new DataInputStream(
new GZIPInputStream(new FileInputStream(chunkFile)));
DataOutputStream out = region.getChunkDataOutputStream(x & 31, z & 31);
while (len != -1)
{
out.write(buf, 0, len);
len = istream.read(buf);
}
out.close();
istream.close();
return true;
}
catch (IOException e)
{
e.printStackTrace();
}
return false;
}
private static void unpack(File worldDir, File targetDir)
{
File regionDir = new File(worldDir, "region");
if (!regionDir.exists())
exit("error: region directory not found");
Set<File> processedFiles = null;
if (worldDir != targetDir)
processedFiles = new HashSet<File>();
Pattern regionFilePattern = Pattern.compile("r\\.(-?[0-9]+)\\.(-?[0-9]+).data");
Matcher match;
for (File file : regionDir.listFiles())
{
if (!file.isFile())
continue;
match = regionFilePattern.matcher(file.getName());
if (match.matches())
{
unpackRegionFile(targetDir, file, match);
if (processedFiles != null)
processedFiles.add(file);
}
}
if (processedFiles != null)
copyDir(worldDir, targetDir, processedFiles);
}
private static void unpackRegionFile(File worldDir, File file, Matcher match)
{
long regionModified = file.lastModified();
RegionFile region = new RegionFile(file);
String name = file.getName();
int regionX = Integer.parseInt(match.group(1));
int regionZ = Integer.parseInt(match.group(2));
int nWritten = 0, nSkipped = 0;
for (int x = 0; x < 32; ++x)
{
for (int z = 0; z < 32; ++z)
{
DataInputStream istream = region.getChunkDataInputStream(x, z);
if (istream == null)
continue;
int chunkX = x + (regionX << 5);
int chunkZ = z + (regionZ << 5);
String chunkName = "c." + Integer.toString(chunkX, 36) + "." + Integer.toString(chunkZ, 36) + ".dat";
File chunkFile = new File(worldDir, Integer.toString(chunkX & 63, 36));
chunkFile = new File(chunkFile, Integer.toString(chunkZ & 63, 36));
if (!chunkFile.exists())
chunkFile.mkdirs();
chunkFile = new File(chunkFile, chunkName);
byte buf[] = new byte[4096];
int len = 0;
if (chunkFile.lastModified() > regionModified)
{
nSkipped++;
}
else
{
try
{
DataOutputStream out = new DataOutputStream(
new GZIPOutputStream(new FileOutputStream(chunkFile)));
while (len != -1)
{
out.write(buf, 0, len);
len = istream.read(buf);
}
out.close();
nWritten++;
}
catch (IOException e)
{
e.printStackTrace();
}
}
if (isConsole)
System.out.print("\r" + name + ": unpacked " + nWritten + " chunks" +
(nSkipped > 0 ? ", skipped " + nSkipped + " newer ones" : ""));
}
}
if (isConsole)
System.out.print("\r");
System.out.println(name + ": unpacked " + nWritten + " chunks" +
(nSkipped > 0 ? ", skipped " + nSkipped + " newer ones" : ""));
}
}

View File

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 608 B