First implementation of dynamic chunk loading. Super buggy, WIP

This commit is contained in:
OLEGSHA 2020-12-28 11:26:44 +03:00
parent eaea6fdad9
commit cde854346c
18 changed files with 365 additions and 182 deletions

View File

@ -107,15 +107,15 @@ implements GenericChunk<
return data;
}
public void markForUpdate() {
public synchronized void markForUpdate() {
this.needsUpdate = true;
}
public boolean needsUpdate() {
public synchronized boolean needsUpdate() {
return needsUpdate;
}
public void render(ShapeRenderHelper renderer) {
public synchronized void render(ShapeRenderHelper renderer) {
if (model == null || needsUpdate()) {
buildModel();
}

View File

@ -49,7 +49,8 @@ implements GenericWorld<
private final WorldData data;
private final Map<ChunkData, ChunkRender> chunks = new HashMap<>();
private final Map<ChunkData, ChunkRender> chunks =
Collections.synchronizedMap(new HashMap<>());
private final Map<EntityData, EntityRenderable> entityModels =
Collections.synchronizedMap(new WeakHashMap<>());
@ -94,23 +95,18 @@ implements GenericWorld<
}
public void render(ShapeRenderHelper renderer) {
for (ChunkRender chunk : getChunks()) {
chunk.render(renderer);
}
getChunks().forEach(chunk -> chunk.render(renderer));
renderEntities(renderer);
}
private void renderEntities(ShapeRenderHelper renderer) {
FaceCulling.push(false);
for (ChunkRender chunk : getChunks()) {
chunk.getData().forEachEntity(entity -> {
getData().forEachEntity(entity -> {
renderer.pushTransform().translate(entity.getPosition());
getEntityRenderable(entity).render(renderer);
renderer.popTransform();
});
}
FaceCulling.pop();
}

View File

@ -0,0 +1,56 @@
package ru.windcorp.progressia.common.world;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import glm.vec._3.i.Vec3i;
public class PacketRevokeChunk extends PacketChunkChange {
private final Vec3i position = new Vec3i();
public PacketRevokeChunk() {
this("Core:RevokeChunk");
}
protected PacketRevokeChunk(String id) {
super(id);
}
public void set(Vec3i chunkPos) {
this.position.set(chunkPos.x, chunkPos.y, chunkPos.z);
}
@Override
public void read(DataInput input) throws IOException {
this.position.set(input.readInt(), input.readInt(), input.readInt());
}
@Override
public void write(DataOutput output) throws IOException {
output.writeInt(this.position.x);
output.writeInt(this.position.y);
output.writeInt(this.position.z);
}
@Override
public void apply(WorldData world) {
synchronized (world) {
ChunkData chunk = world.getChunk(position);
if (chunk != null) {
world.removeChunk(chunk);
}
}
}
@Override
public void getAffectedChunk(Vec3i output) {
output.set(getPosition().x, getPosition().y, getPosition().z);
}
public Vec3i getPosition() {
return position;
}
}

View File

@ -9,12 +9,16 @@ import ru.windcorp.progressia.common.io.ChunkIO;
import ru.windcorp.progressia.common.util.DataBuffer;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class PacketLoadChunk extends PacketChunkChange {
public class PacketSendChunk extends PacketChunkChange {
private final DataBuffer data = new DataBuffer();
private final Vec3i position = new Vec3i();
public PacketLoadChunk(String id) {
public PacketSendChunk() {
this("Core:SendChunk");
}
protected PacketSendChunk(String id) {
super(id);
}

View File

@ -87,7 +87,7 @@ implements GenericWorld<
}
public void tmp_generate() {
final int size = 10;
final int size = 1;
Vec3i cursor = new Vec3i(0, 0, 0);
for (cursor.x = -(size / 2); cursor.x <= (size / 2); ++cursor.x) {

View File

@ -0,0 +1,22 @@
package ru.windcorp.progressia.common.world.generic;
import gnu.trove.impl.sync.TSynchronizedLongSet;
import gnu.trove.set.hash.TLongHashSet;
public class ChunkSets {
public static ChunkSet newHashSet() {
return new LongBasedChunkSet(new TLongHashSet());
}
public static ChunkSet newSyncHashSet(Object mutex) {
return new LongBasedChunkSet(new TSynchronizedLongSet(new TLongHashSet(), mutex));
}
public static ChunkSet newSyncHashSet() {
return new LongBasedChunkSet(new TSynchronizedLongSet(new TLongHashSet()));
}
private ChunkSets() {}
}

View File

@ -118,8 +118,10 @@ public interface GenericWorld<
*/
default void forEachEntity(Consumer<? super E> action) {
synchronized (this) { // TODO HORRIBLY MUTILATE THE CORPSE OF TROVE4J so that gnu.trove.impl.sync.SynchronizedCollection.forEach is synchronized
getEntities().forEach(action);
}
}
default void forEachEntityIn(Vec3i min, Vec3i max, Consumer<? super E> action) {
forEachEntity(e -> {

View File

@ -1,83 +0,0 @@
package ru.windcorp.progressia.server;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import glm.vec._3.i.Vec3i;
import gnu.trove.set.hash.TLongHashSet;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.generic.ChunkSet;
import ru.windcorp.progressia.common.world.generic.LongBasedChunkSet;
import ru.windcorp.progressia.test.TestContent;
public class ChunkLoadManager {
private final Server server;
private final Collection<Collection<? extends ChunkLoader>> allChunkLoaders =
Collections.synchronizedCollection(new ArrayList<>());
private final ChunkSet requested = new LongBasedChunkSet(new TLongHashSet());
private final ChunkSet toLoad = new LongBasedChunkSet(new TLongHashSet());
private final ChunkSet toUnload = new LongBasedChunkSet(new TLongHashSet());
public ChunkLoadManager(Server server) {
this.server = server;
allChunkLoaders.add(server.getPlayerManager().getPlayers());
}
public void tick() {
gatherRequests();
updateQueues();
processQueues();
}
private void gatherRequests() {
requested.clear();
allChunkLoaders.forEach(collection -> {
collection.forEach(this::gatherRequests);
});
}
private void gatherRequests(ChunkLoader loader) {
loader.requestChunksToLoad(requested::add);
}
private void updateQueues() {
ChunkSet loaded = getServer().getWorld().getData().getLoadedChunks();
toLoad.clear();
toLoad.addAll(requested);
toLoad.removeAll(loaded);
toUnload.clear();
toUnload.addAll(loaded);
toUnload.removeAll(requested);
}
private void processQueues() {
toUnload.forEach(this::unloadChunk);
toLoad.forEach(this::loadChunk);
}
public Server getServer() {
return server;
}
public void loadChunk(Vec3i pos) {
ChunkData chunk = new ChunkData(pos, getServer().getWorld().getData());
TestContent.generateChunk(chunk);
getServer().getWorld().getData().addChunk(chunk);
}
public void unloadChunk(Vec3i pos) {
getServer().getWorld().getData().removeChunk(getServer().getWorld().getData().getChunk(pos));
}
}

View File

@ -0,0 +1,180 @@
package ru.windcorp.progressia.server;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import org.apache.logging.log4j.LogManager;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.generic.ChunkSet;
import ru.windcorp.progressia.common.world.generic.ChunkSets;
import ru.windcorp.progressia.test.TestChunkSender;
import ru.windcorp.progressia.test.TestContent;
public class ChunkManager {
private class PlayerVision {
private final ChunkSet visible = ChunkSets.newSyncHashSet();
private final ChunkSet requested = ChunkSets.newHashSet();
private final ChunkSet toSend = ChunkSets.newHashSet();
private final ChunkSet toRevoke = ChunkSets.newHashSet();
public boolean isChunkVisible(Vec3i chunkPos) {
return visible.contains(chunkPos);
}
public void gatherRequests(Player player) {
requested.clear();
player.requestChunksToLoad(requested::add);
}
public void updateQueues(Player player) {
toSend.clear();
toSend.addAll(requested);
toSend.removeAll(visible);
toSend.retainAll(loaded);
toRevoke.clear();
toRevoke.addAll(visible);
toRevoke.removeIf(v -> loaded.contains(v) && requested.contains(v));
}
public void processQueues(Player player) {
toRevoke.forEach(chunkPos -> revokeChunk(player, chunkPos));
toRevoke.clear();
toSend.forEach(chunkPos -> sendChunk(player, chunkPos));
toSend.clear();
}
}
private final Server server;
private final ChunkSet loaded;
private final ChunkSet requested = ChunkSets.newHashSet();
private final ChunkSet toLoad = ChunkSets.newHashSet();
private final ChunkSet toUnload = ChunkSets.newHashSet();
// TODO replace with a normal Map managed by some sort of PlayerListener, weak maps are weak
private final Map<Player, PlayerVision> visions = Collections.synchronizedMap(new WeakHashMap<>());
public ChunkManager(Server server) {
this.server = server;
this.loaded = server.getWorld().getData().getLoadedChunks();
}
public void tick() {
synchronized (getServer().getWorld().getData()) {
synchronized (visions) {
gatherRequests();
updateQueues();
processQueues();
}
}
}
private void gatherRequests() {
requested.clear();
server.getPlayerManager().getPlayers().forEach(p -> {
PlayerVision vision = getVision(p, true);
vision.gatherRequests(p);
requested.addAll(vision.requested);
});
}
private void updateQueues() {
toLoad.clear();
toLoad.addAll(requested);
toLoad.removeAll(loaded);
toUnload.clear();
toUnload.addAll(loaded);
toUnload.removeAll(requested);
visions.forEach((p, v) -> {
v.updateQueues(p);
});
}
private void processQueues() {
toUnload.forEach(this::unloadChunk);
toUnload.clear();
toLoad.forEach(this::loadChunk);
toLoad.clear();
visions.forEach((p, v) -> {
v.processQueues(p);
});
}
private PlayerVision getVision(Player player, boolean createIfMissing) {
return createIfMissing ? visions.computeIfAbsent(player, k -> new PlayerVision()) : visions.get(player);
}
public void loadChunk(Vec3i chunkPos) {
LogManager.getLogger().info("Loading {} {} {}", chunkPos.x, chunkPos.y, chunkPos.z);
WorldData world = getServer().getWorld().getData();
ChunkData chunk = new ChunkData(chunkPos, world);
TestContent.generateChunk(chunk);
world.addChunk(chunk);
}
public void unloadChunk(Vec3i chunkPos) {
LogManager.getLogger().info("Unloading {} {} {}", chunkPos.x, chunkPos.y, chunkPos.z);
WorldData world = getServer().getWorld().getData();
ChunkData chunk = world.getChunk(chunkPos);
if (chunk == null) {
throw new IllegalStateException(String.format(
"Chunk (%d; %d; %d) not loaded, cannot unload",
chunkPos.x, chunkPos.y, chunkPos.z
));
}
world.removeChunk(chunk);
}
public void sendChunk(Player player, Vec3i chunkPos) {
LogManager.getLogger().info("Sending {} {} {}", chunkPos.x, chunkPos.y, chunkPos.z);
TestChunkSender.sendChunk(server, player.getClient(), chunkPos);
getVision(player, true).visible.add(chunkPos);
}
public void revokeChunk(Player player, Vec3i chunkPos) {
LogManager.getLogger().info("Revoking {} {} {}", chunkPos.x, chunkPos.y, chunkPos.z);
TestChunkSender.revokeChunk(player.getClient(), chunkPos);
PlayerVision vision = getVision(player, false);
if (vision != null) {
vision.visible.remove(chunkPos);
}
}
public boolean isChunkVisible(Vec3i chunkPos, Player player) {
PlayerVision vision = getVision(player, false);
if (vision == null) {
return false;
}
return vision.isChunkVisible(chunkPos);
}
public Server getServer() {
return server;
}
}

View File

@ -18,6 +18,8 @@ public class Player extends PlayerData implements ChunkLoader {
super(entity);
this.server = server;
this.client = client;
client.setPlayer(this);
}
public Server getServer() {
@ -43,6 +45,7 @@ public class Player extends PlayerData implements ChunkLoader {
for (cursor.y = -iRadius; cursor.y <= +iRadius; ++cursor.y) {
for (cursor.z = -iRadius; cursor.z <= +iRadius; ++cursor.z) {
if (cursor.x * cursor.x + cursor.y * cursor.y + cursor.z * cursor.z <= radius) {
cursor.add(start);
chunkConsumer.accept(cursor);
cursor.sub(start);

View File

@ -4,6 +4,8 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.Vectors;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.test.TestContent;
@ -29,7 +31,13 @@ public class PlayerManager {
public EntityData conjurePlayerEntity(String login) {
// TODO Live up to the name
if (TestContent.PLAYER_LOGIN.equals(login)) {
// TODO load appropriate chunks
Vec3i chunkPos = Vectors.ZERO_3i;
if (getServer().getWorld().getChunk(chunkPos) == null) {
getServer().getChunkManager().loadChunk(chunkPos);
}
return getServer().getWorld().getData().getEntity(TestContent.PLAYER_ENTITY_ID);
} else {
CrashReports.report(null, "Unknown login %s, javahorse stupid", login);

View File

@ -29,8 +29,9 @@ public class Server {
private final ServerThread serverThread;
private final ClientManager clientManager = new ClientManager(this);
private final PlayerManager playerManager = new PlayerManager(this);
private final ClientManager clientManager;
private final PlayerManager playerManager;
private final ChunkManager chunkManager;
private final TaskQueue taskQueue = new TaskQueue(this::isServerThread);
@ -40,7 +41,12 @@ public class Server {
this.world = new WorldLogic(world, this);
this.serverThread = new ServerThread(this);
this.clientManager = new ClientManager(this);
this.playerManager = new PlayerManager(this);
this.chunkManager = new ChunkManager(this);
schedule(this::scheduleChunkTicks);
schedule(chunkManager::tick);
}
/**
@ -64,6 +70,10 @@ public class Server {
return playerManager;
}
public ChunkManager getChunkManager() {
return chunkManager;
}
/**
* Checks if this thread is the main thread of this server.
* @return {@code true} iff the invocation occurs in server main thread
@ -160,7 +170,7 @@ public class Server {
}
public float getLoadDistance(Player player) {
return Units.get(100.0f, "m");
return Units.get(10.0f, "m");
}
/**

View File

@ -1,6 +1,5 @@
package ru.windcorp.progressia.server.comms;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
@ -11,10 +10,6 @@ import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import ru.windcorp.progressia.common.comms.CommsChannel.State;
import ru.windcorp.progressia.common.comms.packets.Packet;
import ru.windcorp.progressia.common.io.ChunkIO;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.PacketLoadChunk;
import ru.windcorp.progressia.common.world.PacketSetLocalPlayer;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.server.Player;
@ -62,30 +57,16 @@ public class ClientManager {
private void addClientPlayer(ClientPlayer client) {
String login = client.getLogin();
EntityData entity = getServer().getPlayerManager().conjurePlayerEntity(login);
EntityData entity;
synchronized (getServer().getWorld().getData()) {
entity = getServer().getPlayerManager().conjurePlayerEntity(login);
Player player = new Player(entity, getServer(), client);
getServer().getPlayerManager().getPlayers().add(player);
for (ChunkData chunk : server.getWorld().getData().getChunks()) {
if (!client.canSeeChunk(chunk.getPosition())) {
continue;
}
PacketLoadChunk packet = new PacketLoadChunk("Core:LoadChunk");
packet.getPosition().set(
chunk.getPosition().x,
chunk.getPosition().y,
chunk.getPosition().z
);
try {
ChunkIO.save(chunk, packet.getData().getOutputStream());
} catch (IOException e) {
CrashReports.report(e, "ClientManager fjcked up. javahorse stupid");
}
client.sendPacket(packet);
getServer().getChunkManager().sendChunk(player, entity.getChunkCoords(null));
}
PacketSetLocalPlayer packet = new PacketSetLocalPlayer();
@ -119,7 +100,7 @@ public class ClientManager {
getClients().forEach(c -> {
if (c.getState() != State.CONNECTED) return;
if (!(c instanceof ClientPlayer)) return;
if (!((ClientPlayer) c).canSeeChunk(chunkPos)) return;
if (!((ClientPlayer) c).isChunkVisible(chunkPos)) return;
c.sendPacket(packet);
});
}
@ -133,7 +114,7 @@ public class ClientManager {
getClients().forEach(c -> {
if (c.getState() != State.CONNECTED) return;
if (!(c instanceof ClientPlayer)) return;
if (!((ClientPlayer) c).canSeeEntity(entityId)) return;
if (!((ClientPlayer) c).isChunkVisible(entityId)) return;
c.sendPacket(packet);
});
}

View File

@ -1,20 +1,32 @@
package ru.windcorp.progressia.server.comms;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.server.Player;
public abstract class ClientPlayer extends Client {
public abstract class ClientPlayer extends ClientChat {
private Player player;
public ClientPlayer(int id) {
super(id);
}
public Player getPlayer() {
return player;
}
public void setPlayer(Player player) {
this.player = player;
}
public abstract String getLogin();
public boolean canSeeChunk(Vec3i chunkPos) {
return true;
public boolean isChunkVisible(Vec3i chunkPos) {
if (player == null) return false;
return player.getServer().getChunkManager().isChunkVisible(chunkPos, player);
}
public boolean canSeeEntity(long entityId) {
public boolean isChunkVisible(long entityId) {
return true;
}

View File

@ -15,7 +15,6 @@ import ru.windcorp.progressia.server.Server;
import ru.windcorp.progressia.server.world.block.BlockLogic;
import ru.windcorp.progressia.server.world.tile.TileLogic;
import ru.windcorp.progressia.server.world.tile.TileLogicStack;
import ru.windcorp.progressia.test.TestChunkSender;
public class WorldLogic
implements GenericWorld<
@ -48,7 +47,6 @@ implements GenericWorld<
});
data.addListener(ChunkDataListeners.createAdder(new UpdateTriggerer(server)));
data.addListener(new TestChunkSender(server));
}
@Override

View File

@ -2,34 +2,29 @@ package ru.windcorp.progressia.test;
import java.io.IOException;
import glm.Glm;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.io.ChunkIO;
import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.PacketLoadChunk;
import ru.windcorp.progressia.common.world.PacketSetLocalPlayer;
import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.WorldDataListener;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.common.world.PacketRevokeChunk;
import ru.windcorp.progressia.common.world.PacketSendChunk;
import ru.windcorp.progressia.server.Server;
import ru.windcorp.progressia.server.comms.ClientPlayer;
public class TestChunkSender implements WorldDataListener {
public class TestChunkSender {
private final Server server;
public static void sendChunk(Server server, ClientPlayer receiver, Vec3i chunkPos) {
ChunkData chunk = server.getWorld().getData().getChunk(chunkPos);
public TestChunkSender(Server server) {
this.server = server;
if (chunk == null) {
throw new IllegalStateException(String.format(
"Chunk (%d; %d; %d) is not loaded, cannot send",
chunkPos.x, chunkPos.y, chunkPos.z
));
}
@Override
public void onChunkLoaded(WorldData world, ChunkData chunk) {
PacketLoadChunk packet = new PacketLoadChunk("Core:LoadChunk");
packet.getPosition().set(
chunk.getPosition().x,
chunk.getPosition().y,
chunk.getPosition().z
);
PacketSendChunk packet = new PacketSendChunk();
packet.getPosition().set(chunkPos.x, chunkPos.y, chunkPos.z);
try {
ChunkIO.save(chunk, packet.getData().getOutputStream());
@ -37,22 +32,13 @@ public class TestChunkSender implements WorldDataListener {
CrashReports.report(e, "TestChunkSender fjcked up. javahorse stupid");
}
server.getClientManager().broadcastLocal(packet, chunk.getPosition());
tmp_sendPlayerIfPossible(world, chunk);
receiver.sendPacket(packet);
}
private void tmp_sendPlayerIfPossible(WorldData world, ChunkData chunk) {
EntityData e = world.getEntity(TestContent.PLAYER_ENTITY_ID);
if (e == null) return;
if (Glm.equals(e.getChunkCoords(null), chunk.getPosition())) {
System.out.printf("TestChunkSender: player found in (%d; %d; %d)\n", e.getChunkCoords(null).x, e.getChunkCoords(null).y, e.getChunkCoords(null).z);
PacketSetLocalPlayer packet = new PacketSetLocalPlayer();
packet.set(e.getEntityId());
server.getClientManager().broadcastToAllPlayers(packet);
}
public static void revokeChunk(ClientPlayer receiver, Vec3i chunkPos) {
PacketRevokeChunk packet = new PacketRevokeChunk();
packet.set(chunkPos);
receiver.sendPacket(packet);
}
}

View File

@ -346,10 +346,10 @@ public class TestContent {
));
chunk.getEntities().add(player);
EntityData statie = EntityDataRegistry.getInstance().create("Test:Statie");
statie.setEntityId(STATIE_ENTITY_ID);
statie.setPosition(new Vec3(0, 15, 16));
chunk.getEntities().add(statie);
// EntityData statie = EntityDataRegistry.getInstance().create("Test:Statie");
// statie.setEntityId(STATIE_ENTITY_ID);
// statie.setPosition(new Vec3(0, 15, 16));
// chunk.getEntities().add(statie);
}
}

View File

@ -17,6 +17,7 @@ import ru.windcorp.progressia.client.graphics.world.LocalPlayer;
import ru.windcorp.progressia.common.Units;
import ru.windcorp.progressia.common.util.FloatMathUtils;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.server.ServerState;
public class TestPlayerControls {
@ -96,6 +97,13 @@ public class TestPlayerControls {
}
player.getVelocity().set(change);
// THIS IS TERRIBLE TEST
EntityData serverEntity = ServerState.getInstance().getWorld().getData().getEntity(TestContent.PLAYER_ENTITY_ID);
if (serverEntity != null) {
serverEntity.setPosition(player.getPosition());
}
}
public void handleInput(Input input) {