Fix Forge client-side multiplayer

This commit is contained in:
James Seibel
2023-07-27 21:35:03 -05:00
parent 3062c29230
commit 895a0db542
4 changed files with 200 additions and 116 deletions
@@ -21,9 +21,10 @@ package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.world.*;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
@@ -52,6 +53,8 @@ import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
/**
@@ -62,53 +65,71 @@ import java.util.concurrent.TimeUnit;
public class ClientApi
{
private static final Logger LOGGER = LogManager.getLogger();
public static final boolean ENABLE_EVENT_LOGGING = true;
public static boolean prefLoggerEnabled = false;
public static final ClientApi INSTANCE = new ClientApi();
public static TestRenderer testRenderer = new TestRenderer();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
public static final long SPAM_LOGGER_FLUSH_NS = TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS);
private boolean configOverrideReminderPrinted = false;
public boolean rendererDisabledBecauseOfExceptions = false;
private long lastFlushNanoTime = 0;
private boolean isServerCommunicationEnabled = true;
private boolean serverIsMalformed = false;
private boolean isServerCommunicationEnabled = false;
/** set to true if any unexpected responses are received from the server */
private boolean serverNetworkingIsMalformed = false;
/** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */
private final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>();
/** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */
private final HashMap<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>();
//==============//
// constructors //
//==============//
private ClientApi()
{
}
//========//
// events //
//========//
private ClientApi() { }
//==============//
// world events //
//==============//
/**
* May be fired slightly before or after the associated
* {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} event
* depending on how the host mod loader functions.
*/
public void onClientOnlyConnected()
{
// only continue if the client is connected to a different server
if (MC.clientConnectedToDedicatedServer())
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Client on ClientOnly mode connecting.");
}
LOGGER.info("Client on ClientOnly mode connecting.");
// firing after clientLevelLoadEvent
// TODO if level has prepped to load it should fire level load event
SharedApi.setDhWorld(new DhClientWorld());
LOGGER.info("Loading ["+this.waitingClientLevels.size()+"] waiting client level wrappers.");
for (IClientLevelWrapper level : this.waitingClientLevels)
{
this.clientLevelLoadEvent(level);
}
this.waitingClientLevels.clear();
}
}
@@ -119,21 +140,94 @@ public class ClientApi
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Client on ClientOnly mode disconnecting.");
}
LOGGER.info("Client on ClientOnly mode disconnecting.");
world.close();
SharedApi.setDhWorld(null);
}
// clear the previous server's information
this.isServerCommunicationEnabled = false;
this.serverIsMalformed = false;
this.serverNetworkingIsMalformed = false;
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false);
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null);
// remove any waiting items
this.waitingChunkByClientLevelAndPos.clear();
this.waitingClientLevels.clear();
}
}
//==============//
// level events //
//==============//
public void clientLevelUnloadEvent(IClientLevelWrapper level)
{
LOGGER.info("Client level "+level+" unloading.");
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.unloadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
}
}
public void clientLevelLoadEvent(IClientLevelWrapper level)
{
if (this.isServerCommunicationEnabled)
{
LOGGER.info("Server supports communication, deferring loading.");
return;
}
LOGGER.info("Client level " + level + " loading.");
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.getOrLoadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
this.loadWaitingChunksForLevel(level);
}
else
{
this.waitingClientLevels.add(level);
}
}
private void loadWaitingChunksForLevel(IClientLevelWrapper level)
{
HashSet<Pair<IClientLevelWrapper, DhChunkPos>> keysToRemove = new HashSet<>();
for (Pair<IClientLevelWrapper, DhChunkPos> levelChunkPair : this.waitingChunkByClientLevelAndPos.keySet())
{
// only load chunks that came from this level
IClientLevelWrapper levelWrapper = levelChunkPair.first;
if (levelWrapper.equals(level))
{
IChunkWrapper chunkWrapper = this.waitingChunkByClientLevelAndPos.get(levelChunkPair);
this.applyChunkUpdate(chunkWrapper, levelWrapper);
keysToRemove.add(levelChunkPair);
}
}
LOGGER.info("Loaded ["+keysToRemove.size()+"] waiting chunk wrappers.");
for (Pair<IClientLevelWrapper, DhChunkPos> keyToRemove : keysToRemove)
{
this.waitingChunkByClientLevelAndPos.remove(keyToRemove);
}
}
//=======================//
// chunk modified events //
//=======================//
public void clientChunkLoadEvent(IChunkWrapper chunk, IClientLevelWrapper level) { this.applyChunkUpdate(chunk, level); }
public void clientChunkSaveEvent(IChunkWrapper chunk, IClientLevelWrapper level) { this.applyChunkUpdate(chunk, level); }
private void applyChunkUpdate(IChunkWrapper chunk, IClientLevelWrapper level)
@@ -144,10 +238,14 @@ public class ClientApi
return;
}
// only continue if the level is still loaded
// only continue if the level is loaded
IDhLevel dhLevel = SharedApi.getAbstractDhWorld().getLevel(level);
if (dhLevel == null)
{
// If the level isn't loaded yet, keep track of which chunks were loaded so we can use them later.
// This may happen if the world and level load events happen out of order
this.waitingChunkByClientLevelAndPos.replace(new Pair<>(level, chunk.getChunkPos()), chunk);
return;
}
@@ -169,81 +267,25 @@ public class ClientApi
}
}
public void clientLevelUnloadEvent(IClientLevelWrapper level)
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Client level "+level+" unloading.");
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.unloadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
}
}
public void clientLevelLoadEvent(IClientLevelWrapper level)
{
if (this.isServerCommunicationEnabled)
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Server supports communication, deferring loading.");
}
return;
}
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Client level " + level + " loading.");
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.getOrLoadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
}
}
public void serverLevelLoadEvent(IServerKeyedClientLevel level)
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Server level " + level + " (" + level.getServerLevelKey() + ") loading.");
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.getOrLoadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(level));
}
}
//===============//
// render events //
//===============//
public void rendererShutdownEvent()
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Renderer shutting down.");
}
LOGGER.info("Renderer shutting down.");
IProfilerWrapper profiler = MC.getProfiler();
profiler.push("DH-RendererShutdown");
profiler.pop();
}
public void rendererStartupEvent()
{
if (ENABLE_EVENT_LOGGING)
{
LOGGER.info("Renderer starting up.");
}
LOGGER.info("Renderer starting up.");
IProfilerWrapper profiler = MC.getProfiler();
profiler.push("DH-RendererStartup");
@@ -252,7 +294,7 @@ public class ClientApi
GLProxy.getInstance();
profiler.pop();
}
public void clientTickEvent()
{
IProfilerWrapper profiler = MC.getProfiler();
@@ -275,24 +317,41 @@ public class ClientApi
profiler.pop();
}
//============//
// networking //
//============//
/** @param byteBuf is Netty's {@link ByteBuffer} wrapper. */
public void serverMessageReceived(ByteBuf byteBuf)
{
// It is important to ensure malicious server input is ignored.
if (this.serverIsMalformed)
if (this.serverNetworkingIsMalformed)
{
return;
}
short commandLength = byteBuf.readShort();
if (commandLength > 32) // TODO 32 should be put into a constant somewhere
if (commandLength > 32) // TODO 32 should be put into a constant somewhere, what does it represent?
{
LOGGER.error("Server sent command > 32");
ClientApi.INSTANCE.serverIsMalformed = true;
ClientApi.INSTANCE.serverNetworkingIsMalformed = true;
return;
}
String eventType = null;
try
{
eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString();
}
catch (Exception e)
{
LOGGER.error("Server sent un-parsable command. Error: "+e.getMessage());
return;
}
String eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString();
switch (eventType)
{
case "ServerCommsEnabled":
@@ -305,13 +364,13 @@ public class ClientApi
this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
});
break;
case "WorldChanged":
short worldKeyLength = byteBuf.readShort();
if (worldKeyLength > 128) // TODO 128 should be put into a constant somewhere
{
LOGGER.error("Server sent worldKey > 128");
this.serverIsMalformed = true;
this.serverNetworkingIsMalformed = true;
return;
}
@@ -320,7 +379,7 @@ public class ClientApi
{
LOGGER.error("Server sent invalid world key name, and is being ignored.");
this.isServerCommunicationEnabled = false;
this.serverIsMalformed = true;
this.serverNetworkingIsMalformed = true;
return;
}
@@ -332,18 +391,18 @@ public class ClientApi
}
IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), worldKey);
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel);
this.serverLevelLoadEvent(clientLevel);
this.clientLevelLoadEvent(clientLevel);
});
break;
}
}
//===========//
// rendering //
//===========//
public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks)
{
if (ModInfo.IS_DEV_BUILD && !this.configOverrideReminderPrinted && MC.playerExists())
@@ -72,7 +72,7 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
{
LOGGER.warn("Unable to create render data folder, file saving may fail.");
}
fileHandlerThreadPool = ThreadUtil.makeSingleThreadPool("Render Source File Handler ["+this.level.getClientLevelWrapper().getDimensionType().getDimensionName()+"]");
this.fileHandlerThreadPool = ThreadUtil.makeSingleThreadPool("Render Source File Handler ["+this.level.getLevelWrapper().getDimensionType().getDimensionName()+"]");
this.threadPoolMsg = new F3Screen.NestedMessage(this::f3Log);
@@ -257,7 +257,9 @@ public class ClientLevelModule implements Closeable {
this.renderSourceFileHandler = new RenderSourceFileHandler(fullDataSourceProvider, dhClientLevel, saveStructure);
this.quadtree = new LodQuadTree(dhClientLevel, Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistance.get() * LodUtil.CHUNK_WIDTH,
MC_CLIENT.getPlayerBlockPos().x, MC_CLIENT.getPlayerBlockPos().z, this.renderSourceFileHandler);
// initial position is (0,0) just in case the player hasn't loaded in yet, the tree will be moved once the level starts ticking
0, 0,
this.renderSourceFileHandler);
RenderBufferHandler renderBufferHandler = new RenderBufferHandler(this.quadtree);
this.renderer = new LodRenderer(renderBufferHandler);
@@ -0,0 +1,23 @@
package com.seibel.distanthorizons.core.util.objects;
import java.util.Objects;
/** A simple way to hold 2 objects together */
public final class Pair<T, U>
{
public final T first;
public final U second;
public Pair(T first, U second)
{
this.second = second;
this.first = first;
}
@Override
public String toString() { return "("+this.first+", "+this.second+")"; }
@Override
public int hashCode() { return Objects.hash(this.first, this.second); }
}