Added world generation hints, altered ChunkIO slightly, fixed some bugs

- A custom Object can be appended to any ChunkData to save its
generation status
  - Handled by WorldGenerator
  - Persistent
  - Used to determine whether a chunk is ready to be sent to client
- ChunkIO now requires an IOConext
- ChunkIO now requires Data{In,Out}putStreams (as a combination of
Data{In,Out}put and {In,Out}putStream)
- Fixed some minor bugs
This commit is contained in:
OLEGSHA 2021-01-01 23:06:32 +03:00
parent 77599e857d
commit 06f4b4637d
15 changed files with 291 additions and 129 deletions

View File

@ -1,10 +1,11 @@
package ru.windcorp.progressia.common.io;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.state.IOContext;
import ru.windcorp.progressia.common.util.namespaces.Namespaced;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.DecodingException;
@ -27,10 +28,10 @@ public abstract class ChunkCodec extends Namespaced {
return signature;
}
public abstract ChunkData decode(WorldData world, Vec3i position, InputStream data) throws DecodingException, IOException;
public abstract ChunkData decode(WorldData world, Vec3i position, DataInputStream input, IOContext context) throws DecodingException, IOException;
public abstract boolean shouldEncode(ChunkData chunk);
public abstract boolean shouldEncode(ChunkData chunk, IOContext context);
public abstract void encode(ChunkData chunk, OutputStream output) throws IOException;
public abstract void encode(ChunkData chunk, DataOutputStream output, IOContext context) throws IOException;
}

View File

@ -1,9 +1,9 @@
package ru.windcorp.progressia.common.io;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -11,6 +11,7 @@ import java.util.List;
import glm.vec._3.i.Vec3i;
import gnu.trove.map.TByteObjectMap;
import gnu.trove.map.hash.TByteObjectHashMap;
import ru.windcorp.progressia.common.state.IOContext;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.DecodingException;
@ -21,7 +22,7 @@ public class ChunkIO {
private static final TByteObjectMap<ChunkCodec> CODECS_BY_ID = new TByteObjectHashMap<>();
private static final List<ChunkCodec> CODECS_BY_PRIORITY = new ArrayList<>();
public static ChunkData load(WorldData world, Vec3i position, InputStream data)
public static ChunkData load(WorldData world, Vec3i position, DataInputStream data, IOContext context)
throws DecodingException, IOException
{
if (CODECS_BY_ID.isEmpty()) throw new IllegalStateException("No codecs registered");
@ -35,7 +36,7 @@ public class ChunkIO {
}
try {
return codec.decode(world, position, data);
return codec.decode(world, position, data, context);
} catch (IOException | DecodingException e) {
throw e;
} catch (Throwable t) {
@ -47,14 +48,14 @@ public class ChunkIO {
}
}
public static void save(ChunkData chunk, OutputStream output)
public static void save(ChunkData chunk, DataOutputStream output, IOContext context)
throws IOException
{
ChunkCodec codec = getCodec(chunk);
ChunkCodec codec = getCodec(chunk, context);
try {
output.write(codec.getSignature());
codec.encode(chunk, output);
codec.encode(chunk, output, context);
} catch (IOException e) {
throw e;
} catch (Throwable t) {
@ -70,9 +71,9 @@ public class ChunkIO {
return CODECS_BY_ID.get(signature);
}
public static ChunkCodec getCodec(ChunkData chunk) {
public static ChunkCodec getCodec(ChunkData chunk, IOContext context) {
for (ChunkCodec codec : CODECS_BY_PRIORITY) {
if (codec.shouldEncode(chunk)) {
if (codec.shouldEncode(chunk, context)) {
return codec;
}
}
@ -82,7 +83,7 @@ public class ChunkIO {
}
/**
* Sorted is order of decreasing priority
* Sorted in order of decreasing priority
* @return
*/
public static List<ChunkCodec> getCodecs() {

View File

@ -38,8 +38,8 @@ public class DataBuffer {
}
};
private final DataInput reader = new DataInputStream(inputStream);
private final DataOutput writer = new DataOutputStream(outputStream);
private final DataInputStream reader = new DataInputStream(inputStream);
private final DataOutputStream writer = new DataOutputStream(outputStream);
public DataBuffer(int capacity) {
this.buffer = new TByteArrayList(capacity);
@ -53,25 +53,23 @@ public class DataBuffer {
this.buffer = new TByteArrayList(copyFrom.buffer);
}
public InputStream getInputStream() {
position = 0;
return inputStream;
}
public OutputStream getOutputStream() {
buffer.resetQuick();
return outputStream;
}
public DataInput getReader() {
public DataInputStream getReader() {
position = 0;
return reader;
}
public DataOutput getWriter() {
public InputStream getInputStream() {
return getReader();
}
public DataOutputStream getWriter() {
buffer.resetQuick();
return writer;
}
public OutputStream getOutputStream() {
return getWriter();
}
public int getSize() {
return buffer.size();

View File

@ -59,6 +59,8 @@ implements GenericChunk<
BLOCK_FACE_COUNT
];
private Object generationHint = null;
private final Collection<ChunkDataListener> listeners =
Collections.synchronizedCollection(new ArrayList<>());
@ -238,6 +240,14 @@ implements GenericChunk<
getListeners().forEach(l -> l.beforeChunkUnloaded(this));
}
public Object getGenerationHint() {
return generationHint;
}
public void setGenerationHint(Object generationHint) {
this.generationHint = generationHint;
}
/**
* Implementation of {@link TileDataStack} used internally by {@link ChunkData} to
* actually store the tiles. This is basically an array wrapper with reporting

View File

@ -6,6 +6,7 @@ import java.io.IOException;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.io.ChunkIO;
import ru.windcorp.progressia.common.state.IOContext;
import ru.windcorp.progressia.common.util.DataBuffer;
import ru.windcorp.progressia.common.util.crash.CrashReports;
@ -26,7 +27,7 @@ public class PacketSendChunk extends PacketChunkChange {
this.position.set(chunk.getX(), chunk.getY(), chunk.getZ());
try {
ChunkIO.save(chunk, this.data.getOutputStream());
ChunkIO.save(chunk, this.data.getWriter(), IOContext.COMMS);
} catch (IOException e) {
// Impossible
}
@ -50,7 +51,7 @@ public class PacketSendChunk extends PacketChunkChange {
@Override
public void apply(WorldData world) {
try {
world.addChunk(ChunkIO.load(world, position, data.getInputStream()));
world.addChunk(ChunkIO.load(world, position, data.getReader(), IOContext.COMMS));
} catch (DecodingException | IOException e) {
CrashReports.report(e, "Could not load chunk");
}

View File

@ -101,8 +101,8 @@ public interface GenericWorld<
return stack.get(layer);
}
default boolean isChunkLoaded(Vec3i pos) {
return getChunk(pos) != null;
default boolean isChunkLoaded(Vec3i chunkPos) {
return getChunk(chunkPos) != null;
}
default boolean isBlockLoaded(Vec3i blockInWorld) {

View File

@ -33,9 +33,12 @@ public class ChunkManager {
public void updateQueues(Player player) {
toSend.clear();
toSend.addAll(requested);
toSend.removeAll(visible);
toSend.retainAll(loaded);
requested.forEachIn(server.getWorld(), chunk -> {
if (!chunk.isReady()) return;
if (visible.contains(chunk)) return;
toSend.add(chunk);
});
toRevoke.clear();
toRevoke.addAll(visible);
@ -120,13 +123,13 @@ public class ChunkManager {
WorldData world = getServer().getWorld().getData();
ChunkData chunk = TestWorldDiskIO.tryToLoad(chunkPos, world);
if (chunk == null) {
chunk = getServer().getWorld().generate(chunkPos);
ChunkData chunk = TestWorldDiskIO.tryToLoad(chunkPos, world, getServer());
if (chunk != null) {
world.addChunk(chunk);
} else {
getServer().getWorld().generate(chunkPos);
}
world.addChunk(chunk);
}
public void unloadChunk(Vec3i chunkPos) {
@ -143,7 +146,7 @@ public class ChunkManager {
world.removeChunk(chunk);
TestWorldDiskIO.saveChunk(chunk);
TestWorldDiskIO.saveChunk(chunk, getServer());
}

View File

@ -56,8 +56,8 @@ public class ServerThread implements Runnable {
try {
server.tick();
ticker.runOneTick();
} catch (Exception e) {
CrashReports.report(e, "Got an exception in the server thread");
} catch (Throwable e) {
CrashReports.report(e, "Got a throwable in the server thread");
}
}

View File

@ -4,8 +4,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.logging.log4j.LogManager;
import glm.vec._3.i.Vec3i;
import gnu.trove.TCollections;
import gnu.trove.map.TIntObjectMap;
@ -65,7 +63,6 @@ public class ClientManager {
getServer().getPlayerManager().getPlayers().add(player);
PacketSetLocalPlayer packet = new PacketSetLocalPlayer();
LogManager.getLogger().info("Sending local player ID {}", EntityData.formatEntityId(entity.getEntityId()));
packet.set(entity.getEntityId());
client.sendPacket(packet);
}

View File

@ -86,6 +86,10 @@ public class ChunkLogic implements GenericChunk<
return data;
}
public boolean isReady() {
return getWorld().getGenerator().isChunkReady(getData().getGenerationHint());
}
public boolean hasTickingBlocks() {
return !tickingBlocks.isEmpty();
}

View File

@ -0,0 +1,48 @@
package ru.windcorp.progressia.server.world.generation;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Objects;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.DecodingException;
public abstract class AbstractWorldGenerator<H> extends WorldGenerator {
private final Class<H> hintClass;
public AbstractWorldGenerator(String id, Class<H> hintClass) {
super(id);
this.hintClass = Objects.requireNonNull(hintClass, "hintClass");
}
@Override
public final Object readGenerationHint(DataInputStream input) throws IOException, DecodingException {
return doReadGenerationHint(input);
}
@Override
public final void writeGenerationHint(DataOutputStream output, Object hint) throws IOException {
doWriteGenerationHint(output, hintClass.cast(hint));
}
protected abstract H doReadGenerationHint(DataInputStream input) throws IOException, DecodingException;
protected abstract void doWriteGenerationHint(DataOutputStream output, H hint) throws IOException;
@Override
public final boolean isChunkReady(Object hint) {
return checkIsChunkReady(hintClass.cast(hint));
}
protected abstract boolean checkIsChunkReady(H hint);
protected H getHint(ChunkData chunk) {
return hintClass.cast(chunk.getGenerationHint());
}
protected void setHint(ChunkData chunk, H hint) {
chunk.setGenerationHint(hint);
}
}

View File

@ -1,16 +1,25 @@
package ru.windcorp.progressia.server.world.generation;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.namespaces.Namespaced;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.DecodingException;
import ru.windcorp.progressia.common.world.WorldData;
public abstract class WorldGenerator extends Namespaced {
public WorldGenerator(String id) {
WorldGenerator(String id) {
super(id);
// package-private constructor; extend AbstractWorldGeneration
}
public abstract ChunkData generate(Vec3i chunkPos, WorldData world);
public abstract Object readGenerationHint(DataInputStream input) throws IOException, DecodingException;
public abstract void writeGenerationHint(DataOutputStream output, Object hint) throws IOException;
public abstract boolean isChunkReady(Object hint);
}

View File

@ -15,5 +15,11 @@ public abstract class CachedChunkChange<P extends PacketChunkChange> extends Cac
public void getRelevantChunk(Vec3i output) {
getPacket().getAffectedChunk(output);
}
@Override
protected Vec3i getAffectedChunk(Vec3i output) {
getPacket().getAffectedChunk(output);
return output;
}
}

View File

@ -5,8 +5,6 @@ import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
@ -17,6 +15,7 @@ import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import ru.windcorp.jputil.functions.ThrowingConsumer;
import ru.windcorp.progressia.common.io.ChunkCodec;
import ru.windcorp.progressia.common.state.IOContext;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.DecodingException;
import ru.windcorp.progressia.common.world.WorldData;
@ -57,7 +56,7 @@ public class TestChunkCodec extends ChunkCodec {
}
@Override
public boolean shouldEncode(ChunkData chunk) {
public boolean shouldEncode(ChunkData chunk, IOContext context) {
return true;
}
@ -66,9 +65,7 @@ public class TestChunkCodec extends ChunkCodec {
*/
@Override
public ChunkData decode(WorldData world, Vec3i position, InputStream inputStream) throws DecodingException, IOException {
DataInput input = new DataInputStream(inputStream);
public ChunkData decode(WorldData world, Vec3i position, DataInputStream input, IOContext context) throws DecodingException, IOException {
BlockData[] blockPalette = readBlockPalette(input);
TileData[] tilePalette = readTilePalette(input);
@ -136,10 +133,7 @@ public class TestChunkCodec extends ChunkCodec {
*/
@Override
public void encode(ChunkData chunk, OutputStream outputStream) throws IOException {
DataOutput output = new DataOutputStream(outputStream);
public void encode(ChunkData chunk, DataOutputStream output, IOContext context) throws IOException {
Palette<BlockData> blockPalette = createBlockPalette(chunk);
Palette<TileData> tilePalette = createTilePalette(chunk);

View File

@ -1,8 +1,16 @@
package ru.windcorp.progressia.test.gen;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Random;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.VectorUtil;
import ru.windcorp.progressia.common.util.Vectors;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.Coordinates;
import ru.windcorp.progressia.common.world.DecodingException;
import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.block.BlockData;
import ru.windcorp.progressia.common.world.block.BlockDataRegistry;
@ -10,28 +18,46 @@ import ru.windcorp.progressia.common.world.block.BlockFace;
import ru.windcorp.progressia.common.world.tile.TileData;
import ru.windcorp.progressia.common.world.tile.TileDataRegistry;
import ru.windcorp.progressia.server.world.WorldLogic;
import ru.windcorp.progressia.server.world.generation.WorldGenerator;
public class TestWorldGenerator extends WorldGenerator {
import ru.windcorp.progressia.server.world.generation.AbstractWorldGenerator;
public class TestWorldGenerator extends AbstractWorldGenerator<Boolean> {
public TestWorldGenerator(WorldLogic world) {
super("Test:WorldGenerator");
super("Test:WorldGenerator", Boolean.class);
}
@Override
protected Boolean doReadGenerationHint(DataInputStream input) throws IOException, DecodingException {
return input.readBoolean();
}
@Override
protected void doWriteGenerationHint(DataOutputStream output, Boolean hint) throws IOException {
output.writeBoolean(hint);
}
@Override
protected boolean checkIsChunkReady(Boolean hint) {
return hint;
}
@Override
public ChunkData generate(Vec3i chunkPos, WorldData world) {
ChunkData chunk = generateUnpopulated(chunkPos, world);
world.addChunk(chunk);
findAndPopulate(chunkPos, world);
return chunk;
}
private ChunkData generateUnpopulated(Vec3i chunkPos, WorldData world) {
ChunkData chunk = new ChunkData(chunkPos, world);
chunk.setGenerationHint(false);
final int bpc = ChunkData.BLOCKS_PER_CHUNK;
BlockData dirt = BlockDataRegistry.getInstance().get("Test:Dirt");
BlockData stone = BlockDataRegistry.getInstance().get("Test:Stone");
BlockData air = BlockDataRegistry.getInstance().get("Test:Air");
TileData grass = TileDataRegistry.getInstance().get("Test:Grass");
TileData stones = TileDataRegistry.getInstance().get("Test:Stones");
TileData flowers = TileDataRegistry.getInstance().get("Test:YellowFlowers");
TileData sand = TileDataRegistry.getInstance().get("Test:Sand");
final float maxHeight = 32;
final float rho = 2000;
@ -48,70 +74,134 @@ public class TestWorldGenerator extends WorldGenerator {
}
}
Vec3i pos = new Vec3i();
for (pos.x = 0; pos.x < bpc; ++pos.x) {
for (pos.y = 0; pos.y < bpc; ++pos.y) {
for (pos.z = 0; pos.z < bpc; ++pos.z) {
int layer = pos.z - heightMap[pos.x][pos.y];
if (layer < -4) {
chunk.setBlock(pos, stone, false);
} else if (layer < 0) {
chunk.setBlock(pos, dirt, false);
} else {
chunk.setBlock(pos, air, false);
}
}
VectorUtil.iterateCuboid(0, 0, 0, bpc, bpc, bpc, pos -> {
int layer = pos.z - heightMap[pos.x][pos.y];
if (layer < -4) {
chunk.setBlock(pos, stone, false);
} else if (layer < 0) {
chunk.setBlock(pos, dirt, false);
} else {
chunk.setBlock(pos, air, false);
}
}
for (int x = 0; x < bpc; ++x) {
for (int y = 0; y < bpc; ++y) {
// int z = heightMap[x][y];
for (int z = 0; z < bpc; ++z) {
pos.set(x, y, z);
int layer = pos.z - heightMap[x][y];
if (layer == -1) {
chunk.getTiles(pos, BlockFace.TOP).add(grass);
for (BlockFace face : BlockFace.getFaces()) {
if (face.getVector().z != 0) continue;
pos.add(face.getVector());
if (!ChunkData.isInBounds(pos) || (chunk.getBlock(pos) == air)) {
pos.sub(face.getVector());
chunk.getTiles(pos, face).add(grass);
} else {
pos.sub(face.getVector());
}
}
int hash = x*x * 19 ^ y*y * 41 ^ pos.z*pos.z * 147;
if (hash % 5 == 0) {
chunk.getTiles(pos, BlockFace.TOP).addFarthest(sand);
}
hash = x*x * 13 ^ y*y * 37 ^ pos.z*pos.z * 129;
if (hash % 5 == 0) {
chunk.getTiles(pos, BlockFace.TOP).addFarthest(stones);
}
hash = x*x * 17 ^ y*y * 39 ^ pos.z*pos.z * 131;
if (hash % 9 == 0) {
chunk.getTiles(pos, BlockFace.TOP).addFarthest(flowers);
}
}
}
}
}
});
return chunk;
}
private void findAndPopulate(Vec3i changePos, WorldData world) {
VectorUtil.iterateCuboidAround(changePos, 3, candidatePos -> {
if (canBePopulated(candidatePos, world)) {
populate(candidatePos, world);
}
});
}
private boolean canBePopulated(Vec3i candidatePos, WorldData world) {
Vec3i cursor = Vectors.grab3i();
ChunkData candidate = world.getChunk(candidatePos);
if (candidate == null || isChunkReady(candidate.getGenerationHint())) return false;
for (int dx = -1; dx <= 1; ++dx) {
cursor.x = candidatePos.x + dx;
for (int dy = -1; dy <= 1; ++dy) {
cursor.y = candidatePos.y + dy;
for (int dz = -1; dz <= 1; ++dz) {
if ((dx | dy | dz) == 0) continue;
cursor.z = candidatePos.z + dz;
ChunkData chunk = world.getChunk(cursor);
if (chunk == null) {
return false;
}
}
}
}
Vectors.release(cursor);
return true;
}
private void populate(Vec3i chunkPos, WorldData world) {
Random random = new Random(chunkPos.x + chunkPos.y + chunkPos.z);
ChunkData chunk = world.getChunk(chunkPos);
assert chunk != null : "Something went wrong when populating chunk at (" + chunkPos.x + "; " + chunkPos.y + "; " + chunkPos.z + ")";
BlockData air = BlockDataRegistry.getInstance().get("Test:Air");
Vec3i biw = new Vec3i();
int minX = Coordinates.getInWorld(chunkPos.x, 0);
int maxX = Coordinates.getInWorld(chunkPos.x + 1, 0);
int minY = Coordinates.getInWorld(chunkPos.y, 0);
int maxY = Coordinates.getInWorld(chunkPos.y + 1, 0);
int minZ = Coordinates.getInWorld(chunkPos.z, 0);
int maxZ = Coordinates.getInWorld(chunkPos.z + 1, 0);
for (biw.x = minX; biw.x < maxX; ++biw.x) {
for (biw.y = minY; biw.y < maxY; ++biw.y) {
for (biw.z = minZ; biw.z < maxZ + 1 && world.getBlock(biw) != air; ++biw.z);
biw.z -= 1;
if (biw.z == maxZ) continue;
if (biw.z < minZ) continue;
addTiles(chunk, biw, world, random);
}
}
chunk.setGenerationHint(true);
}
private void addTiles(ChunkData chunk, Vec3i biw, WorldData world, Random random) {
addGrass(chunk, biw, world, random);
addDecor(chunk, biw, world, random);
}
private void addGrass(ChunkData chunk, Vec3i biw, WorldData world, Random random) {
BlockData air = BlockDataRegistry.getInstance().get("Test:Air");
TileData grass = TileDataRegistry.getInstance().get("Test:Grass");
world.getTiles(biw, BlockFace.TOP).add(grass);
for (BlockFace face : BlockFace.getFaces()) {
if (face.getVector().z != 0) continue;
biw.add(face.getVector());
if (world.getBlock(biw) == air) {
biw.sub(face.getVector());
world.getTiles(biw, face).add(grass);
} else {
biw.sub(face.getVector());
}
}
}
private void addDecor(ChunkData chunk, Vec3i biw, WorldData world, Random random) {
if (random.nextInt(8) == 0) {
world.getTiles(biw, BlockFace.TOP).addFarthest(
TileDataRegistry.getInstance().get("Test:Sand")
);
}
if (random.nextInt(8) == 0) {
world.getTiles(biw, BlockFace.TOP).addFarthest(
TileDataRegistry.getInstance().get("Test:Stones")
);
}
if (random.nextInt(8) == 0) {
world.getTiles(biw, BlockFace.TOP).addFarthest(
TileDataRegistry.getInstance().get("Test:YellowFlowers")
);
}
}
}