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 # BlueJ files
*.ctxt *.ctxt
# run
saves/
# Mobile Tools for Java (J2ME) # Mobile Tools for Java (J2ME)
.mtj.tmp/ .mtj.tmp/

View File

@ -4,9 +4,11 @@ import java.util.Random;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import com.github.halotroop.litecraft.save.LitecraftSave;
import com.github.halotroop.litecraft.screens.TitleScreen; 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.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.*;
import com.github.hydos.ginger.engine.api.game.*; import com.github.hydos.ginger.engine.api.game.*;
import com.github.hydos.ginger.engine.cameras.*; import com.github.hydos.ginger.engine.cameras.*;
@ -26,6 +28,7 @@ import tk.valoeghese.gateways.client.io.*;
public class Litecraft extends Game public class Litecraft extends Game
{ {
private World world; private World world;
private LitecraftSave save;
private Ginger ginger3D; private Ginger ginger3D;
private static Litecraft INSTANCE; private static Litecraft INSTANCE;
@ -51,6 +54,8 @@ public class Litecraft extends Game
setupKeybinds(); setupKeybinds();
Block b = Blocks.AIR; // make sure blocks are initialised
GingerUtils.init(); GingerUtils.init();
Window.setBackgroundColour(0.2f, 0.2f, 0.6f); Window.setBackgroundColour(0.2f, 0.2f, 0.6f);
TexturedModel dirtModel = ModelLoader.loadGenericCube("block/cubes/stone/brick/stonebrick.png"); TexturedModel dirtModel = ModelLoader.loadGenericCube("block/cubes/stone/brick/stonebrick.png");
@ -89,6 +94,7 @@ public class Litecraft extends Game
@Override @Override
public void exit() public void exit()
{ {
this.world.unloadAllChunks();
ginger3D.cleanup(); ginger3D.cleanup();
System.exit(0); System.exit(0);
} }
@ -146,7 +152,8 @@ public class Litecraft extends Game
public void onPlayButtonClick() { public void onPlayButtonClick() {
if (world == null) 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; package com.github.halotroop.litecraft.save;
import java.io.*; 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 final class LitecraftSave
{ {
public LitecraftSave(String name, boolean mustCreateNew) public LitecraftSave(String name, boolean mustCreateNew)
{ {
StringBuilder sb = new StringBuilder(SAVE_DIR).append(name); StringBuilder sb = new StringBuilder(SAVE_DIR).append(name);
File saveFile = new File(sb.toString()); File saveDir = new File(sb.toString());
if (mustCreateNew) if (mustCreateNew)
{ {
while (saveFile.exists()) while (saveDir.exists())
{ {
sb.append('_'); sb.append('_'); // append "_" to the save name until we get a unique save, if we must create a new save
saveFile = new File(sb.toString()); saveDir = new File(sb.toString());
} }
} }
this.file = saveFile; this.file = saveDir;
this.file.mkdirs(); this.file.mkdirs();
} }
private final File file; private final File file;
public void saveChunk(Chunk chunk) public boolean saveChunk(Chunk chunk)
{ {
StringBuilder fileLocBuilder = new StringBuilder(this.file.getPath()) StringBuilder fileLocBuilder = new StringBuilder(this.file.getPath())
.append('/').append(chunk.dimension)
.append('/').append(chunk.chunkX) .append('/').append(chunk.chunkX)
.append('/').append(chunk.chunkZ); .append('/').append(chunk.chunkZ);
File chunkDir = new File(fileLocBuilder.toString()); 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()); File chunkFile = new File(fileLocBuilder.append('/').append(chunk.chunkY).append(".sod").toString());
try try
{ {
chunkFile.createNewFile(); chunkFile.createNewFile();
BinaryData data = new BinaryData(); BinaryData data = new BinaryData(); // create new empty binary data
chunk.write(data); chunk.write(data); // write the chunk info to the binary data
data.write(chunkFile); return data.write(chunkFile); // write the data to the file, return whether an io exception occurred
} }
catch (IOException e) catch (IOException e)
{ {
e.printStackTrace(); 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()) File chunkFile = new File(new StringBuilder(this.file.getPath())
.append('/').append(dimension)
.append('/').append(chunkX) .append('/').append(chunkX)
.append('/').append(chunkZ) .append('/').append(chunkZ)
.append('/').append(chunkY).append(".sod").toString()); .append('/').append(chunkY).append(".sod").toString());
@ -62,12 +70,60 @@ public final class LitecraftSave
{ {
BinaryData data = BinaryData.read(chunkFile); BinaryData data = BinaryData.read(chunkFile);
Chunk result = new Chunk(chunkX, chunkY, chunkZ); Chunk result = new Chunk(chunkX, chunkY, chunkZ, dimension); // create chunk
result.read(data); result.read(data); // load the chunk data we have just read into the chunk
return result; return result;
} }
else return null; 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/"; 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 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 public static class Properties
{ // add properties to this builder! { // add properties to this builder!
private boolean visible = true; 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 chunkX, chunkY, chunkZ;
public final int chunkStartX, chunkStartY, chunkStartZ; public final int chunkStartX, chunkStartY, chunkStartZ;
private boolean fullyGenerated = false; 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>(); renderList = new ArrayList<BlockEntity>();
this.chunkX = chunkX; this.chunkX = chunkX;
@ -40,6 +41,7 @@ public class Chunk implements BlockAccess, WorldGenConstants, DataStorage
this.chunkStartX = chunkX << POS_SHIFT; this.chunkStartX = chunkX << POS_SHIFT;
this.chunkStartY = chunkY << POS_SHIFT; this.chunkStartY = chunkY << POS_SHIFT;
this.chunkStartZ = chunkZ << POS_SHIFT; this.chunkStartZ = chunkZ << POS_SHIFT;
this.dimension = dimension;
} }
public boolean doRender() public boolean doRender()

View File

@ -1,7 +1,9 @@
package com.github.halotroop.litecraft.world; package com.github.halotroop.litecraft.world;
import java.util.Random; 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.types.block.Block;
import com.github.halotroop.litecraft.world.block.BlockRenderer; import com.github.halotroop.litecraft.world.block.BlockRenderer;
import com.github.halotroop.litecraft.world.gen.*; import com.github.halotroop.litecraft.world.gen.*;
@ -18,31 +20,48 @@ public class World implements BlockAccess, WorldGenConstants
private final WorldModifier[] worldModifiers; private final WorldModifier[] worldModifiers;
private final ChunkGenerator chunkGenerator; private final ChunkGenerator chunkGenerator;
private final BlockAccess genBlockAccess; private final BlockAccess genBlockAccess;
private final LitecraftSave save;
private final long seed; private final long seed;
private final int dimension;
public Player player; 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.chunks = new Long2ObjectArrayMap<>();
this.seed = seed; this.seed = seed;
this.chunkGenerator = dim.createChunkGenerator(seed); this.chunkGenerator = dim.createChunkGenerator(seed);
this.worldModifiers = dim.getWorldModifierArray(); this.worldModifiers = dim.getWorldModifierArray();
this.genBlockAccess = new GenWorld(this); 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 i = (0 - (size/2)); i < (size/2); i++)
for (int k = (0 - (size/2)); k < (size/2); k++) for (int k = (0 - (size/2)); k < (size/2); k++)
for (int y = -2; y < 0; ++y) for (int y = -2; y < 0; ++y)
this.getChunk(i, y, k).setRender(true); 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) 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; if (chunk.isFullyGenerated()) return chunk;
@ -51,6 +70,23 @@ public class World implements BlockAccess, WorldGenConstants
return chunk; 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) 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); 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) public void render(BlockRenderer blockRenderer)
{ this.chunks.forEach((pos, chunk) -> chunk.render(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 java.util.*;
import com.github.halotroop.litecraft.world.Chunk; import it.unimi.dsi.fastutil.ints.*;
public abstract class Dimension<T extends ChunkGenerator> public abstract class Dimension<T extends ChunkGenerator>
{ {
public List<WorldModifier> worldModifiers = new ArrayList<>(); 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); this.worldModifiers.add(modifier);
return this; return this;
@ -20,5 +27,5 @@ public abstract class Dimension<T extends ChunkGenerator>
public abstract T createChunkGenerator(long seed); 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 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.util.noise.OctaveSimplexNoise;
import com.github.halotroop.litecraft.world.Chunk; import com.github.halotroop.litecraft.world.Chunk;
public class OverworldChunkGenerator implements ChunkGenerator, WorldGenConstants 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.noise = new OctaveSimplexNoise(new Random(seed), 3, 250.0, 35.0, 10.0);
this.dimension = dimension;
} }
private final OctaveSimplexNoise noise; private final OctaveSimplexNoise noise;
private final int dimension;
@Override @Override
public Chunk generateChunk(int chunkX, int chunkY, int chunkZ) 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) { for (int x = 0; x < CHUNK_SIZE; ++x) {
double totalX = x + chunk.chunkStartX; double totalX = x + chunk.chunkStartX;
@ -28,15 +30,15 @@ public class OverworldChunkGenerator implements ChunkGenerator, WorldGenConstant
for (int y = 0; y < CHUNK_SIZE; ++y) { for (int y = 0; y < CHUNK_SIZE; ++y) {
int totalY = chunk.chunkStartY + y; int totalY = chunk.chunkStartY + y;
Block block = Block.AIR; Block block = Blocks.AIR;
if (totalY < height - 3) if (totalY < height - 3)
{ {
block = Block.DIRT; block = Blocks.DIRT;
} }
else if (totalY < height) else if (totalY < height)
{ {
block = Block.STONE; block = Blocks.STONE;
} }
chunk.setBlock(x, y, z, block); 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 class OverworldDimension extends Dimension<OverworldChunkGenerator>
{ {
public OverworldDimension(int id)
{
super(id);
}
@Override @Override
public OverworldChunkGenerator createChunkGenerator(long seed) public OverworldChunkGenerator createChunkGenerator(long seed)
{ {
return new OverworldChunkGenerator(seed); return new OverworldChunkGenerator(seed, this.id);
} }
} }