From 4332a782214e05ed64f662c717e6da1ab5c9e661 Mon Sep 17 00:00:00 2001 From: OLEGSHA Date: Fri, 26 Mar 2021 20:26:12 +0300 Subject: [PATCH] Refactored ChunkManager and EntityManager, added server event bus --- .../common/world/generic/ChunkMaps.java | 261 ++++++++++++++++++ .../progressia/server/ChunkManager.java | 245 ---------------- .../progressia/server/EntityManager.java | 197 ------------- .../progressia/server/PlayerManager.java | 7 + .../ru/windcorp/progressia/server/Server.java | 89 +++--- .../server/comms/ClientManager.java | 2 +- .../progressia/server/comms/ClientPlayer.java | 4 +- .../progressia/server/events/ClientEvent.java | 46 +++ .../progressia/server/events/PlayerEvent.java | 49 ++++ .../server/events/PlayerJoinedEvent.java | 33 +++ .../server/events/PlayerLeftEvent.java | 33 +++ .../progressia/server/events/ServerEvent.java | 68 +++++ .../server/management/load/ChunkManager.java | 192 +++++++++++++ .../management/load/ChunkRequestDaemon.java | 206 ++++++++++++++ .../server/management/load/EntityManager.java | 97 +++++++ .../management/load/EntityRequestDaemon.java | 121 ++++++++ .../server/management/load/LoadManager.java | 65 +++++ .../server/management/load/PlayerVision.java | 85 ++++++ .../server/management/load/VisionManager.java | 108 ++++++++ 19 files changed, 1429 insertions(+), 479 deletions(-) create mode 100644 src/main/java/ru/windcorp/progressia/common/world/generic/ChunkMaps.java delete mode 100644 src/main/java/ru/windcorp/progressia/server/ChunkManager.java delete mode 100644 src/main/java/ru/windcorp/progressia/server/EntityManager.java create mode 100644 src/main/java/ru/windcorp/progressia/server/events/ClientEvent.java create mode 100644 src/main/java/ru/windcorp/progressia/server/events/PlayerEvent.java create mode 100644 src/main/java/ru/windcorp/progressia/server/events/PlayerJoinedEvent.java create mode 100644 src/main/java/ru/windcorp/progressia/server/events/PlayerLeftEvent.java create mode 100644 src/main/java/ru/windcorp/progressia/server/events/ServerEvent.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/ChunkRequestDaemon.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/EntityManager.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/EntityRequestDaemon.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/LoadManager.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/PlayerVision.java create mode 100644 src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java diff --git a/src/main/java/ru/windcorp/progressia/common/world/generic/ChunkMaps.java b/src/main/java/ru/windcorp/progressia/common/world/generic/ChunkMaps.java new file mode 100644 index 0000000..e67f67e --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/world/generic/ChunkMaps.java @@ -0,0 +1,261 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.common.world.generic; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import glm.vec._3.i.Vec3i; +import gnu.trove.map.hash.TLongObjectHashMap; + +public class ChunkMaps { + + public static ChunkMap newHashMap() { + return new LongBasedChunkMap(new TLongObjectHashMap()); + } + + public static ChunkMap newSyncHashMap(Object mutex) { + return new SynchronizedChunkMap(new LongBasedChunkMap(new TLongObjectHashMap()), mutex); + } + + public static ChunkMap newSyncHashMap() { + return newSyncHashMap(null); + } + + @SuppressWarnings("unchecked") + public static ChunkMap empty() { + return (ChunkMap) EMPTY_MAP; + } + + private ChunkMaps() { + } + + private final static ChunkMap EMPTY_MAP = new ChunkMap() { + + @Override + public int size() { + return 0; + } + + @Override + public boolean containsKey(Vec3i pos) { + return false; + } + + @Override + public Object get(Vec3i pos) { + return null; + } + + @Override + public Object put(Vec3i pos, Object obj) { + throw new UnsupportedOperationException(); + } + + @Override + public Object remove(Vec3i pos) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection values() { + return Collections.emptyList(); + } + + @Override + public ChunkSet keys() { + return ChunkSets.empty(); + } + + @Override + public boolean removeIf(BiPredicate condition) { + return false; + } + + @Override + public void forEach(BiConsumer action) { + // Do nothing + } + + }; + + private static class SynchronizedChunkMap implements ChunkMap { + + private final ChunkMap parent; + private final Object mutex; + + public SynchronizedChunkMap(ChunkMap parent, Object mutex) { + Objects.requireNonNull(parent, "parent"); + this.parent = parent; + + this.mutex = mutex == null ? this : mutex; + } + + @Override + public int size() { + synchronized (mutex) { + return parent.size(); + } + } + + @Override + public boolean isEmpty() { + synchronized (mutex) { + return parent.isEmpty(); + } + } + + @Override + public boolean containsKey(Vec3i pos) { + synchronized (mutex) { + return parent.containsKey(pos); + } + } + + @Override + public V get(Vec3i pos) { + synchronized (mutex) { + return parent.get(pos); + } + } + + @Override + public V put(Vec3i pos, V obj) { + synchronized (mutex) { + return parent.put(pos, obj); + } + } + + @Override + public V remove(Vec3i pos) { + synchronized (mutex) { + return parent.remove(pos); + } + } + + @Override + public boolean containsValue(V value) { + synchronized (mutex) { + return parent.containsValue(value); + } + } + + @Override + public V getOrDefault(Vec3i pos, V def) { + synchronized (mutex) { + return parent.getOrDefault(pos, def); + } + } + + @Override + public V compute(Vec3i pos, BiFunction remappingFunction) { + synchronized (mutex) { + return parent.compute(pos, remappingFunction); + } + } + + @Override + public boolean containsChunk(GenericChunk chunk) { + synchronized (mutex) { + return parent.containsChunk(chunk); + } + } + + @Override + public V get(GenericChunk chunk) { + synchronized (mutex) { + return parent.get(chunk); + } + } + + @Override + public V put(GenericChunk chunk, V obj) { + synchronized (mutex) { + return parent.put(chunk, obj); + } + } + + @Override + public V remove(GenericChunk chunk) { + synchronized (mutex) { + return parent.remove(chunk); + } + } + + @Override + public V getOrDefault(GenericChunk chunk, V def) { + synchronized (mutex) { + return parent.getOrDefault(chunk, def); + } + } + + @Override + public > V compute( + C chunk, + BiFunction remappingFunction + ) { + synchronized (mutex) { + return parent.compute(chunk, remappingFunction); + } + } + + @Override + public Collection values() { + synchronized (mutex) { + return parent.values(); + } + } + + @Override + public ChunkSet keys() { + synchronized (mutex) { + return parent.keys(); + } + } + + @Override + public boolean removeIf(BiPredicate condition) { + synchronized (mutex) { + return parent.removeIf(condition); + } + } + + @Override + public void forEach(BiConsumer action) { + synchronized (mutex) { + parent.forEach(action); + } + } + + @Override + public > void forEachIn( + GenericWorld world, + BiConsumer action + ) { + synchronized (mutex) { + parent.forEachIn(world, action); + } + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/ChunkManager.java b/src/main/java/ru/windcorp/progressia/server/ChunkManager.java deleted file mode 100644 index ede6d70..0000000 --- a/src/main/java/ru/windcorp/progressia/server/ChunkManager.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Progressia - * Copyright (C) 2020-2021 Wind Corporation and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package ru.windcorp.progressia.server; - -import java.util.Collections; -import java.util.Map; -import java.util.WeakHashMap; - -import glm.vec._3.i.Vec3i; -import ru.windcorp.progressia.common.world.ChunkData; -import ru.windcorp.progressia.common.world.PacketRevokeChunk; -import ru.windcorp.progressia.common.world.PacketSendChunk; -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.TestWorldDiskIO; - -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(); - - requested.forEachIn(server.getWorld(), chunk -> { - if (!chunk.isReady()) - return; - if (visible.contains(chunk)) - return; - toSend.add(chunk); - }); - - 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 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::loadOrGenerateChunk); - 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 loadOrGenerateChunk(Vec3i chunkPos) { - - boolean chunkLoadedFromDisk = loadChunk(chunkPos); - - if (!chunkLoadedFromDisk) { - getServer().getWorld().generate(chunkPos); - } - - } - - public boolean loadChunk(Vec3i chunkPos) { - - WorldData world = getServer().getWorld().getData(); - - ChunkData chunk = TestWorldDiskIO.tryToLoad(chunkPos, world, getServer()); - if (chunk != null) { - world.addChunk(chunk); - return true; - } else { - return false; - } - - } - - public void unloadChunk(Vec3i chunkPos) { - - 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); - - TestWorldDiskIO.saveChunk(chunk, getServer()); - - } - - public void sendChunk(Player player, Vec3i chunkPos) { - ChunkData chunk = server.getWorld().getData().getChunk(chunkPos); - - if (chunk == null) { - throw new IllegalStateException( - String.format( - "Chunk (%d; %d; %d) is not loaded, cannot send", - chunkPos.x, - chunkPos.y, - chunkPos.z - ) - ); - } - - PacketSendChunk packet = new PacketSendChunk(); - packet.set(chunk); - player.getClient().sendPacket(packet); - - getVision(player, true).visible.add(chunkPos); - } - - public void revokeChunk(Player player, Vec3i chunkPos) { - PacketRevokeChunk packet = new PacketRevokeChunk(); - packet.set(chunkPos); - player.getClient().sendPacket(packet); - - 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 ChunkSet getVisibleChunks(Player player) { - PlayerVision vision = getVision(player, false); - - if (vision == null) { - return ChunkSets.empty(); - } - - return vision.visible; - } - - public Server getServer() { - return server; - } - -} diff --git a/src/main/java/ru/windcorp/progressia/server/EntityManager.java b/src/main/java/ru/windcorp/progressia/server/EntityManager.java deleted file mode 100644 index d904c83..0000000 --- a/src/main/java/ru/windcorp/progressia/server/EntityManager.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Progressia - * Copyright (C) 2020-2021 Wind Corporation and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package ru.windcorp.progressia.server; - -import java.util.Collections; -import java.util.Map; -import java.util.WeakHashMap; - -import glm.vec._3.i.Vec3i; -import gnu.trove.TCollections; -import gnu.trove.iterator.TLongIterator; -import gnu.trove.set.TLongSet; -import gnu.trove.set.hash.TLongHashSet; -import ru.windcorp.jputil.chars.StringUtil; -import ru.windcorp.progressia.common.util.Vectors; -import ru.windcorp.progressia.common.world.entity.EntityData; -import ru.windcorp.progressia.common.world.entity.PacketRevokeEntity; -import ru.windcorp.progressia.common.world.entity.PacketSendEntity; -import ru.windcorp.progressia.common.world.generic.ChunkSet; - -public class EntityManager { - - private class PlayerVision { - - private final TLongSet visible = TCollections.synchronizedSet(new TLongHashSet()); - private final TLongSet requested = new TLongHashSet(); - private final TLongSet toSend = new TLongHashSet(); - private final TLongSet toRevoke = new TLongHashSet(); - - public boolean isEntityVisible(long entityId) { - return visible.contains(entityId); - } - - public void gatherRequests(Player player) { - requested.clear(); - - ChunkSet visibleChunks = player.getClient().getVisibleChunks(); - Vec3i v = Vectors.grab3i(); - - getServer().getWorld().forEachEntity(entity -> { - if (visibleChunks.contains(entity.getChunkCoords(v))) { - requested.add(entity.getEntityId()); - } - }); - - Vectors.release(v); - } - - public void updateQueues(Player player) { - toSend.clear(); - toSend.addAll(requested); - toSend.removeAll(visible); - toSend.retainAll(loaded); - - toRevoke.clear(); - - for (TLongIterator it = visible.iterator(); it.hasNext();) { - long entityId = it.next(); - if (!loaded.contains(entityId) || !requested.contains(entityId)) { - toRevoke.add(entityId); - } - } - } - - public void processQueues(Player player) { - toRevoke.forEach(entityId -> { - revokeEntity(player, entityId); - return true; - }); - toRevoke.clear(); - - toSend.forEach(entityId -> { - sendEntity(player, entityId); - return true; - }); - toSend.clear(); - } - - } - - private final Server server; - - private final TLongSet loaded; - - // TODO replace with a normal Map managed by some sort of PlayerListener, - // weak maps are weak - private final Map visions = Collections.synchronizedMap(new WeakHashMap<>()); - - public EntityManager(Server server) { - this.server = server; - this.loaded = server.getWorld().getData().getLoadedEntities(); - } - - public void tick() { - synchronized (getServer().getWorld().getData()) { - synchronized (visions) { - gatherRequests(); - updateQueues(); - processQueues(); - } - } - } - - private void gatherRequests() { - server.getPlayerManager().getPlayers().forEach(p -> { - PlayerVision vision = getVision(p, true); - vision.gatherRequests(p); - }); - } - - private void updateQueues() { - visions.forEach((p, v) -> { - v.updateQueues(p); - }); - } - - private void processQueues() { - 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 sendEntity(Player player, long entityId) { - - EntityData entity = server.getWorld().getData().getEntity(entityId); - - if (entity == null) { - throw new IllegalStateException( - "Entity with entity ID " + new String(StringUtil.toFullHex(entityId)) + " is not loaded, cannot send" - ); - } - - PacketSendEntity packet = new PacketSendEntity(); - packet.set(entity); - player.getClient().sendPacket(packet); - - getVision(player, true).visible.add(entityId); - } - - public void revokeEntity(Player player, long entityId) { - PacketRevokeEntity packet = new PacketRevokeEntity(); - packet.set(entityId); - player.getClient().sendPacket(packet); - - PlayerVision vision = getVision(player, false); - if (vision != null) { - vision.visible.remove(entityId); - } - } - - public boolean isEntityVisible(long entityId, Player player) { - PlayerVision vision = getVision(player, false); - - if (vision == null) { - return false; - } - - return vision.isEntityVisible(entityId); - } - - private static final TLongSet EMPTY_LONG_SET = TCollections.unmodifiableSet(new TLongHashSet()); - - public TLongSet getVisibleEntities(Player player) { - PlayerVision vision = getVision(player, false); - - if (vision == null) { - return EMPTY_LONG_SET; - } - - return vision.visible; - } - - public Server getServer() { - return server; - } - -} diff --git a/src/main/java/ru/windcorp/progressia/server/PlayerManager.java b/src/main/java/ru/windcorp/progressia/server/PlayerManager.java index 60b07f1..10799e0 100644 --- a/src/main/java/ru/windcorp/progressia/server/PlayerManager.java +++ b/src/main/java/ru/windcorp/progressia/server/PlayerManager.java @@ -26,6 +26,7 @@ import glm.vec._3.Vec3; import ru.windcorp.progressia.common.util.crash.CrashReports; import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.entity.EntityDataRegistry; +import ru.windcorp.progressia.server.events.PlayerJoinedEvent; import ru.windcorp.progressia.test.TestContent; public class PlayerManager { @@ -44,6 +45,8 @@ public class PlayerManager { public void addPlayer(Player player) { this.players.add(player); + System.out.println("PlayerManager.addPlayer()"); + getServer().postEvent(new PlayerJoinedEvent.Immutable(getServer(), player)); } public EntityData conjurePlayerEntity(String login) { @@ -69,6 +72,10 @@ public class PlayerManager { return player; } + + public Object getMutex() { + return players; + } public Server getServer() { return server; diff --git a/src/main/java/ru/windcorp/progressia/server/Server.java b/src/main/java/ru/windcorp/progressia/server/Server.java index 8ca9383..d4d1883 100644 --- a/src/main/java/ru/windcorp/progressia/server/Server.java +++ b/src/main/java/ru/windcorp/progressia/server/Server.java @@ -15,18 +15,25 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package ru.windcorp.progressia.server; import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; +import com.google.common.eventbus.EventBus; + import ru.windcorp.jputil.functions.ThrowingRunnable; import ru.windcorp.progressia.common.Units; import ru.windcorp.progressia.common.util.TaskQueue; +import ru.windcorp.progressia.common.util.crash.ReportingEventBus; import ru.windcorp.progressia.common.world.WorldData; import ru.windcorp.progressia.server.comms.ClientManager; +import ru.windcorp.progressia.server.events.ServerEvent; +import ru.windcorp.progressia.server.management.load.ChunkRequestDaemon; +import ru.windcorp.progressia.server.management.load.EntityRequestDaemon; +import ru.windcorp.progressia.server.management.load.LoadManager; import ru.windcorp.progressia.server.world.WorldLogic; import ru.windcorp.progressia.server.world.tasks.WorldAccessor; import ru.windcorp.progressia.server.world.ticking.Change; @@ -53,25 +60,32 @@ public class Server { private final ClientManager clientManager; private final PlayerManager playerManager; - private final ChunkManager chunkManager; - private final EntityManager entityManager; + private final LoadManager loadManager; private final TaskQueue taskQueue = new TaskQueue(this::isServerThread); + private final EventBus eventBus = ReportingEventBus.create("ServerEvents"); + private final TickingSettings tickingSettings = new TickingSettings(); public Server(WorldData world) { - this.world = new WorldLogic(world, this, w -> new TestPlanetGenerator("Test:PlanetGenerator", new Planet(4, 9.8f, 16f, 16f), w)); + this.world = new WorldLogic( + world, + this, + w -> new TestPlanetGenerator("Test:PlanetGenerator", new Planet(4, 9.8f, 16f, 16f), w) + ); this.serverThread = new ServerThread(this); this.clientManager = new ClientManager(this); this.playerManager = new PlayerManager(this); - this.chunkManager = new ChunkManager(this); - this.entityManager = new EntityManager(this); + this.loadManager = new LoadManager(this); - schedule(chunkManager::tick); - schedule(entityManager::tick); - schedule(this::scheduleWorldTicks); // Must run after chunkManager so it only schedules chunks that hadn't unloaded + schedule(new ChunkRequestDaemon(loadManager.getChunkManager())::tick); + schedule(new EntityRequestDaemon(loadManager.getEntityManager())::tick); + + // Must run after request daemons so it only schedules chunks that + // hadn't unloaded + schedule(this::scheduleWorldTicks); } /** @@ -84,8 +98,8 @@ public class Server { } /** - * Returns this server's {@link ClientManager}. - * Use this to deal with communications, e.g. send packets. + * Returns this server's {@link ClientManager}. Use this to deal with + * communications, e.g. send packets. * * @return the {@link ClientManager} that handles this server */ @@ -97,8 +111,8 @@ public class Server { return playerManager; } - public ChunkManager getChunkManager() { - return chunkManager; + public LoadManager getLoadManager() { + return loadManager; } /** @@ -111,9 +125,9 @@ public class Server { } /** - * Requests that the provided task is executed once on next server tick. - * The task will be run in the main server thread. The task object is - * discarded after execution. + * Requests that the provided task is executed once on next server tick. The + * task will be run in the main server thread. The task object is discarded + * after execution. *

* Use this method to request a one-time (rare) action that must necessarily * happen in the main server thread, such as initialization tasks or @@ -130,13 +144,12 @@ public class Server { /** * Executes the tasks in the server main thread as soon as possible. *

- * If this method is invoked in the server main thread, then the task is - * run immediately (the method blocks until the task finishes). Otherwise - * this method behaves exactly like {@link #invokeLater(Runnable)}. + * If this method is invoked in the server main thread, then the task is run + * immediately (the method blocks until the task finishes). Otherwise this + * method behaves exactly like {@link #invokeLater(Runnable)}. *

* Use this method to make sure that a piece of code is run in the main - * server - * thread. + * server thread. * * @param task the task to run * @see #invokeLater(Runnable) @@ -146,11 +159,7 @@ public class Server { taskQueue.invokeNow(task); } - public void waitAndInvoke( - ThrowingRunnable task - ) - throws InterruptedException, - E { + public void waitAndInvoke(ThrowingRunnable task) throws InterruptedException, E { taskQueue.waitAndInvoke(task); } @@ -170,6 +179,20 @@ public class Server { serverThread.getTicker().requestEvaluation(evaluation); } + public void subscribe(Object object) { + eventBus.register(object); + } + + public void unsubscribe(Object object) { + eventBus.unregister(object); + } + + public void postEvent(ServerEvent event) { + event.setServer(this); + eventBus.post(event); + event.setServer(null); + } + /** * Returns the duration of the last server tick. Server logic should assume * that this much in-world time has passed. @@ -186,8 +209,8 @@ public class Server { /** * Returns the {@link WorldAccessor} object for this server. Use the - * provided accessor to - * request common {@link Evaluation}s and {@link Change}s. + * provided accessor to request common {@link Evaluation}s and + * {@link Change}s. * * @return a {@link WorldAccessor} * @see #requestChange(Change) @@ -227,8 +250,7 @@ public class Server { /** * Shuts the server down, disconnecting the clients with the provided - * message. - * This method blocks until the shutdown is complete. + * message. This method blocks until the shutdown is complete. * * @param message the message to send to the clients as the disconnect * reason @@ -245,10 +267,9 @@ public class Server { /** * Returns an instance of {@link java.util.Random Random} that can be used - * as a source of indeterministic - * randomness. World generation and other algorithms that must have random - * but reproducible results should - * not use this. + * as a source of indeterministic randomness. World generation and other + * algorithms that must have random but reproducible results should not use + * this. * * @return a thread-safe indeterministic instance of * {@link java.util.Random}. diff --git a/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java b/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java index 74c636d..dfa631a 100644 --- a/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java +++ b/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java @@ -81,7 +81,7 @@ public class ClientManager { EntityData entity = getServer().getPlayerManager().conjurePlayerEntity(login); Player player = new Player(entity, getServer(), client); - getServer().getPlayerManager().getPlayers().add(player); + getServer().getPlayerManager().addPlayer(player); PacketSetLocalPlayer packet = new PacketSetLocalPlayer(); packet.set(entity.getEntityId()); diff --git a/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java b/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java index c455b8e..c05d952 100644 --- a/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java +++ b/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java @@ -44,13 +44,13 @@ public abstract class ClientPlayer extends ClientChat { public boolean isChunkVisible(Vec3i chunkPos) { if (player == null) return false; - return player.getServer().getChunkManager().isChunkVisible(chunkPos, player); + return player.getServer().getLoadManager().getVisionManager().isChunkVisible(chunkPos, player); } public ChunkSet getVisibleChunks() { if (player == null) return ChunkSets.empty(); - return player.getServer().getChunkManager().getVisibleChunks(player); + return player.getServer().getLoadManager().getVisionManager().getVisibleChunks(player); } public boolean isChunkVisible(long entityId) { diff --git a/src/main/java/ru/windcorp/progressia/server/events/ClientEvent.java b/src/main/java/ru/windcorp/progressia/server/events/ClientEvent.java new file mode 100644 index 0000000..c795fa8 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/events/ClientEvent.java @@ -0,0 +1,46 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.events; + +import ru.windcorp.progressia.server.Server; +import ru.windcorp.progressia.server.comms.Client; + +public interface ClientEvent extends ServerEvent { + + Client getClient(); + + public static abstract class Immutable extends ServerEvent.Default implements ClientEvent { + + private final Client client; + + public Immutable(Server server, Client client) { + super(server); + this.client = client; + } + + /** + * @return the client + */ + @Override + public Client getClient() { + return client; + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/events/PlayerEvent.java b/src/main/java/ru/windcorp/progressia/server/events/PlayerEvent.java new file mode 100644 index 0000000..544418e --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/events/PlayerEvent.java @@ -0,0 +1,49 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.events; + +import ru.windcorp.progressia.server.Player; +import ru.windcorp.progressia.server.Server; +import ru.windcorp.progressia.server.comms.Client; + +public interface PlayerEvent extends ClientEvent { + + Player getPlayer(); + + public static abstract class Immutable extends ServerEvent.Default implements PlayerEvent { + + private final Player player; + + public Immutable(Server server, Player player) { + super(server); + this.player = player; + } + + @Override + public Player getPlayer() { + return player; + } + + @Override + public Client getClient() { + return player.getClient(); + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/events/PlayerJoinedEvent.java b/src/main/java/ru/windcorp/progressia/server/events/PlayerJoinedEvent.java new file mode 100644 index 0000000..eed2a90 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/events/PlayerJoinedEvent.java @@ -0,0 +1,33 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.events; + +import ru.windcorp.progressia.server.Player; +import ru.windcorp.progressia.server.Server; + +public interface PlayerJoinedEvent extends PlayerEvent { + + public static class Immutable extends PlayerEvent.Immutable implements PlayerJoinedEvent { + + public Immutable(Server server, Player player) { + super(server, player); + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/events/PlayerLeftEvent.java b/src/main/java/ru/windcorp/progressia/server/events/PlayerLeftEvent.java new file mode 100644 index 0000000..d6e4417 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/events/PlayerLeftEvent.java @@ -0,0 +1,33 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.events; + +import ru.windcorp.progressia.server.Player; +import ru.windcorp.progressia.server.Server; + +public interface PlayerLeftEvent extends PlayerEvent { + + public static class Immutable extends PlayerEvent.Immutable implements PlayerLeftEvent { + + public Immutable(Server server, Player player) { + super(server, player); + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/events/ServerEvent.java b/src/main/java/ru/windcorp/progressia/server/events/ServerEvent.java new file mode 100644 index 0000000..b63f727 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/events/ServerEvent.java @@ -0,0 +1,68 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.events; + +import ru.windcorp.progressia.server.Server; + +/** + * An interface for all events issued by a {@link Server}. + */ +public interface ServerEvent { + + /** + * Returns the server instance that this event happened on. + * + * @return the relevant server + */ + Server getServer(); + + /** + * Sets the server instance that the event is posted on. The value provided + * to this method must be returned by subsequent calls to + * {@link #getServer()}. Do not call this method when handling the event. + * + * @param server the server dispatching the event or {@code null} to unbind + * any previously bound server + */ + void setServer(Server server); + + /** + * A default implementation of {@link ServerEvent}. This is not necessarily + * extended by server events. + */ + public static abstract class Default implements ServerEvent { + + private Server server; + + public Default(Server server) { + this.server = server; + } + + @Override + public Server getServer() { + return server; + } + + @Override + public void setServer(Server server) { + this.server = server; + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java b/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java new file mode 100644 index 0000000..a31a9cd --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java @@ -0,0 +1,192 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.management.load; + +import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.world.ChunkData; +import ru.windcorp.progressia.common.world.PacketRevokeChunk; +import ru.windcorp.progressia.common.world.PacketSendChunk; +import ru.windcorp.progressia.common.world.WorldData; +import ru.windcorp.progressia.server.Player; +import ru.windcorp.progressia.server.Server; +import ru.windcorp.progressia.test.TestWorldDiskIO; + +/** + * Chunk manager provides facilities to load, unload and generate chunks for a + * {@link Server} on demand. + */ +public class ChunkManager { + + private final LoadManager loadManager; + + public ChunkManager(LoadManager loadManager) { + this.loadManager = loadManager; + } + + /** + * @return the loadManager + */ + public LoadManager getLoadManager() { + return loadManager; + } + + /** + * @return the server + */ + public Server getServer() { + return getLoadManager().getServer(); + } + + /** + * Describes the result of an attempt to load a chunk. + */ + public static enum LoadResult { + /** + * A chunk has successfully been read from disk and is now loaded. + */ + LOADED_FROM_DISK, + + /** + * A chunk has successfully been generated and is now loaded. + */ + GENERATED, + + /** + * A chunk has already been loaded and so no action has been taken. + */ + ALREADY_LOADED, + + /** + * A chunk has not been loaded previously and the operation has failed + * to load it. It is not currently loaded. + */ + NOT_LOADED + } + + /** + * Loads or generates the chunk at the given location unless it is already + * loaded. The chunk is loaded after this method completes normally. + * + * @param chunkPos the position of the chunk + * @return one of {@link LoadResult#LOADED_FROM_DISK LOADED_FROM_DISK}, + * {@link LoadResult#GENERATED GENERATED} or + * {@link LoadResult#ALREADY_LOADED ALREADY_LOADED} + */ + public LoadResult loadOrGenerateChunk(Vec3i chunkPos) { + LoadResult loadResult = loadChunk(chunkPos); + + if (loadResult == LoadResult.NOT_LOADED) { + getServer().getWorld().generate(chunkPos); + return LoadResult.GENERATED; + } else { + return loadResult; + } + } + + /** + * Attempts to load the chunk from disk unless it is already loaded. If the + * chunk is not currently loaded and it is not available on the disk this + * method does nothing. + * + * @param chunkPos the position of the chunk + * @return one of {@link LoadResult#LOADED_FROM_DISK LOADED_FROM_DISK}, + * {@link LoadResult#NOT_LOADED NOT_LOADED} or + * {@link LoadResult#ALREADY_LOADED ALREADY_LOADED} + */ + public LoadResult loadChunk(Vec3i chunkPos) { + if (isChunkLoaded(chunkPos)) { + return LoadResult.ALREADY_LOADED; + } + + WorldData world = getServer().getWorld().getData(); + + ChunkData chunk = TestWorldDiskIO.tryToLoad(chunkPos, world, getServer()); + if (chunk != null) { + world.addChunk(chunk); + return LoadResult.LOADED_FROM_DISK; + } else { + return LoadResult.NOT_LOADED; + } + } + + /** + * Unloads the chunk and saves it to disk if the chunk is loaded, otherwise + * does nothing. + * + * @param chunkPos the position of the chunk + * @return {@code true} iff the chunk had been loaded and was unloaded by + * this method + */ + public boolean unloadChunk(Vec3i chunkPos) { + WorldData world = getServer().getWorld().getData(); + ChunkData chunk = world.getChunk(chunkPos); + + if (chunk == null) { + return false; + } + + world.removeChunk(chunk); + TestWorldDiskIO.saveChunk(chunk, getServer()); + + return true; + } + + public void sendChunk(Player player, Vec3i chunkPos) { + ChunkData chunk = getServer().getWorld().getData().getChunk(chunkPos); + + if (chunk == null) { + throw new IllegalStateException( + String.format( + "Chunk (%d; %d; %d) is not loaded, cannot send", + chunkPos.x, + chunkPos.y, + chunkPos.z + ) + ); + } + + PacketSendChunk packet = new PacketSendChunk(); + packet.set(chunk); + player.getClient().sendPacket(packet); + + getLoadManager().getVisionManager().getVision(player, true).getVisibleChunks().add(chunkPos); + } + + public void revokeChunk(Player player, Vec3i chunkPos) { + PacketRevokeChunk packet = new PacketRevokeChunk(); + packet.set(chunkPos); + player.getClient().sendPacket(packet); + + PlayerVision vision = getLoadManager().getVisionManager().getVision(player, false); + if (vision != null) { + vision.getVisibleChunks().remove(chunkPos); + } + } + + /** + * Checks whether or not the chunk at the specified location is loaded. A + * loaded chunk is accessible through the server's {@link WorldData} object. + * + * @param chunkPos the position of the chunk + * @return {@code true} iff the chunk is loaded + */ + public boolean isChunkLoaded(Vec3i chunkPos) { + return getServer().getWorld().isChunkLoaded(chunkPos); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/ChunkRequestDaemon.java b/src/main/java/ru/windcorp/progressia/server/management/load/ChunkRequestDaemon.java new file mode 100644 index 0000000..ff3c7e0 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/ChunkRequestDaemon.java @@ -0,0 +1,206 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.management.load; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.Units; +import ru.windcorp.progressia.common.world.generic.ChunkMap; +import ru.windcorp.progressia.common.world.generic.ChunkMaps; +import ru.windcorp.progressia.common.world.generic.ChunkSet; +import ru.windcorp.progressia.common.world.generic.ChunkSets; +import ru.windcorp.progressia.server.Server; + +/** + * Chunk request daemon gathers chunk requests from players (via {@link VisionManager}) and loads or unloads chunks appropriately. + */ +public class ChunkRequestDaemon { + + private static final float CHUNK_UNLOAD_DELAY = Units.get(5, "s"); + + private final ChunkManager chunkManager; + + private final ChunkSet loaded; + private final ChunkSet requested = ChunkSets.newHashSet(); + private final ChunkSet toLoad = ChunkSets.newHashSet(); + private final ChunkSet toRequestUnload = ChunkSets.newHashSet(); + + private final Collection buffer = new ArrayList<>(); + + private static class ChunkUnloadRequest { + private final Vec3i chunkPos; + private final long unloadAt; + + public ChunkUnloadRequest(Vec3i chunkPos, long unloadAt) { + this.chunkPos = chunkPos; + this.unloadAt = unloadAt; + } + + /** + * @return the chunk position + */ + public Vec3i getChunkPos() { + return chunkPos; + } + + /** + * @return the moment when the chunks becomes eligible for unloading + */ + public long getUnloadAt() { + return unloadAt; + } + } + + private final ChunkMap unloadSchedule = ChunkMaps.newHashMap(); + + public ChunkRequestDaemon(ChunkManager chunkManager) { + this.chunkManager = chunkManager; + this.loaded = getServer().getWorld().getData().getLoadedChunks(); + } + + public void tick() { + synchronized (getServer().getWorld().getData()) { + synchronized (getServer().getPlayerManager().getMutex()) { + loadAndUnloadChunks(); + sendAndRevokeChunks(); + } + } + } + + private void loadAndUnloadChunks() { + gatherLoadRequests(); + updateLoadQueues(); + processLoadQueues(); + } + + private void gatherLoadRequests() { + requested.clear(); + + getChunkManager().getLoadManager().getVisionManager().forEachVision(vision -> { + vision.getRequestedChunks().clear(); + vision.getPlayer().requestChunksToLoad(vision.getRequestedChunks()::add); + requested.addAll(vision.getRequestedChunks()); + }); + } + + private void updateLoadQueues() { + toLoad.clear(); + toLoad.addAll(requested); + toLoad.removeAll(loaded); + + toRequestUnload.clear(); + toRequestUnload.addAll(loaded); + toRequestUnload.removeAll(requested); + } + + private void processLoadQueues() { + toRequestUnload.forEach(this::scheduleUnload); + toRequestUnload.clear(); + + toLoad.forEach(getChunkManager()::loadOrGenerateChunk); + toLoad.clear(); + + unloadScheduledChunks(); + } + + private void scheduleUnload(Vec3i chunkPos) { + if (unloadSchedule.containsKey(chunkPos)) { + // Unload already requested, skip + return; + } + + long unloadAt = System.currentTimeMillis() + (long) (getUnloadDelay() * 1000); + Vec3i chunkPosCopy = new Vec3i(chunkPos); + + unloadSchedule.put(chunkPosCopy, new ChunkUnloadRequest(chunkPosCopy, unloadAt)); + } + + private void unloadScheduledChunks() { + long now = System.currentTimeMillis(); + + for (Iterator it = unloadSchedule.values().iterator(); it.hasNext();) { + ChunkUnloadRequest request = it.next(); + + if (request.getUnloadAt() < now) { + it.remove(); + getChunkManager().unloadChunk(request.getChunkPos()); + } + } + } + + private void sendAndRevokeChunks() { + getChunkManager().getLoadManager().getVisionManager().forEachVision(vision -> { + revokeChunks(vision); + sendChunks(vision); + }); + } + + private void sendChunks(PlayerVision vision) { + vision.getRequestedChunks().forEachIn(getServer().getWorld(), chunk -> { + if (!chunk.isReady()) + return; + if (vision.isChunkVisible(chunk.getPosition())) + return; + buffer.add(chunk.getPosition()); + }); + + if (buffer.isEmpty()) return; + for (Vec3i chunkPos : buffer) { + getChunkManager().sendChunk(vision.getPlayer(), chunkPos); + } + + buffer.clear(); + } + + private void revokeChunks(PlayerVision vision) { + vision.getVisibleChunks().forEach(chunkPos -> { + if (getChunkManager().isChunkLoaded(chunkPos) && vision.getRequestedChunks().contains(chunkPos)) + return; + buffer.add(new Vec3i(chunkPos)); + }); + + if (buffer.isEmpty()) return; + for (Vec3i chunkPos : buffer) { + getChunkManager().revokeChunk(vision.getPlayer(), chunkPos); + } + + buffer.clear(); + } + + /** + * @return the minimum amount of time a chunk will spend in the unload queue + */ + public float getUnloadDelay() { + return CHUNK_UNLOAD_DELAY; + } + + /** + * @return the manager + */ + public ChunkManager getChunkManager() { + return chunkManager; + } + + public Server getServer() { + return getChunkManager().getServer(); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/EntityManager.java b/src/main/java/ru/windcorp/progressia/server/management/load/EntityManager.java new file mode 100644 index 0000000..6aa3853 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/EntityManager.java @@ -0,0 +1,97 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.server.management.load; + +import gnu.trove.set.TLongSet; +import ru.windcorp.progressia.common.world.entity.EntityData; +import ru.windcorp.progressia.common.world.entity.PacketRevokeEntity; +import ru.windcorp.progressia.common.world.entity.PacketSendEntity; +import ru.windcorp.progressia.server.Player; +import ru.windcorp.progressia.server.Server; + +public class EntityManager { + + private final LoadManager loadManager; + + private final TLongSet loaded; + + public EntityManager(LoadManager loadManager) { + this.loadManager = loadManager; + this.loaded = getServer().getWorld().getData().getLoadedEntities(); + } + + public void sendEntity(Player player, long entityId) { + PlayerVision vision = getLoadManager().getVisionManager().getVision(player, true); + if (!vision.getVisibleEntities().add(entityId)) { + return; + } + + EntityData entity = getServer().getWorld().getData().getEntity(entityId); + + if (entity == null) { + throw new IllegalStateException( + "Entity with entity ID " + EntityData.formatEntityId(entityId) + " is not loaded, cannot send" + ); + } + + PacketSendEntity packet = new PacketSendEntity(); + packet.set(entity); + player.getClient().sendPacket(packet); + } + + public void revokeEntity(Player player, long entityId) { + PlayerVision vision = getLoadManager().getVisionManager().getVision(player, false); + if (vision == null) { + return; + } + if (!vision.getVisibleEntities().remove(entityId)) { + return; + } + + PacketRevokeEntity packet = new PacketRevokeEntity(); + packet.set(entityId); + player.getClient().sendPacket(packet); + } + + public boolean isEntityVisible(Player player, long entityId) { + PlayerVision vision = getLoadManager().getVisionManager().getVision(player, false); + + if (vision == null) { + return false; + } + + return vision.isEntityVisible(entityId); + } + + public boolean isEntityLoaded(long entityId) { + return loaded.contains(entityId); + } + + /** + * @return the loadManager + */ + public LoadManager getLoadManager() { + return loadManager; + } + + public Server getServer() { + return getLoadManager().getServer(); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/EntityRequestDaemon.java b/src/main/java/ru/windcorp/progressia/server/management/load/EntityRequestDaemon.java new file mode 100644 index 0000000..9605ffe --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/EntityRequestDaemon.java @@ -0,0 +1,121 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.management.load; + +import java.util.function.Consumer; + +import glm.vec._3.i.Vec3i; +import gnu.trove.TLongCollection; +import gnu.trove.iterator.TLongIterator; +import gnu.trove.list.array.TLongArrayList; +import gnu.trove.set.TLongSet; +import ru.windcorp.progressia.common.util.Vectors; +import ru.windcorp.progressia.common.world.generic.ChunkSet; +import ru.windcorp.progressia.server.Server; + +public class EntityRequestDaemon { + + private final EntityManager entityManager; + + private final TLongCollection buffer = new TLongArrayList(); + + public EntityRequestDaemon(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public void tick() { + synchronized (getServer().getWorld().getData()) { + synchronized (getServer().getPlayerManager().getMutex()) { + gatherRequests(); + revokeEntities(); + sendEntities(); + } + } + } + + private void gatherRequests() { + Vec3i v = Vectors.grab3i(); + + forEachVision(vision -> { + + TLongSet requestedEntities = vision.getRequestedEntities(); + requestedEntities.clear(); + + ChunkSet visibleChunks = vision.getVisibleChunks(); + getServer().getWorld().forEachEntity(entity -> { + if (visibleChunks.contains(entity.getChunkCoords(v))) { + requestedEntities.add(entity.getEntityId()); + } + }); + }); + + Vectors.release(v); + } + + private void sendEntities() { + forEachVision(vision -> { + for (TLongIterator it = vision.getRequestedEntities().iterator(); it.hasNext();) { + long entityId = it.next(); + if (getEntityManager().isEntityLoaded(entityId) && !vision.getVisibleEntities().contains(entityId)) { + buffer.add(entityId); + } + } + + if (buffer.isEmpty()) return; + for (TLongIterator it = buffer.iterator(); it.hasNext();) { + getEntityManager().sendEntity(vision.getPlayer(), it.next()); + } + + buffer.clear(); + }); + } + + private void revokeEntities() { + forEachVision(vision -> { + for (TLongIterator it = vision.getVisibleEntities().iterator(); it.hasNext();) { + long entityId = it.next(); + if (!getEntityManager().isEntityLoaded(entityId) || !vision.getRequestedEntities().contains(entityId)) { + buffer.add(entityId); + } + } + + if (buffer.isEmpty()) return; + for (TLongIterator it = buffer.iterator(); it.hasNext();) { + getEntityManager().revokeEntity(vision.getPlayer(), it.next()); + } + + buffer.clear(); + }); + } + + /** + * @return the entityManager + */ + public EntityManager getEntityManager() { + return entityManager; + } + + public Server getServer() { + return getEntityManager().getServer(); + } + + private void forEachVision(Consumer action) { + getEntityManager().getLoadManager().getVisionManager().forEachVision(action); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/LoadManager.java b/src/main/java/ru/windcorp/progressia/server/management/load/LoadManager.java new file mode 100644 index 0000000..3670116 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/LoadManager.java @@ -0,0 +1,65 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.management.load; + +import ru.windcorp.progressia.server.Server; + +public class LoadManager { + + private final Server server; + private final ChunkManager chunkManager; + private final EntityManager entityManager; + private final VisionManager visionManager; + + public LoadManager(Server server) { + this.server = server; + + this.chunkManager = new ChunkManager(this); + this.entityManager = new EntityManager(this); + this.visionManager = new VisionManager(this); + } + + /** + * @return the chunkManager + */ + public ChunkManager getChunkManager() { + return chunkManager; + } + + /** + * @return the entityManager + */ + public EntityManager getEntityManager() { + return entityManager; + } + + /** + * @return the visionManager + */ + public VisionManager getVisionManager() { + return visionManager; + } + + /** + * @return the server + */ + public Server getServer() { + return server; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/PlayerVision.java b/src/main/java/ru/windcorp/progressia/server/management/load/PlayerVision.java new file mode 100644 index 0000000..bb8e474 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/PlayerVision.java @@ -0,0 +1,85 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.management.load; + +import glm.vec._3.i.Vec3i; +import gnu.trove.TCollections; +import gnu.trove.set.TLongSet; +import gnu.trove.set.hash.TLongHashSet; +import ru.windcorp.progressia.common.world.generic.ChunkSet; +import ru.windcorp.progressia.common.world.generic.ChunkSets; +import ru.windcorp.progressia.server.Player; + +public class PlayerVision { + + private final Player player; + + private final ChunkSet visibleChunks = ChunkSets.newSyncHashSet(); + private final ChunkSet requestedChunks = ChunkSets.newHashSet(); + + private final TLongSet visibleEntities = TCollections.synchronizedSet(new TLongHashSet()); + private final TLongSet requestedEntities = new TLongHashSet(); + + public PlayerVision(Player player) { + this.player = player; + } + + public boolean isChunkVisible(Vec3i chunkPos) { + return visibleChunks.contains(chunkPos); + } + + public boolean isEntityVisible(long entityId) { + return visibleEntities.contains(entityId); + } + + /** + * @return the requestedChunks + */ + public ChunkSet getRequestedChunks() { + return requestedChunks; + } + + /** + * @return the visibleChunks + */ + public ChunkSet getVisibleChunks() { + return visibleChunks; + } + + /** + * @return the requestedEntities + */ + public TLongSet getRequestedEntities() { + return requestedEntities; + } + + /** + * @return the visibleEntities + */ + public TLongSet getVisibleEntities() { + return visibleEntities; + } + + /** + * @return the player + */ + public Player getPlayer() { + return player; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java b/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java new file mode 100644 index 0000000..cdf2d8e --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java @@ -0,0 +1,108 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.server.management.load; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import com.google.common.eventbus.Subscribe; + +import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.world.generic.ChunkSet; +import ru.windcorp.progressia.common.world.generic.ChunkSets; +import ru.windcorp.progressia.server.Player; +import ru.windcorp.progressia.server.Server; +import ru.windcorp.progressia.server.events.PlayerJoinedEvent; +import ru.windcorp.progressia.server.events.PlayerLeftEvent; + +public class VisionManager { + + private final LoadManager loadManager; + + private final Map visions = Collections.synchronizedMap(new HashMap<>()); + + public VisionManager(LoadManager loadManager) { + this.loadManager = loadManager; + getServer().subscribe(this); + } + + @Subscribe + private void onPlayerJoined(PlayerJoinedEvent event) { + System.out.println("VisionManager.onPlayerJoined()"); + getVision(event.getPlayer(), true); + } + + @Subscribe + private void onPlayerLeft(PlayerLeftEvent event) { + System.out.println("VisionManager.onPlayerLeft()"); + visions.remove(event.getPlayer()); + } + + public PlayerVision getVision(Player player, boolean createIfMissing) { + if (createIfMissing) { + return visions.computeIfAbsent(player, PlayerVision::new); + } else { + return visions.get(player); + } + } + + public void forEachVision(Consumer action) { + visions.values().forEach(action); + } + + public boolean isChunkVisible(Vec3i chunkPos, Player player) { + PlayerVision vision = getVision(player, false); + + if (vision == null) { + return false; + } + + return vision.isChunkVisible(chunkPos); + } + + public ChunkSet getVisibleChunks(Player player) { + PlayerVision vision = getVision(player, false); + + if (vision == null) { + return ChunkSets.empty(); + } + + return vision.getVisibleChunks(); + } + + /** + * @return the loadManager + */ + public LoadManager getLoadManager() { + return loadManager; + } + + /** + * @return the chunkManager + */ + public ChunkManager getChunkManager() { + return getLoadManager().getChunkManager(); + } + + public Server getServer() { + return getLoadManager().getServer(); + } + +}