diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java index b2aa3e9ec..5dab86791 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ServerApi.java @@ -149,37 +149,37 @@ public class ServerApi public void serverPlayerJoinEvent(IServerPlayerWrapper player) { IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); - if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well + LOGGER.info("Player [" + player.getName() + "] joined."); + if (serverWorld != null) { - LOGGER.info("Player [" + player.getName()+ "] joined."); - ((DhServerWorld) serverWorld).addPlayer(player); + serverWorld.addPlayer(player); } } public void serverPlayerDisconnectEvent(IServerPlayerWrapper player) { IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); - if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well + LOGGER.info("Player [" + player.getName() + "] disconnected."); + if (serverWorld != null) { - LOGGER.info("Player [" + player.getName() + "] disconnected."); - ((DhServerWorld) serverWorld).removePlayer(player); + serverWorld.removePlayer(player); } } public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel) { IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); - if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well + LOGGER.info("Player [" + player.getName() + "] changed level: [" + originLevel.getKeyedLevelDimensionName() + "] -> [" + destinationLevel.getKeyedLevelDimensionName() + "]."); + if (serverWorld != null) { - LOGGER.info("Player [" + player.getName() + "] changed level: ["+originLevel.getKeyedLevelDimensionName()+"] -> ["+destinationLevel.getKeyedLevelDimensionName()+"]."); - ((DhServerWorld) serverWorld).changePlayerLevel(player, originLevel, destinationLevel); + serverWorld.changePlayerLevel(player, originLevel, destinationLevel); } } public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message) { IDhServerWorld serverWorld = SharedApi.getIDhServerWorld(); - if (serverWorld instanceof DhServerWorld) // TODO add support for DhClientServerWorld's (lan worlds) as well + if (serverWorld != null) { - ((DhServerWorld) serverWorld).remotePlayerConnectionHandler.handlePluginMessage(player, message); + serverWorld.getServerPlayerStateManager().handlePluginMessage(player, message); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java index 40afa8b85..97b6ba83f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java @@ -117,11 +117,11 @@ public class SharedApi public static AbstractDhWorld getAbstractDhWorld() { return currentWorld; } /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientServerWorld} */ - public static DhClientServerWorld getDhClientServerWorld() { return (currentWorld != null && DhClientServerWorld.class.isInstance(currentWorld)) ? (DhClientServerWorld) currentWorld : null; } + public static DhClientServerWorld getDhClientServerWorld() { return (currentWorld instanceof DhClientServerWorld) ? (DhClientServerWorld) currentWorld : null; } /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhClientWorld} or {@link DhClientServerWorld} */ - public static IDhClientWorld getIDhClientWorld() { return (currentWorld != null && IDhClientWorld.class.isInstance(currentWorld)) ? (IDhClientWorld) currentWorld : null; } + public static IDhClientWorld getIDhClientWorld() { return (currentWorld instanceof IDhClientWorld) ? (IDhClientWorld) currentWorld : null; } /** returns null if the {@link SharedApi#currentWorld} isn't a {@link DhServerWorld} or {@link DhClientServerWorld} */ - public static IDhServerWorld getIDhServerWorld() { return (currentWorld != null && IDhServerWorld.class.isInstance(currentWorld)) ? (IDhServerWorld) currentWorld : null; } + public static IDhServerWorld getIDhServerWorld() { return (currentWorld instanceof IDhServerWorld) ? (IDhServerWorld) currentWorld : null; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java index 02b5a39be..8f99a0d21 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java @@ -28,8 +28,10 @@ import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.level.WorldGenModule; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.util.LodUtil; @@ -418,9 +420,13 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im /** used by external event listeners */ - @FunctionalInterface public interface IOnWorldGenCompleteListener { + boolean shouldDoWorldGen(); + + @Nullable + DhBlockPos2D getTargetPosForGeneration(); + /** Fired whenever a section has completed generating */ void onWorldGenTaskComplete(long pos); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java new file mode 100644 index 000000000..49ffc28e7 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhServerLevel.java @@ -0,0 +1,510 @@ +package com.seibel.distanthorizons.core.level; + +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; +import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; +import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; +import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; +import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; +import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; +import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; +import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; +import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; +import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.math.Vec3d; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.util.Map; +import java.util.concurrent.*; + +public abstract class AbstractDhServerLevel extends AbstractDhLevel implements IDhServerLevel +{ + protected static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(), + () -> Config.Client.Advanced.Logging.logNetworkEvent.get()); + + /** 1 Mebibyte minus 576 bytes for other info */ + public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000; + + public final ServerLevelModule serverside; + protected final IServerLevelWrapper serverLevelWrapper; + + protected final ServerPlayerStateManager serverPlayerStateManager; + + /** + * This queue is used for ensuring fair generation speed for each player.
+ * Every tick the first player gets used for centering generation, and then is immediately moved into the back of the queue.
+ * TODO only add players that actually have something to generate + */ + protected final ConcurrentLinkedQueue worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>(); + + private final ConcurrentMap requestGroupByPos = new ConcurrentHashMap<>(); + private final ConcurrentMap requestGroupByFutureId = new ConcurrentHashMap<>(); + + + + //=============// + // constructor // + //=============// + + public AbstractDhServerLevel( + AbstractSaveStructure saveStructure, + IServerLevelWrapper serverLevelWrapper, + ServerPlayerStateManager serverPlayerStateManager + ) + { this(saveStructure, serverLevelWrapper, serverPlayerStateManager, true); } + public AbstractDhServerLevel( + AbstractSaveStructure saveStructure, + IServerLevelWrapper serverLevelWrapper, + ServerPlayerStateManager serverPlayerStateManager, + boolean runRepoReliantSetup + ) + { + if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs()) + { + LOGGER.warn("unable to create data folder."); + } + this.serverLevelWrapper = serverLevelWrapper; + this.serverside = new ServerLevelModule(this, saveStructure); + this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile); + if (runRepoReliantSetup) + { + this.runRepoReliantSetup(); + } + + LOGGER.info("Started " + this.getClass().getSimpleName() + " for [" + serverLevelWrapper + "] at [" + saveStructure + "]."); + + this.serverPlayerStateManager = serverPlayerStateManager; + } + + + + //=======// + // ticks // + //=======// + + @Override + public void serverTick() + { + // Send finished data source requests + for (Map.Entry entry : this.requestGroupByPos.entrySet()) + { + DataSourceRequestGroup requestGroup = entry.getValue(); + + if (requestGroup.fullDataSource == null) + { + continue; + } + + NETWORK_LOGGER.debug("[" + this.serverLevelWrapper.getDimensionName() + "] Fulfilled request group [" + entry.getKey() + "]"); + + // Make this group unavailable for adding into + this.requestGroupByPos.remove(entry.getKey()); + requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE); + requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE); + + ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); + if (executor == null) + { + LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null"); + continue; + } + CompletableFuture.runAsync(() -> + { + FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource); + for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values()) + { + this.requestGroupByFutureId.remove(msg.futureId); + + ServerPlayerState serverPlayerState = this.serverPlayerStateManager.getConnectedPlayer(msg.serverPlayer()); + if (serverPlayerState == null) + { + continue; + } + + serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release(); + payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, msg.getSession()::sendMessage); + msg.sendResponse(new FullDataSourceResponseMessage(payload)); + } + }, executor); + } + } + + @Override + public abstract boolean shouldDoWorldGen(); + + @Override + @Nullable + public DhBlockPos2D getTargetPosForGeneration() + { + IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek(); + if (firstPlayer == null) + { + return null; + } + + // Put first player in back before removing from front, so it can be removed by other thread without blocking + // - if it gets removed, remove() below will remove the item we just put instead + this.worldGenPlayerCenteringQueue.add(firstPlayer); + this.worldGenPlayerCenteringQueue.remove(firstPlayer); + + Vec3d position = firstPlayer.getPosition(); + return new DhBlockPos2D((int) position.x, (int) position.z); + } + + @Override public void worldGenTick() + { + this.serverside.worldGenModule.worldGenTick(); + } + + + + //==================// + // network handling // + //==================// + + public void registerNetworkHandlers(ServerPlayerState serverPlayerState) + { + serverPlayerState.networkSession.registerHandler(FullDataSourceRequestMessage.class, (message) -> + { + if (!this.messagePlayerInThisLevel(message)) + { + // we can't handle players in other levels, don't continue + return; + } + + + ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this); + + if (message.clientTimestamp == null) + { + this.queueWorldGenForRequestMessage(serverPlayerState, message, rateLimiterSet); + } + else + { + this.queueLodSyncForRequestMessage(serverPlayerState, message, rateLimiterSet); + } + }); + + + serverPlayerState.networkSession.registerHandler(CancelMessage.class, msg -> + { + DataSourceRequestGroup requestGroup = this.requestGroupByFutureId.remove(msg.futureId); + if (requestGroup == null) + { + return; + } + + // If this fails, the group is being removed and completing cancellation is not necessary + if (requestGroup.requestRemoveSemaphore.tryAcquire()) + { + // Prevent adding requests in case the group will be removed by this cancellation + requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE); + requestGroup.requestRemoveSemaphore.release(); + + serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release(); + + FullDataSourceRequestMessage requestMessage = requestGroup.requestMessages.remove(msg.futureId); + if (requestGroup.requestMessages.isEmpty()) + { + NETWORK_LOGGER.debug("[" + this.serverLevelWrapper.getDimensionName() + "] Cancelled request group [" + DhSectionPos.toString(requestMessage.sectionPos) + "]."); + this.requestGroupByPos.remove(requestMessage.sectionPos); + this.serverside.fullDataFileHandler.removeRetrievalRequestIf(pos -> pos == requestMessage.sectionPos); + } + else + { + requestGroup.requestAddSemaphore.release(Short.MAX_VALUE); + } + } + }); + } + private void queueLodSyncForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet) + { + if (!serverPlayerState.sessionConfig.getSynchronizeOnLogin()) + { + message.sendResponse(new RequestRejectedException("Operation is disabled in config.")); + return; + } + + if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(message)) + { + return; + } + + + // the client timestamp will be null if we want to retrieve the LOD regardless of when it was last updated + long clientTimestamp = (message.clientTimestamp != null) ? message.clientTimestamp : -1; + // the server timestamp will be null if no LOD data exists for this position + Long serverTimestamp = this.serverside.fullDataFileHandler.getTimestampForPos(message.sectionPos); + if (serverTimestamp == null + || serverTimestamp <= clientTimestamp) + { + // either no data exists to sync, or the client is already up to date + rateLimiterSet.syncOnLoginRateLimiter.release(); + message.sendResponse(new FullDataSourceResponseMessage(null)); + return; + } + + + + ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); + if (executor == null) + { + // shouldn't normally happen, but just in case + LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null"); + return; + } + + this.serverside.fullDataFileHandler.getAsync(message.sectionPos).thenAcceptAsync(fullDataSource -> + { + rateLimiterSet.syncOnLoginRateLimiter.release(); + + FullDataPayload payload = new FullDataPayload(fullDataSource); + payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, message.getSession()::sendMessage); + message.sendResponse(new FullDataSourceResponseMessage(payload)); + }, executor); + } + private void queueWorldGenForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet) + { + if (!serverPlayerState.sessionConfig.isDistantGenerationEnabled()) + { + message.sendResponse(new RequestRejectedException("Operation is disabled in config.")); + return; + } + + if (!rateLimiterSet.generationRequestRateLimiter.tryAcquire(message)) + { + return; + } + + while (true) + { + DataSourceRequestGroup requestGroup = this.requestGroupByPos.computeIfAbsent(message.sectionPos, pos -> + { + DataSourceRequestGroup newGroup = new DataSourceRequestGroup(); + this.tryFulfillDataSourceRequestGroup(newGroup, pos); + NETWORK_LOGGER.debug("[" + this.serverLevelWrapper.getDimensionName() + "] Created request group for pos [" + DhSectionPos.toString(pos) + "]."); + return newGroup; + }); + + // If this fails, loop until either a permit is acquired or the group is removed to create another one + if (!requestGroup.requestAddSemaphore.tryAcquire()) + { + Thread.yield(); + continue; + } + + this.requestGroupByFutureId.put(message.futureId, requestGroup); + requestGroup.requestMessages.put(message.futureId, message); + requestGroup.requestAddSemaphore.release(); + break; + } + } + + + /** May send an error message in response if the message is a {@link AbstractTrackableMessage} */ + private boolean messagePlayerInThisLevel(T message) + { + if (!(message instanceof ILevelRelatedMessage)) + { + LodUtil.assertNotReach("Received message [" + ILevelRelatedMessage.class.getSimpleName() + "] does not implement [" + message.getClass().getSimpleName() + "]"); + } + + // Only handle requests for this level + if (!((ILevelRelatedMessage) message).isSameLevelAs(this.getServerLevelWrapper())) + { + return false; + } + + LodUtil.assertTrue(message.getSession().serverPlayer != null); + + // Check if the player is in this dimension, + // since handling multiple dimensions isn't allowed + if (message.getSession().serverPlayer.getLevel() != this.getLevelWrapper()) + { + // If the message can be replied to - reply with an error, otherwise just ignore + if (message instanceof AbstractTrackableMessage) + { + ((AbstractTrackableMessage) message).sendResponse( + new InvalidLevelException( + "Generation not allowed. " + + "Requested dimension: [" + ((ILevelRelatedMessage) message).getLevelName() + "], " + + "player dimension: [" + message.getSession().serverPlayer.getLevel().getDimensionName() + "], " + + "handler dimension: [" + this.getLevelWrapper().getDimensionName() + "]" + ) + ); + } + + return false; + } + + + return true; + } + + + + //===========// + // world gen // + //===========// + + @Override + public void onWorldGenTaskComplete(long pos) + { + DataSourceRequestGroup requestGroup = this.requestGroupByPos.get(pos); + if (requestGroup != null) + { + this.tryFulfillDataSourceRequestGroup(requestGroup, pos); + } + } + + private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos) + { + this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource -> + { + if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps)) + { + requestGroup.fullDataSource = fullDataSource; + } + else + { + this.serverside.fullDataFileHandler.queuePositionForRetrieval(pos); + } + }); + } + + + + //=================// + // player handling // + //=================// + + public void addPlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.add(serverPlayer); } + public void removePlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.remove(serverPlayer); } + + @Override + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) + { + if (!Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates.get()) + { + return this.getFullDataProvider().updateDataSourceAsync(data); + } + + ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); + if (executor == null) + { + LOGGER.warn("Unable to send FullDataPartialUpdateMessage - getNetworkCompressionExecutor() is null"); + return this.getFullDataProvider().updateDataSourceAsync(data); + } + CompletableFuture.runAsync(() -> + { + FullDataPayload payload = new FullDataPayload(data); + for (ServerPlayerState serverPlayerState : this.serverPlayerStateManager.getConnectedPlayers()) + { + if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper) + { + continue; + } + + if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled()) + { + continue; + } + + Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition(); + int distanceFromPlayer = DhSectionPos.getManhattanBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16; + if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance() + && distanceFromPlayer <= serverPlayerState.sessionConfig.getRenderDistanceRadius()) + { + payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, serverPlayerState.networkSession::sendMessage); + serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload)); + } + } + }, executor); + + + return this.getFullDataProvider().updateDataSourceAsync(data); + } + + + + //=========// + // getters // + //=========// + + @Override + public int getMinY() { return this.getLevelWrapper().getMinHeight(); } + + @Override + public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; } + + @Override + public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); } + + @Override + public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } + + @Override + public AbstractSaveStructure getSaveStructure() { return this.serverside.saveStructure; } + + @Override + public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } + + + + //==========// + // shutdown // + //==========// + + @Override + public void close() + { + super.close(); + this.serverside.close(); + LOGGER.info("Closed DHLevel for [" + this.getLevelWrapper() + "]."); + } + + + + //================// + // helper classes // + //================// + + private static class DataSourceRequestGroup + { + public final ConcurrentMap requestMessages = new ConcurrentHashMap<>(); + + @CheckForNull + public FullDataSourceV2 fullDataSource; + + /** + * These semaphores prevent a given thread from accidentally locking on the same group + * multiple times, as the semaphore is tied to the given thread.
+ * Reentrant Lock isn't used since it would allow the thread to lock on the same group.
+ * the Short.MAX_VALUE is just a very large number that should be larger than the number of + * threads we'll have. + */ + public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true); + /** @see DataSourceRequestGroup#requestAddSemaphore */ + public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true); + + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java index c409b502f..56a03f798 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientLevel.java @@ -52,6 +52,7 @@ import javax.annotation.CheckForNull; import java.awt.*; import java.io.File; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; /** The level used when connected to a server */ @@ -108,7 +109,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel this.dataFileHandler = new RemoteFullDataSourceProvider(this, saveStructure, fullDataSaveDirOverride, this.syncOnLoginRequestQueue); this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Client.Advanced.WorldGenerator.enableDistantGeneration); - this.worldGenModule = new WorldGenModule(this); + this.worldGenModule = new WorldGenModule(this, this.dataFileHandler, () -> new WorldGenState(this, networkState)); this.clientside = new ClientLevelModule(this); @@ -171,7 +172,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel } @Override - public void worldGenTick() + public boolean shouldDoWorldGen() { ClientNetworkState networkState = this.networkState; @@ -182,33 +183,23 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel isAllowedDimension = MC_CLIENT.getWrappedClientLevel() == this.levelWrapper; } - boolean shouldDoWorldGen = isClientUsable + return isClientUsable && networkState.sessionConfig.isDistantGenerationEnabled() && isAllowedDimension && this.clientside.isRendering(); - - boolean isWorldGenRunning = this.worldGenModule.isWorldGenRunning(); - if (shouldDoWorldGen && !isWorldGenRunning) - { - // start world gen - this.worldGenModule.startWorldGen(this.dataFileHandler, new WorldGenState(this, networkState)); - - // populate the queue based on the current rendering tree - ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); - renderState.quadtree.leafNodeIterator().forEachRemaining(node -> { - this.dataFileHandler.getAsync(node.sectionPos); - }); - } - else if (!shouldDoWorldGen && isWorldGenRunning) - { - // stop world gen - this.worldGenModule.stopWorldGen(this.dataFileHandler); - } - - if (this.worldGenModule.isWorldGenRunning()) - { - this.worldGenModule.worldGenTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); - } + } + + @Override + @Nullable + public DhBlockPos2D getTargetPosForGeneration() + { + return new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()); + } + + @Override + public void worldGenTick() + { + this.worldGenModule.worldGenTick(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java index ef307e589..6085f9d61 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhClientServerLevel.java @@ -22,12 +22,11 @@ package com.seibel.distanthorizons.core.level; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; -import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.logging.f3.F3Screen; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; @@ -36,24 +35,20 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrap import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; -import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.util.List; import java.util.concurrent.CompletableFuture; /** The level used on a singleplayer world */ -public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLevel, IDhServerLevel +public class DhClientServerLevel extends AbstractDhServerLevel implements IDhClientLevel { - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); - public final ServerLevelModule serverside; public final ClientLevelModule clientside; - - private final IServerLevelWrapper serverLevelWrapper; + private int localPlayerWorldGenPosInQueue = 0; @@ -61,20 +56,13 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev // constructor // //=============// - public DhClientServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper) + public DhClientServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, ServerPlayerStateManager serverPlayerStateManager) { - if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs()) - { - LOGGER.warn("unable to create data folder."); - } - this.serverLevelWrapper = serverLevelWrapper; - this.serverLevelWrapper.setParentLevel(this); - this.serverside = new ServerLevelModule(this, saveStructure); - this.clientside = new ClientLevelModule(this); - this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile); - this.runRepoReliantSetup(); + super(saveStructure, serverLevelWrapper, serverPlayerStateManager, false); - LOGGER.info("Started " + DhClientServerLevel.class.getSimpleName() + " for " + serverLevelWrapper + " with saves at " + saveStructure); + this.serverLevelWrapper.setParentLevel(this); + this.clientside = new ClientLevelModule(this); + this.runRepoReliantSetup(); } @@ -95,32 +83,26 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev { this.clientside.renderDeferred(renderEventParam, profiler); } @Override - public void serverTick() { } - - @Override - public void worldGenTick() + public boolean shouldDoWorldGen() { - this.serverside.worldGeneratorEnabledConfig.pollNewValue(); // if not called the get() line below may not - boolean shouldDoWorldGen = this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering(); - boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning(); - if (shouldDoWorldGen && !isWorldGenRunning) - { - // start world gen - this.serverside.worldGenModule.startWorldGen(this.serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); - } - else if (!shouldDoWorldGen && isWorldGenRunning) - { - // stop world gen - this.serverside.worldGenModule.stopWorldGen(this.serverside.fullDataFileHandler); - } - - if (isWorldGenRunning) - { - this.serverside.worldGenModule.worldGenTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); - } + return this.serverside.worldGeneratorEnabledConfig.get() && this.clientside.isRendering() || !this.worldGenPlayerCenteringQueue.isEmpty(); } - + @Override + @Nullable + public DhBlockPos2D getTargetPosForGeneration() + { + if (this.localPlayerWorldGenPosInQueue > 0) + { + this.localPlayerWorldGenPosInQueue--; + return super.getTargetPosForGeneration(); + } + else + { + this.localPlayerWorldGenPosInQueue = this.worldGenPlayerCenteringQueue.size(); + return new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()); + } + } //========// // render // @@ -157,28 +139,14 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev public void clearRenderCache() { this.clientside.clearRenderCache(); } @Override - public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; } - @Override - public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); } - - @Override - public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } - - @Override - public AbstractSaveStructure getSaveStructure() + public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { - return this.serverside.saveStructure; + return CompletableFuture.allOf( + super.updateDataSourcesAsync(data), + this.clientside.updateDataSourcesAsync(data) + ); } - @Override - public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } - - @Override - public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) { return this.clientside.updateDataSourcesAsync(data); } - - @Override - public int getMinY() { return this.getLevelWrapper().getMinHeight(); } - //===========// @@ -247,6 +215,8 @@ public class DhClientServerLevel extends AbstractDhLevel implements IDhClientLev @Override public void onWorldGenTaskComplete(long pos) { + super.onWorldGenTaskComplete(pos); + DebugRenderer.makeParticle( new DebugRenderer.BoxParticle( new DebugRenderer.Box(pos, 128f, 156f, 0.09f, Color.red.darker()), diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java index f146d42b7..3fd3b0e80 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/DhServerLevel.java @@ -19,86 +19,23 @@ package com.seibel.distanthorizons.core.level; -import com.seibel.distanthorizons.core.config.Config; -import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; -import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.structure.AbstractSaveStructure; -import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; -import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; -import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.multiplayer.server.RemotePlayerConnectionHandler; -import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; -import com.seibel.distanthorizons.core.network.exceptions.InvalidLevelException; -import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException; -import com.seibel.distanthorizons.core.network.messages.ILevelRelatedMessage; -import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; -import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage; -import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; -import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPayload; -import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage; -import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage; -import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage; -import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; -import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.util.math.Vec3d; -import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; -import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; -import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import javax.annotation.CheckForNull; import java.util.List; -import java.util.Map; -import java.util.concurrent.*; -public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel +public class DhServerLevel extends AbstractDhServerLevel { - private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - private static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(), - () -> Config.Client.Advanced.Logging.logNetworkEvent.get()); - - /** 1 Mebibyte minus 576 bytes for other info */ - public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000; - - public final ServerLevelModule serverside; - private final IServerLevelWrapper serverLevelWrapper; - - private final RemotePlayerConnectionHandler remotePlayerConnectionHandler; - - /** - * This queue is used for ensuring fair generation speed for each player.
- * Every tick the first player gets used for centering generation, and then is immediately moved into the back of the queue.
- * TODO only add players that actually have something to generate - */ - private final ConcurrentLinkedQueue worldGenPlayerCenteringQueue = new ConcurrentLinkedQueue<>(); - - private final ConcurrentMap requestGroupByPos = new ConcurrentHashMap<>(); - private final ConcurrentMap requestGroupByFutureId = new ConcurrentHashMap<>(); - - - //=============// // constructor // //=============// - public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, RemotePlayerConnectionHandler remotePlayerConnectionHandler) + public DhServerLevel(AbstractSaveStructure saveStructure, IServerLevelWrapper serverLevelWrapper, ServerPlayerStateManager serverPlayerStateManager) { - if (saveStructure.getFullDataFolder(serverLevelWrapper).mkdirs()) - { - LOGGER.warn("unable to create data folder."); - } - this.serverLevelWrapper = serverLevelWrapper; - this.serverside = new ServerLevelModule(this, saveStructure); - this.createAndSetSupportingRepos(this.serverside.fullDataFileHandler.repo.databaseFile); - this.runRepoReliantSetup(); - - LOGGER.info("Started DHLevel for ["+serverLevelWrapper+"] at ["+saveStructure+"]."); - - this.remotePlayerConnectionHandler = remotePlayerConnectionHandler; + super(saveStructure, serverLevelWrapper, serverPlayerStateManager); } @@ -108,355 +45,9 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel //=======// @Override - public void serverTick() + public boolean shouldDoWorldGen() { - // Send finished data source requests - for (Map.Entry entry : this.requestGroupByPos.entrySet()) - { - DataSourceRequestGroup requestGroup = entry.getValue(); - - if (requestGroup.fullDataSource == null) - { - continue; - } - - NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Fulfilled request group ["+entry.getKey()+"]"); - - // Make this group unavailable for adding into - this.requestGroupByPos.remove(entry.getKey()); - requestGroup.requestRemoveSemaphore.acquireUninterruptibly(Short.MAX_VALUE); - requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE); - - ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); - if (executor == null) - { - LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null"); - continue; - } - CompletableFuture.runAsync(() -> - { - FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource); - for (FullDataSourceRequestMessage msg : requestGroup.requestMessages.values()) - { - this.requestGroupByFutureId.remove(msg.futureId); - - ServerPlayerState serverPlayerState = this.remotePlayerConnectionHandler.getConnectedPlayer(msg.serverPlayer()); - if (serverPlayerState == null) - { - continue; - } - - serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release(); - payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, msg.getSession()::sendMessage); - msg.sendResponse(new FullDataSourceResponseMessage(payload)); - } - }, executor); - } - } - - @Override - public void worldGenTick() - { - boolean shouldDoWorldGen = true; //todo; - boolean isWorldGenRunning = this.serverside.worldGenModule.isWorldGenRunning(); - if (shouldDoWorldGen && !isWorldGenRunning) - { - // start world gen - this.serverside.worldGenModule.startWorldGen(this.serverside.fullDataFileHandler, new ServerLevelModule.WorldGenState(this)); - } - else if (!shouldDoWorldGen && isWorldGenRunning) - { - // stop world gen - this.serverside.worldGenModule.stopWorldGen(this.serverside.fullDataFileHandler); - } - - if (this.serverside.worldGenModule.isWorldGenRunning()) - { - IServerPlayerWrapper firstPlayer = this.worldGenPlayerCenteringQueue.peek(); - if (firstPlayer == null) - { - return; - } - - // Put first player in back before removing from front, so it can be removed by other thread without blocking - // - if it gets removed, remove() below will remove the item we just put instead - this.worldGenPlayerCenteringQueue.add(firstPlayer); - this.worldGenPlayerCenteringQueue.remove(firstPlayer); - - Vec3d position = firstPlayer.getPosition(); - this.serverside.worldGenModule.worldGenTick(new DhBlockPos2D((int) position.x, (int) position.z)); - } - } - - - - //==================// - // network handling // - //==================// - - public void registerNetworkHandlers(ServerPlayerState serverPlayerState) - { - serverPlayerState.networkSession.registerHandler(FullDataSourceRequestMessage.class, (message) -> - { - if (!this.messagePlayerInThisLevel(message)) - { - // we can't handle players in other levels, don't continue - return; - } - - - ServerPlayerState.RateLimiterSet rateLimiterSet = serverPlayerState.getRateLimiterSet(this); - - if (message.clientTimestamp == null) - { - this.queueWorldGenForRequestMessage(serverPlayerState, message, rateLimiterSet); - } - else - { - this.queueLodSyncForRequestMessage(serverPlayerState, message, rateLimiterSet); - } - }); - - - serverPlayerState.networkSession.registerHandler(CancelMessage.class, msg -> - { - DataSourceRequestGroup requestGroup = this.requestGroupByFutureId.remove(msg.futureId); - if (requestGroup == null) - { - return; - } - - // If this fails, the group is being removed and completing cancellation is not necessary - if (requestGroup.requestRemoveSemaphore.tryAcquire()) - { - // Prevent adding requests in case the group will be removed by this cancellation - requestGroup.requestAddSemaphore.acquireUninterruptibly(Short.MAX_VALUE); - requestGroup.requestRemoveSemaphore.release(); - - serverPlayerState.getRateLimiterSet(this).generationRequestRateLimiter.release(); - - FullDataSourceRequestMessage requestMessage = requestGroup.requestMessages.remove(msg.futureId); - if (requestGroup.requestMessages.isEmpty()) - { - NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Cancelled request group ["+DhSectionPos.toString(requestMessage.sectionPos)+"]."); - this.requestGroupByPos.remove(requestMessage.sectionPos); - this.serverside.fullDataFileHandler.removeRetrievalRequestIf(pos -> pos == requestMessage.sectionPos); - } - else - { - requestGroup.requestAddSemaphore.release(Short.MAX_VALUE); - } - } - }); - } - private void queueLodSyncForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet) - { - if (!serverPlayerState.sessionConfig.getSynchronizeOnLogin()) - { - message.sendResponse(new RequestRejectedException("Operation is disabled in config.")); - return; - } - - if (!rateLimiterSet.syncOnLoginRateLimiter.tryAcquire(message)) - { - return; - } - - - // the client timestamp will be null if we want to retrieve the LOD regardless of when it was last updated - long clientTimestamp = (message.clientTimestamp != null) ? message.clientTimestamp : -1; - // the server timestamp will be null if no LOD data exists for this position - Long serverTimestamp = this.serverside.fullDataFileHandler.getTimestampForPos(message.sectionPos); - if (serverTimestamp == null - || serverTimestamp <= clientTimestamp) - { - // either no data exists to sync, or the client is already up to date - rateLimiterSet.syncOnLoginRateLimiter.release(); - message.sendResponse(new FullDataSourceResponseMessage(null)); - return; - } - - - - ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); - if (executor == null) - { - // shouldn't normally happen, but just in case - LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null"); - return; - } - - this.serverside.fullDataFileHandler.getAsync(message.sectionPos).thenAcceptAsync(fullDataSource -> - { - rateLimiterSet.syncOnLoginRateLimiter.release(); - - FullDataPayload payload = new FullDataPayload(fullDataSource); - payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, message.getSession()::sendMessage); - message.sendResponse(new FullDataSourceResponseMessage(payload)); - }, executor); - } - private void queueWorldGenForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet) - { - if (!serverPlayerState.sessionConfig.isDistantGenerationEnabled()) - { - message.sendResponse(new RequestRejectedException("Operation is disabled in config.")); - return; - } - - if (!rateLimiterSet.generationRequestRateLimiter.tryAcquire(message)) - { - return; - } - - while (true) - { - DataSourceRequestGroup requestGroup = this.requestGroupByPos.computeIfAbsent(message.sectionPos, pos -> - { - DataSourceRequestGroup newGroup = new DataSourceRequestGroup(); - this.tryFulfillDataSourceRequestGroup(newGroup, pos); - NETWORK_LOGGER.debug("["+this.serverLevelWrapper.getDimensionName()+"] Created request group for pos ["+DhSectionPos.toString(pos)+"]."); - return newGroup; - }); - - // If this fails, loop until either a permit is acquired or the group is removed to create another one - if (!requestGroup.requestAddSemaphore.tryAcquire()) - { - Thread.yield(); - continue; - } - - this.requestGroupByFutureId.put(message.futureId, requestGroup); - requestGroup.requestMessages.put(message.futureId, message); - requestGroup.requestAddSemaphore.release(); - break; - } - } - - - /** May send an error message in response if the message is a {@link AbstractTrackableMessage} */ - private boolean messagePlayerInThisLevel(T message) - { - if (!(message instanceof ILevelRelatedMessage)) - { - LodUtil.assertNotReach("Received message ["+ILevelRelatedMessage.class.getSimpleName()+"] does not implement ["+message.getClass().getSimpleName()+"]"); - } - - // Only handle requests for this level - if (!((ILevelRelatedMessage) message).isSameLevelAs(this.getServerLevelWrapper())) - { - return false; - } - - LodUtil.assertTrue(message.getSession().serverPlayer != null); - - // Check if the player is in this dimension, - // since handling multiple dimensions isn't allowed - if (message.getSession().serverPlayer.getLevel() != this.getLevelWrapper()) - { - // If the message can be replied to - reply with an error, otherwise just ignore - if (message instanceof AbstractTrackableMessage) - { - ((AbstractTrackableMessage) message).sendResponse( - new InvalidLevelException( - "Generation not allowed. " + - "Requested dimension: ["+((ILevelRelatedMessage) message).getLevelName()+"], " + - "player dimension: ["+message.getSession().serverPlayer.getLevel().getDimensionName()+"], " + - "handler dimension: ["+this.getLevelWrapper().getDimensionName()+"]" - ) - ); - } - - return false; - } - - - return true; - } - - - - //===========// - // world gen // - //===========// - - @Override - public void onWorldGenTaskComplete(long pos) - { - DataSourceRequestGroup requestGroup = this.requestGroupByPos.get(pos); - if (requestGroup != null) - { - this.tryFulfillDataSourceRequestGroup(requestGroup, pos); - } - } - - private void tryFulfillDataSourceRequestGroup(DataSourceRequestGroup requestGroup, long pos) - { - this.serverside.fullDataFileHandler.getAsync(pos).thenAccept(fullDataSource -> - { - if (this.serverside.fullDataFileHandler.isFullyGenerated(fullDataSource.columnGenerationSteps)) - { - requestGroup.fullDataSource = fullDataSource; - } - else - { - this.serverside.fullDataFileHandler.queuePositionForRetrieval(pos); - } - }); - } - - - - //=================// - // player handling // - //=================// - - public void addPlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.add(serverPlayer); } - public void removePlayer(IServerPlayerWrapper serverPlayer) { this.worldGenPlayerCenteringQueue.remove(serverPlayer); } - - - - @Override - public CompletableFuture updateDataSourcesAsync(FullDataSourceV2 data) - { - if (!Config.Client.Advanced.Multiplayer.ServerNetworking.enableRealTimeUpdates.get()) - { - return this.getFullDataProvider().updateDataSourceAsync(data); - } - - ThreadPoolExecutor executor = ThreadPoolUtil.getNetworkCompressionExecutor(); - if (executor == null) - { - LOGGER.warn("Unable to send FullDataPartialUpdateMessage - getNetworkCompressionExecutor() is null"); - return this.getFullDataProvider().updateDataSourceAsync(data); - } - CompletableFuture.runAsync(() -> - { - FullDataPayload payload = new FullDataPayload(data); - for (ServerPlayerState serverPlayerState : this.remotePlayerConnectionHandler.getConnectedPlayers()) - { - if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper) - { - continue; - } - - if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled()) - { - continue; - } - - Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition(); - int distanceFromPlayer = DhSectionPos.getManhattanBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16; - if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance() - && distanceFromPlayer <= serverPlayerState.sessionConfig.getRenderDistanceRadius()) - { - payload.splitAndSend(FULL_DATA_SPLIT_SIZE_IN_BYTES, serverPlayerState.networkSession::sendMessage); - serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload)); - } - } - }, executor); - - - return this.getFullDataProvider().updateDataSourceAsync(data); + return true; //todo; } @@ -465,24 +56,6 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel // getters // //=========// - @Override - public int getMinY() { return this.getLevelWrapper().getMinHeight(); } - - @Override - public IServerLevelWrapper getServerLevelWrapper() { return this.serverLevelWrapper; } - - @Override - public ILevelWrapper getLevelWrapper() { return this.getServerLevelWrapper(); } - - @Override - public FullDataSourceProviderV2 getFullDataProvider() { return this.serverside.fullDataFileHandler; } - - @Override - public AbstractSaveStructure getSaveStructure() { return this.serverside.saveStructure; } - - @Override - public boolean hasSkyLight() { return this.serverLevelWrapper.hasSkyLight(); } - @Override public GenericObjectRenderer getGenericRenderer() { @@ -523,30 +96,4 @@ public class DhServerLevel extends AbstractDhLevel implements IDhServerLevel LOGGER.info("Closed DHLevel for ["+this.getLevelWrapper()+"]."); } - - - //================// - // helper classes // - //================// - - private static class DataSourceRequestGroup - { - public final ConcurrentMap requestMessages = new ConcurrentHashMap<>(); - - @CheckForNull - public FullDataSourceV2 fullDataSource; - - /** - * These semaphores prevent a given thread from accidentally locking on the same group - * multiple times, as the semaphore is tied to the given thread.
- * Reentrant Lock isn't used since it would allow the thread to lock on the same group.
- * the Short.MAX_VALUE is just a very large number that should be larger than the number of - * threads we'll have. - */ - public final Semaphore requestAddSemaphore = new Semaphore(Short.MAX_VALUE, true); - /** @see DataSourceRequestGroup#requestAddSemaphore */ - public final Semaphore requestRemoveSemaphore = new Semaphore(Short.MAX_VALUE, true); - - } - } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java index 7ca1f3e09..9da620788 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/ServerLevelModule.java @@ -34,7 +34,7 @@ public class ServerLevelModule implements AutoCloseable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - public final IDhServerLevel parentServerLevel; + private final IDhServerLevel parentServerLevel; public final AbstractSaveStructure saveStructure; public final GeneratedFullDataSourceProvider fullDataFileHandler; public final AppliedConfigState worldGeneratorEnabledConfig; @@ -53,7 +53,7 @@ public class ServerLevelModule implements AutoCloseable this.saveStructure = saveStructure; this.fullDataFileHandler = new GeneratedFullDataSourceProvider(parentServerLevel, saveStructure); this.worldGeneratorEnabledConfig = new AppliedConfigState<>(Config.Client.Advanced.WorldGenerator.enableDistantGeneration); - this.worldGenModule = new WorldGenModule(this.parentServerLevel); + this.worldGenModule = new WorldGenModule(this.parentServerLevel, this.fullDataFileHandler, () -> new ServerLevelModule.WorldGenState(this.parentServerLevel)); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java index 5ddf8729a..9b0c4716d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java @@ -25,11 +25,15 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import java.io.Closeable; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; /** * Handles both single-player/server-side world gen and client side LOD requests. @@ -41,6 +45,9 @@ public class WorldGenModule implements Closeable private final GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener; + private final GeneratedFullDataSourceProvider dataSourceProvider; + private final Supplier worldGenStateSupplier; + private final AtomicReference worldGenStateRef = new AtomicReference<>(); @@ -49,9 +56,15 @@ public class WorldGenModule implements Closeable // constructor // //=============// - public WorldGenModule(GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener) + public WorldGenModule( + GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener, + GeneratedFullDataSourceProvider dataSourceProvider, + Supplier worldGenStateSupplier + ) { this.onWorldGenCompleteListener = onWorldGenCompleteListener; + this.dataSourceProvider = dataSourceProvider; + this.worldGenStateSupplier = worldGenStateSupplier; } @@ -95,13 +108,33 @@ public class WorldGenModule implements Closeable dataFileHandler.removeWorldGenCompleteListener(this.onWorldGenCompleteListener); } - /** @param targetPosForGeneration the position that world generation should be centered around */ - public void worldGenTick(DhBlockPos2D targetPosForGeneration) + public void worldGenTick() { - AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); - if (worldGenState != null) + boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen(); + + boolean isWorldGenRunning = this.isWorldGenRunning(); + if (shouldDoWorldGen && !isWorldGenRunning) { - worldGenState.startGenerationQueueAndSetTargetPos(targetPosForGeneration); + // start world gen + this.startWorldGen(this.dataSourceProvider, this.worldGenStateSupplier.get()); + } + else if (!shouldDoWorldGen && isWorldGenRunning) + { + // stop world gen + this.stopWorldGen(this.dataSourceProvider); + } + + if (this.isWorldGenRunning()) + { + AbstractWorldGenState worldGenState = this.worldGenStateRef.get(); + if (worldGenState != null) + { + DhBlockPos2D targetPosForGeneration = this.onWorldGenCompleteListener.getTargetPosForGeneration(); + if (targetPosForGeneration != null) + { + worldGenState.startGenerationQueueAndSetTargetPos(targetPosForGeneration); + } + } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java index dfb1205a4..c5674a763 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerState.java @@ -2,7 +2,7 @@ package com.seibel.distanthorizons.core.multiplayer.server; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; -import com.seibel.distanthorizons.core.level.DhServerLevel; +import com.seibel.distanthorizons.core.level.AbstractDhServerLevel; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.network.messages.base.CurrentLevelKeyMessage; import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage; @@ -33,8 +33,8 @@ public class ServerPlayerState implements Closeable @NotNull public final SessionConfig sessionConfig = new SessionConfig(); - private final ConcurrentHashMap rateLimiterSets = new ConcurrentHashMap<>(); - public RateLimiterSet getRateLimiterSet(DhServerLevel level) { return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet()); } + private final ConcurrentHashMap rateLimiterSets = new ConcurrentHashMap<>(); + public RateLimiterSet getRateLimiterSet(AbstractDhServerLevel level) { return this.rateLimiterSets.computeIfAbsent(level, ignored -> new RateLimiterSet()); } public void clearRateLimiterSets() { this.rateLimiterSets.clear(); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerStateManager.java similarity index 61% rename from core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java rename to core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerStateManager.java index 2ae9e1290..51ac789f1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/RemotePlayerConnectionHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/server/ServerPlayerStateManager.java @@ -9,11 +9,12 @@ import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; -public class RemotePlayerConnectionHandler +public class ServerPlayerStateManager { private final ConcurrentMap connectedPlayerStateByPlayerWrapper = new ConcurrentHashMap<>(); - private final ConcurrentMap> messageQueueByPlayerWrapper = new ConcurrentHashMap<>(); + private final ConcurrentMap messageQueueByPlayerWrapper = new ConcurrentHashMap<>(); @@ -26,17 +27,8 @@ public class RemotePlayerConnectionHandler ServerPlayerState playerState = new ServerPlayerState(serverPlayer); this.connectedPlayerStateByPlayerWrapper.put(serverPlayer, playerState); - Queue queuedMessages = this.messageQueueByPlayerWrapper.get(serverPlayer); - if (queuedMessages != null) - { - NetworkSession networkSession = playerState.networkSession; - for (AbstractNetworkMessage message : queuedMessages) - { - networkSession.tryHandleMessage(message); - } - - this.messageQueueByPlayerWrapper.remove(serverPlayer); - } + MessageQueueState messageQueue = this.messageQueueByPlayerWrapper.computeIfAbsent(serverPlayer, k -> new MessageQueueState()); + this.handlePluginMessagesFromQueue(playerState, messageQueue); return playerState; } @@ -58,14 +50,24 @@ public class RemotePlayerConnectionHandler public void handlePluginMessage(IServerPlayerWrapper player, AbstractNetworkMessage message) { + MessageQueueState messageQueue = this.messageQueueByPlayerWrapper.computeIfAbsent(player, k -> new MessageQueueState()); + messageQueue.messageQueue.add(message); + ServerPlayerState playerState = this.connectedPlayerStateByPlayerWrapper.get(player); if (playerState != null) { - playerState.networkSession.tryHandleMessage(message); + this.handlePluginMessagesFromQueue(playerState, messageQueue); } - else + } + + private void handlePluginMessagesFromQueue(ServerPlayerState playerState, MessageQueueState messageQueueState) + { + while (!messageQueueState.messageQueue.isEmpty() && messageQueueState.isBeingDrained.compareAndSet(false, true)) { - this.messageQueueByPlayerWrapper.computeIfAbsent(player, k -> new ConcurrentLinkedQueue<>()).add(message); + AbstractNetworkMessage message = messageQueueState.messageQueue.poll(); + playerState.networkSession.tryHandleMessage(message); + + messageQueueState.isBeingDrained.set(false); } } @@ -80,6 +82,12 @@ public class RemotePlayerConnectionHandler public Iterable getConnectedPlayers() { return this.connectedPlayerStateByPlayerWrapper.values(); } + private static class MessageQueueState + { + public final Queue messageQueue = new ConcurrentLinkedQueue<>(); + public final AtomicBoolean isBeingDrained = new AtomicBoolean(); + + } } \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java new file mode 100644 index 000000000..d5bb12dea --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhServerWorld.java @@ -0,0 +1,120 @@ +package com.seibel.distanthorizons.core.world; + +import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; +import com.seibel.distanthorizons.core.level.AbstractDhServerLevel; +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; + +public abstract class AbstractDhServerWorld extends AbstractDhWorld implements IDhServerWorld +{ + protected final HashMap levelWrapperByDhLevel = new HashMap<>(); + public final LocalSaveStructure saveStructure = new LocalSaveStructure(); + + private final ServerPlayerStateManager serverPlayerStateManager; + + public AbstractDhServerWorld(EWorldEnvironment worldEnvironment) + { + super(worldEnvironment); + this.serverPlayerStateManager = new ServerPlayerStateManager(); + } + + + //=================// + // player handling // + //=================// + + + @Override + public ServerPlayerStateManager getServerPlayerStateManager() + { + return this.serverPlayerStateManager; + } + + @Override + public void addPlayer(IServerPlayerWrapper serverPlayer) + { + ServerPlayerState playerState = this.serverPlayerStateManager.registerJoinedPlayer(serverPlayer); + this.getLevel(serverPlayer.getLevel()).addPlayer(serverPlayer); + + for (TDhServerLevel level : this.levelWrapperByDhLevel.values()) + { + level.registerNetworkHandlers(playerState); + } + } + + @Override + public void removePlayer(IServerPlayerWrapper serverPlayer) + { + this.getLevel(serverPlayer.getLevel()).removePlayer(serverPlayer); + this.serverPlayerStateManager.unregisterLeftPlayer(serverPlayer); + + // If player's left, session is already closed + } + + @Override + public void changePlayerLevel(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel) + { + this.getLevel(destinationLevel).addPlayer(player); + this.getLevel(originLevel).removePlayer(player); + } + + + + //================// + // level handling // + //================// + + @Override + public TDhServerLevel getLevel(@NotNull ILevelWrapper wrapper) { return this.levelWrapperByDhLevel.get(wrapper); } + @Override + public Iterable getAllLoadedLevels() { return this.levelWrapperByDhLevel.values(); } + @Override + public int getLoadedLevelCount() { return this.levelWrapperByDhLevel.size(); } + + + + //==============// + // tick methods // + //==============// + + @Override + public void serverTick() { this.levelWrapperByDhLevel.values().forEach(TDhServerLevel::serverTick); } + + @Override + public void worldGenTick() { this.levelWrapperByDhLevel.values().forEach(TDhServerLevel::worldGenTick); } + + + + //================// + // base overrides // + //================// + + @Override + public void close() + { + for (TDhServerLevel level : this.levelWrapperByDhLevel.values()) + { + LOGGER.info("Unloading level [" + level.getLevelWrapper().getDimensionName() + "]."); + + // level wrapper shouldn't be null, but just in case + IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper(); + if (serverLevelWrapper != null) + { + serverLevelWrapper.onUnload(); + } + + level.close(); + } + + this.levelWrapperByDhLevel.clear(); + LOGGER.info("Closed DhWorld of type [" + this.environment + "]."); + } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java index a9d238398..41f9ce6df 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhClientServerWorld.java @@ -19,8 +19,6 @@ package com.seibel.distanthorizons.core.world; -import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; -import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.DhClientServerLevel; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.EventLoop; @@ -31,15 +29,14 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapp import org.jetbrains.annotations.NotNull; import java.io.File; -import java.util.HashMap; +import java.util.Collections; import java.util.HashSet; +import java.util.Set; import java.util.concurrent.ExecutorService; -public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWorld, IDhServerWorld +public class DhClientServerWorld extends AbstractDhServerWorld implements IDhClientWorld { - private final HashMap levelWrapperByDhLevel = new HashMap<>(); - private final HashSet dhLevels = new HashSet<>(); - public final LocalSaveStructure saveStructure = new LocalSaveStructure(); + private final Set dhLevels = Collections.synchronizedSet(new HashSet<>()); public ExecutorService dhTickerThread = ThreadUtil.makeSingleThreadPool("Client Server World Ticker Thread", 2); public EventLoop eventLoop = new EventLoop(this.dhTickerThread, this::_clientTick); //TODO: Rate-limit the loop @@ -71,7 +68,7 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor { File levelFile = this.saveStructure.getLevelFolder(levelWrapper); LodUtil.assertTrue(levelFile != null); - DhClientServerLevel level = new DhClientServerLevel(this.saveStructure, (IServerLevelWrapper) levelWrapper); + DhClientServerLevel level = new DhClientServerLevel(this.saveStructure, (IServerLevelWrapper) levelWrapper, this.getServerPlayerStateManager()); this.dhLevels.add(level); return level; }); @@ -102,14 +99,6 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor } } - @Override - public DhClientServerLevel getLevel(@NotNull ILevelWrapper wrapper) { return this.levelWrapperByDhLevel.get(wrapper); } - - @Override - public Iterable getAllLoadedLevels() { return this.dhLevels; } - @Override - public int getLoadedLevelCount() { return this.dhLevels.size(); } - @Override public void unloadLevel(@NotNull ILevelWrapper wrapper) { @@ -140,16 +129,12 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor this.dhLevels.forEach(DhClientServerLevel::clientTick); } - public void clientTick() + @Override public void clientTick() { //LOGGER.info("Client world tick"); this.eventLoop.tick(); } - public void serverTick() { this.dhLevels.forEach(DhClientServerLevel::serverTick); } - - public void worldGenTick() { this.dhLevels.forEach(DhClientServerLevel::worldGenTick); } - //================// @@ -160,22 +145,22 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor @Override public synchronized void close() { - // clear dhLevels to prevent concurrent modification errors - HashSet levelsToClose = new HashSet<>(this.dhLevels); - this.dhLevels.clear(); - // close each level - for (DhClientServerLevel level : levelsToClose) + synchronized (this.dhLevels) { - LOGGER.info("Unloading level " + level.getServerLevelWrapper().getDimensionName()); - - // level wrapper shouldn't be null, but just in case - IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper(); - if (serverLevelWrapper != null) + // close each level + for (DhClientServerLevel level : this.dhLevels) { - serverLevelWrapper.onUnload(); + LOGGER.info("Unloading level " + level.getServerLevelWrapper().getDimensionName()); + + // level wrapper shouldn't be null, but just in case + IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper(); + if (serverLevelWrapper != null) + { + serverLevelWrapper.onUnload(); + } + + level.close(); } - - level.close(); } this.levelWrapperByDhLevel.clear(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java index 22905066b..96ad5f872 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhServerWorld.java @@ -19,29 +19,16 @@ package com.seibel.distanthorizons.core.world; -import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure; import com.seibel.distanthorizons.core.level.DhServerLevel; -import com.seibel.distanthorizons.core.level.IDhLevel; -import com.seibel.distanthorizons.core.multiplayer.server.RemotePlayerConnectionHandler; -import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState; import com.seibel.distanthorizons.core.util.LodUtil; -import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import org.jetbrains.annotations.NotNull; import java.io.File; -import java.util.HashMap; -public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld +public class DhServerWorld extends AbstractDhServerWorld { - private final HashMap levels; - public final LocalSaveStructure saveStructure; - - public final RemotePlayerConnectionHandler remotePlayerConnectionHandler; - - - //==============// // constructors // //==============// @@ -49,48 +36,10 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld public DhServerWorld() { super(EWorldEnvironment.Server_Only); - - this.saveStructure = new LocalSaveStructure(); - this.levels = new HashMap<>(); - - this.remotePlayerConnectionHandler = new RemotePlayerConnectionHandler(); - LOGGER.info("Started ["+DhServerWorld.class.getSimpleName()+"] of type ["+this.environment+"]."); } - - //=================// - // player handling // - //=================// - - public void addPlayer(IServerPlayerWrapper serverPlayer) - { - ServerPlayerState playerState = this.remotePlayerConnectionHandler.registerJoinedPlayer(serverPlayer); - this.getLevel(serverPlayer.getLevel()).addPlayer(serverPlayer); - - for (DhServerLevel level : this.levels.values()) - { - level.registerNetworkHandlers(playerState); - } - } - - public void removePlayer(IServerPlayerWrapper serverPlayer) - { - this.getLevel(serverPlayer.getLevel()).removePlayer(serverPlayer); - this.remotePlayerConnectionHandler.unregisterLeftPlayer(serverPlayer); - - // If player's left, session is already closed - } - - public void changePlayerLevel(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel) - { - this.getLevel(destinationLevel).addPlayer(player); - this.getLevel(originLevel).removePlayer(player); - } - - - //================// // level handling // //================// @@ -103,30 +52,14 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld return null; } - return this.levels.computeIfAbsent((IServerLevelWrapper) wrapper, (serverLevelWrapper) -> + return this.levelWrapperByDhLevel.computeIfAbsent(wrapper, (serverLevelWrapper) -> { File levelFile = this.saveStructure.getLevelFolder(wrapper); LodUtil.assertTrue(levelFile != null); - return new DhServerLevel(this.saveStructure, serverLevelWrapper, this.remotePlayerConnectionHandler); + return new DhServerLevel(this.saveStructure, (IServerLevelWrapper) serverLevelWrapper, this.getServerPlayerStateManager()); }); } - @Override - public DhServerLevel getLevel(@NotNull ILevelWrapper wrapper) - { - if (!(wrapper instanceof IServerLevelWrapper)) - { - return null; - } - - return this.levels.get(wrapper); - } - - @Override - public Iterable getAllLoadedLevels() { return this.levels.values(); } - @Override - public int getLoadedLevelCount() { return this.levels.size(); } - @Override public void unloadLevel(@NotNull ILevelWrapper wrapper) { @@ -135,51 +68,12 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld return; } - if (this.levels.containsKey(wrapper)) + if (this.levelWrapperByDhLevel.containsKey(wrapper)) { - LOGGER.info("Unloading level {} ", this.levels.get(wrapper)); + LOGGER.info("Unloading level {} ", this.levelWrapperByDhLevel.get(wrapper)); wrapper.onUnload(); - this.levels.remove(wrapper).close(); + this.levelWrapperByDhLevel.remove(wrapper).close(); } } - - - //==============// - // tick methods // - //==============// - - @Override - public void serverTick() { this.levels.values().forEach(DhServerLevel::serverTick); } - - @Override - public void worldGenTick() { this.levels.values().forEach(DhServerLevel::worldGenTick); } - - - - //================// - // base overrides // - //================// - - @Override - public void close() - { - for (DhServerLevel level : this.levels.values()) - { - LOGGER.info("Unloading level [" + level.getLevelWrapper().getDimensionName() + "]."); - - // level wrapper shouldn't be null, but just in case - IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper(); - if (serverLevelWrapper != null) - { - serverLevelWrapper.onUnload(); - } - - level.close(); - } - - this.levels.clear(); - LOGGER.info("Closed DhWorld of type [" + this.environment + "]."); - } - } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java index 17bd2fcbf..d71a510f5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/world/IDhServerWorld.java @@ -20,11 +20,18 @@ package com.seibel.distanthorizons.core.world; import com.seibel.distanthorizons.core.level.IDhServerLevel; +import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; /** Used both for dedicated server and singleplayer worlds */ public interface IDhServerWorld extends IDhWorld { + ServerPlayerStateManager getServerPlayerStateManager(); + void addPlayer(IServerPlayerWrapper serverPlayer); + void removePlayer(IServerPlayerWrapper serverPlayer); + void changePlayerLevel(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel); void serverTick(); default IDhServerLevel getOrLoadServerLevel(ILevelWrapper levelWrapper) { return (IDhServerLevel) this.getOrLoadLevel(levelWrapper); }