world saving and loading integrated into the game

pull/12/head
valoeghese 2020-02-28 10:16:10 +13:00
parent bd18284c40
commit 3d37dec619
11 changed files with 180 additions and 38 deletions

3
.gitignore vendored
View File

@ -7,6 +7,9 @@
# BlueJ files
*.ctxt
# run
saves/
# Mobile Tools for Java (J2ME)
.mtj.tmp/

View File

@ -4,9 +4,11 @@ import java.util.Random;
import org.lwjgl.glfw.GLFW;
import com.github.halotroop.litecraft.save.LitecraftSave;
import com.github.halotroop.litecraft.screens.TitleScreen;
import com.github.halotroop.litecraft.types.block.*;
import com.github.halotroop.litecraft.world.World;
import com.github.halotroop.litecraft.world.gen.Dimension;
import com.github.halotroop.litecraft.world.gen.*;
import com.github.hydos.ginger.engine.api.*;
import com.github.hydos.ginger.engine.api.game.*;
import com.github.hydos.ginger.engine.cameras.*;
@ -26,6 +28,7 @@ import tk.valoeghese.gateways.client.io.*;
public class Litecraft extends Game
{
private World world;
private LitecraftSave save;
private Ginger ginger3D;
private static Litecraft INSTANCE;
@ -51,6 +54,8 @@ public class Litecraft extends Game
setupKeybinds();
Block b = Blocks.AIR; // make sure blocks are initialised
GingerUtils.init();
Window.setBackgroundColour(0.2f, 0.2f, 0.6f);
TexturedModel dirtModel = ModelLoader.loadGenericCube("block/cubes/stone/brick/stonebrick.png");
@ -89,6 +94,7 @@ public class Litecraft extends Game
@Override
public void exit()
{
this.world.unloadAllChunks();
ginger3D.cleanup();
System.exit(0);
}
@ -146,7 +152,8 @@ public class Litecraft extends Game
public void onPlayButtonClick() {
if (world == null)
{
world = new World(new Random().nextLong(), 2, Dimension.OVERWORLD);
this.save = new LitecraftSave("test", false);
this.world = this.save.getWorldOrCreate(Dimensions.OVERWORLD);
}
}
}

View File

@ -1,59 +1,67 @@
package com.github.halotroop.litecraft.save;
import java.io.*;
import java.util.Random;
import com.github.halotroop.litecraft.world.Chunk;
import com.github.halotroop.litecraft.world.*;
import com.github.halotroop.litecraft.world.gen.*;
import tk.valoeghese.sod.BinaryData;
import tk.valoeghese.sod.*;
public final class LitecraftSave
{
public LitecraftSave(String name, boolean mustCreateNew)
{
StringBuilder sb = new StringBuilder(SAVE_DIR).append(name);
File saveFile = new File(sb.toString());
File saveDir = new File(sb.toString());
if (mustCreateNew)
{
while (saveFile.exists())
while (saveDir.exists())
{
sb.append('_');
saveFile = new File(sb.toString());
sb.append('_'); // append "_" to the save name until we get a unique save, if we must create a new save
saveDir = new File(sb.toString());
}
}
this.file = saveFile;
this.file = saveDir;
this.file.mkdirs();
}
private final File file;
public void saveChunk(Chunk chunk)
public boolean saveChunk(Chunk chunk)
{
StringBuilder fileLocBuilder = new StringBuilder(this.file.getPath())
.append('/').append(chunk.dimension)
.append('/').append(chunk.chunkX)
.append('/').append(chunk.chunkZ);
File chunkDir = new File(fileLocBuilder.toString());
chunkDir.mkdirs();
chunkDir.mkdirs(); // create directory for file if it does not exist
// format: <save dir>/<dim>/<chunkX>/<chunkZ>/<chunkY>.sod
File chunkFile = new File(fileLocBuilder.append('/').append(chunk.chunkY).append(".sod").toString());
try
{
chunkFile.createNewFile();
BinaryData data = new BinaryData();
chunk.write(data);
data.write(chunkFile);
BinaryData data = new BinaryData(); // create new empty binary data
chunk.write(data); // write the chunk info to the binary data
return data.write(chunkFile); // write the data to the file, return whether an io exception occurred
}
catch (IOException e)
{
e.printStackTrace();
return false; // io exception = chunk writing failed at some point
}
}
public Chunk readChunk(int chunkX, int chunkY, int chunkZ)
public Chunk readChunk(int chunkX, int chunkY, int chunkZ, int dimension)
{
// format: <save dir>/<dim>/<chunkX>/<chunkZ>/<chunkY>.sod
File chunkFile = new File(new StringBuilder(this.file.getPath())
.append('/').append(dimension)
.append('/').append(chunkX)
.append('/').append(chunkZ)
.append('/').append(chunkY).append(".sod").toString());
@ -62,12 +70,60 @@ public final class LitecraftSave
{
BinaryData data = BinaryData.read(chunkFile);
Chunk result = new Chunk(chunkX, chunkY, chunkZ);
result.read(data);
Chunk result = new Chunk(chunkX, chunkY, chunkZ, dimension); // create chunk
result.read(data); // load the chunk data we have just read into the chunk
return result;
}
else return null;
}
public World getWorldOrCreate(Dimension<?> dim)
{
File worldFile = new File(this.file.getPath() + "/global_data.sod");
if (worldFile.isFile()) // load world
{
BinaryData data = BinaryData.read(worldFile); // read data from the world file
DataSection properties = data.get("properties"); // get the properties data section from the data that we have just read
long seed = 0; // default seed if we cannot read it is 0
try // try read the seed from the file
{
seed = properties.readLong(0); // seed is at index 0
}
catch (Throwable e)
{
e.printStackTrace();
}
World world = new World(seed, 2, dim, this); // create new world with seed read from file or 0, if it could not be read
world.spawnPlayer(); // spawn player in world
return world;
}
else // create world
{
long seed = new Random().nextLong();
try
{
worldFile.createNewFile(); // create world file
BinaryData data = new BinaryData(); // create empty binary data
DataSection properties = new DataSection(); // create empty data section for properties
properties.writeLong(seed); // write seed at index 0
data.put("properties", properties); // add properties section
data.write(worldFile); // write to file
}
catch (IOException e)
{
// If this fails the world seed will not be consistent across saves
e.printStackTrace();
}
World world = new World(seed, 2, dim, this); // create new world with generated seed
world.spawnPlayer(); // spawn player in world
return world;
}
}
private static final String SAVE_DIR = "./saves/";
}

View File

@ -7,10 +7,6 @@ import com.github.hydos.ginger.engine.render.models.TexturedModel;
public class Block
{
public static final Block AIR = new Block(new Properties("air").visible(false));
public static final Block DIRT = new Block("block/cubes/soil/dirt.png", new Properties("dirt"));
public static final Block STONE = new Block("block/cubes/stone/basic/gneiss.png", new Properties("stone"));
public static class Properties
{ // add properties to this builder!
private boolean visible = true;

View File

@ -0,0 +1,10 @@
package com.github.halotroop.litecraft.types.block;
import com.github.halotroop.litecraft.types.block.Block.Properties;
public final class Blocks
{
public static final Block AIR = new Block(new Properties("air").visible(false));
public static final Block DIRT = new Block("block/cubes/soil/dirt.png", new Properties("dirt"));
public static final Block STONE = new Block("block/cubes/stone/basic/gneiss.png", new Properties("stone"));
}

View File

@ -30,8 +30,9 @@ public class Chunk implements BlockAccess, WorldGenConstants, DataStorage
public final int chunkX, chunkY, chunkZ;
public final int chunkStartX, chunkStartY, chunkStartZ;
private boolean fullyGenerated = false;
public final int dimension;
public Chunk(int chunkX, int chunkY, int chunkZ)
public Chunk(int chunkX, int chunkY, int chunkZ, int dimension)
{
renderList = new ArrayList<BlockEntity>();
this.chunkX = chunkX;
@ -40,6 +41,7 @@ public class Chunk implements BlockAccess, WorldGenConstants, DataStorage
this.chunkStartX = chunkX << POS_SHIFT;
this.chunkStartY = chunkY << POS_SHIFT;
this.chunkStartZ = chunkZ << POS_SHIFT;
this.dimension = dimension;
}
public boolean doRender()

View File

@ -1,7 +1,9 @@
package com.github.halotroop.litecraft.world;
import java.util.Random;
import java.util.function.LongConsumer;
import com.github.halotroop.litecraft.save.LitecraftSave;
import com.github.halotroop.litecraft.types.block.Block;
import com.github.halotroop.litecraft.world.block.BlockRenderer;
import com.github.halotroop.litecraft.world.gen.*;
@ -18,31 +20,48 @@ public class World implements BlockAccess, WorldGenConstants
private final WorldModifier[] worldModifiers;
private final ChunkGenerator chunkGenerator;
private final BlockAccess genBlockAccess;
private final LitecraftSave save;
private final long seed;
private final int dimension;
public Player player;
public World(long seed, int size, Dimension<?> dim)
// This will likely become the main public constructor after we add dynamic chunkloading
private World(long seed, Dimension<?> dim, LitecraftSave save)
{
this.chunks = new Long2ObjectArrayMap<>();
this.seed = seed;
this.chunkGenerator = dim.createChunkGenerator(seed);
this.worldModifiers = dim.getWorldModifierArray();
this.genBlockAccess = new GenWorld(this);
this.save = save;
this.dimension = dim.id;
}
public void spawnPlayer()
{
TexturedModel dirtModel = ModelLoader.loadGenericCube("block/cubes/soil/dirt.png");
this.player = new Player(dirtModel, new Vector3f(0, 0, -3), 0, 180f, 0, new Vector3f(0.2f, 0.2f, 0.2f));
}
// this constructor will likely not be neccesary when we have dynamic chunkloading
public World(long seed, int size, Dimension<?> dim, LitecraftSave save)
{
this(seed, dim, save);
for (int i = (0 - (size/2)); i < (size/2); i++)
for (int k = (0 - (size/2)); k < (size/2); k++)
for (int y = -2; y < 0; ++y)
this.getChunk(i, y, k).setRender(true);
TexturedModel dirtModel = ModelLoader.loadGenericCube("block/cubes/soil/dirt.png");
this.player = new Player(dirtModel, new Vector3f(0, 0, -3), 0, 180f, 0, new Vector3f(0.2f, 0.2f, 0.2f));
}
public Chunk getChunk(int chunkX, int chunkY, int chunkZ)
{
Chunk chunk = this.chunks.computeIfAbsent(posHash(chunkX, chunkY, chunkZ), pos -> this.chunkGenerator.generateChunk(chunkX, chunkY, chunkZ));
Chunk chunk = this.chunks.computeIfAbsent(posHash(chunkX, chunkY, chunkZ), pos -> {
Chunk readChunk = save.readChunk(chunkX, chunkY, chunkZ, this.dimension);
return readChunk == null ? this.chunkGenerator.generateChunk(chunkX, chunkY, chunkZ) : readChunk;
});
if (chunk.isFullyGenerated()) return chunk;
@ -51,6 +70,23 @@ public class World implements BlockAccess, WorldGenConstants
return chunk;
}
/**
* @return whether the chunk was unloaded without errors. Will often, but not always, be equal to whether the chunk was already in memory.
*/
public boolean unloadChunk(int chunkX, int chunkY, int chunkZ)
{
long posHash = posHash(chunkX, chunkY, chunkZ);
Chunk chunk = this.chunks.get(posHash);
// If the chunk is not in memory, it does not need to be unloaded
if (chunk == null) return false;
// Otherwise save the chunk
boolean result = this.save.saveChunk(chunk);
this.chunks.remove(posHash);
return result;
}
private void populateChunk(int chunkX, int chunkY, int chunkZ, int chunkStartX, int chunkStartY, int chunkStartZ)
{
Random rand = new Random(this.seed + 5828671L * (long) chunkX + -47245139L * (long) chunkY + 8972357 * (long) chunkZ);
@ -90,4 +126,16 @@ public class World implements BlockAccess, WorldGenConstants
public void render(BlockRenderer blockRenderer)
{ this.chunks.forEach((pos, chunk) -> chunk.render(blockRenderer)); }
public void unloadAllChunks()
{
LongList chunkPositions = new LongArrayList();
this.chunks.forEach((pos, chunk) -> { // for every chunk in memory
chunkPositions.add((long) pos); // add pos to chunk positions list for removal later
this.save.saveChunk(chunk); // save chunk
});
chunkPositions.forEach((LongConsumer) (pos -> this.chunks.remove(pos))); // remove all chunks
}
}

View File

@ -2,13 +2,20 @@ package com.github.halotroop.litecraft.world.gen;
import java.util.*;
import com.github.halotroop.litecraft.world.Chunk;
import it.unimi.dsi.fastutil.ints.*;
public abstract class Dimension<T extends ChunkGenerator>
{
public List<WorldModifier> worldModifiers = new ArrayList<>();
public final int id;
public Dimension addWorldModifier(WorldModifier modifier)
public Dimension(int id)
{
this.id = id;
ID_TO_DIMENSION.put(id, this);
}
public Dimension<T> addWorldModifier(WorldModifier modifier)
{
this.worldModifiers.add(modifier);
return this;
@ -20,5 +27,5 @@ public abstract class Dimension<T extends ChunkGenerator>
public abstract T createChunkGenerator(long seed);
public static final Dimension OVERWORLD = new OverworldDimension();
private static final Int2ObjectMap<Dimension<?>> ID_TO_DIMENSION = new Int2ObjectArrayMap<>();
}

View File

@ -0,0 +1,6 @@
package com.github.halotroop.litecraft.world.gen;
public final class Dimensions
{
public static final Dimension<OverworldChunkGenerator> OVERWORLD = new OverworldDimension(0);
}

View File

@ -2,23 +2,25 @@ package com.github.halotroop.litecraft.world.gen;
import java.util.Random;
import com.github.halotroop.litecraft.types.block.Block;
import com.github.halotroop.litecraft.types.block.*;
import com.github.halotroop.litecraft.util.noise.OctaveSimplexNoise;
import com.github.halotroop.litecraft.world.Chunk;
public class OverworldChunkGenerator implements ChunkGenerator, WorldGenConstants
{
public OverworldChunkGenerator(long seed)
public OverworldChunkGenerator(long seed, int dimension)
{
this.noise = new OctaveSimplexNoise(new Random(seed), 3, 250.0, 35.0, 10.0);
this.dimension = dimension;
}
private final OctaveSimplexNoise noise;
private final int dimension;
@Override
public Chunk generateChunk(int chunkX, int chunkY, int chunkZ)
{
Chunk chunk = new Chunk(chunkX, chunkY, chunkZ);
Chunk chunk = new Chunk(chunkX, chunkY, chunkZ, this.dimension);
for (int x = 0; x < CHUNK_SIZE; ++x) {
double totalX = x + chunk.chunkStartX;
@ -28,15 +30,15 @@ public class OverworldChunkGenerator implements ChunkGenerator, WorldGenConstant
for (int y = 0; y < CHUNK_SIZE; ++y) {
int totalY = chunk.chunkStartY + y;
Block block = Block.AIR;
Block block = Blocks.AIR;
if (totalY < height - 3)
{
block = Block.DIRT;
block = Blocks.DIRT;
}
else if (totalY < height)
{
block = Block.STONE;
block = Blocks.STONE;
}
chunk.setBlock(x, y, z, block);

View File

@ -2,9 +2,14 @@ package com.github.halotroop.litecraft.world.gen;
public class OverworldDimension extends Dimension<OverworldChunkGenerator>
{
public OverworldDimension(int id)
{
super(id);
}
@Override
public OverworldChunkGenerator createChunkGenerator(long seed)
{
return new OverworldChunkGenerator(seed);
return new OverworldChunkGenerator(seed, this.id);
}
}