refactor and rename
This commit is contained in:
+3
-3
@@ -1,15 +1,15 @@
|
||||
package com.seibel.distanthorizons.api.methods.events.interfaces;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Cailin
|
||||
*/
|
||||
public interface IDhServerMessageRecieved<T> extends IDhApiEvent<T>
|
||||
public interface IDhServerMessageReceived<T> extends IDhApiEvent<T>
|
||||
{
|
||||
/**
|
||||
* Triggered when a plugin message is received from the server.
|
||||
* @param channel The name of the channel this was received on.
|
||||
* @param message The message sent from the server.
|
||||
*/
|
||||
void serverMessageRecieved(String channel, byte[] message);
|
||||
void serverMessageReceived(String channel, byte[] message);
|
||||
|
||||
}
|
||||
@@ -21,10 +21,9 @@ 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.IServerEnhancedClientLevel;
|
||||
import com.seibel.distanthorizons.core.level.IEnhancedServerManager;
|
||||
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
|
||||
import com.seibel.distanthorizons.core.level.IKeyedClientLevelManager;
|
||||
import com.seibel.distanthorizons.core.world.*;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IFriendlyByteBuf;
|
||||
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
|
||||
import com.seibel.distanthorizons.core.level.IDhClientLevel;
|
||||
import com.seibel.distanthorizons.core.config.Config;
|
||||
@@ -45,20 +44,19 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* This holds the methods that should be called
|
||||
* by the host mod loader (Fabric, Forge, etc.).
|
||||
* Specifically for the client.
|
||||
*
|
||||
* @author James Seibel
|
||||
* @version 2022-9-16
|
||||
*/
|
||||
public class ClientApi
|
||||
{
|
||||
@@ -70,8 +68,7 @@ public class 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 IEnhancedServerManager ENHANCED_SERVER_MANAGER
|
||||
= SingletonInjector.INSTANCE.get(IEnhancedServerManager.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);
|
||||
|
||||
@@ -131,8 +128,8 @@ public class ClientApi
|
||||
}
|
||||
this.isServerCommunicationEnabled = false;
|
||||
this.serverIsMalformed = false;
|
||||
ENHANCED_SERVER_MANAGER.setUseOverrideWrapper(false);
|
||||
ENHANCED_SERVER_MANAGER.registerServerEnhancedLevel(null);
|
||||
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(false);
|
||||
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,15 +174,21 @@ public class ClientApi
|
||||
|
||||
public void clientLevelLoadEvent(IClientLevelWrapper level)
|
||||
{
|
||||
if (ENABLE_EVENT_LOGGING) {
|
||||
if (this.isServerCommunicationEnabled) {
|
||||
if (this.isServerCommunicationEnabled)
|
||||
{
|
||||
if (ENABLE_EVENT_LOGGING)
|
||||
{
|
||||
LOGGER.info("Server supports communication, deferring loading.");
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (ENABLE_EVENT_LOGGING)
|
||||
{
|
||||
LOGGER.info("Client level " + level + " loading.");
|
||||
}
|
||||
|
||||
|
||||
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
|
||||
if (world != null)
|
||||
{
|
||||
@@ -194,11 +197,11 @@ public class ClientApi
|
||||
}
|
||||
}
|
||||
|
||||
public void serverLevelLoadEvent(IServerEnhancedClientLevel level)
|
||||
public void serverLevelLoadEvent(IServerKeyedClientLevel level)
|
||||
{
|
||||
if (ENABLE_EVENT_LOGGING)
|
||||
{
|
||||
LOGGER.info("Server level " + level + " (" + level.getServerWorldKey() + ") loading.");
|
||||
LOGGER.info("Server level " + level + " (" + level.getServerLevelKey() + ") loading.");
|
||||
}
|
||||
|
||||
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
|
||||
@@ -258,54 +261,65 @@ public class ClientApi
|
||||
}
|
||||
profiler.pop();
|
||||
}
|
||||
|
||||
public void serverMessageReceived(IFriendlyByteBuf buf)
|
||||
|
||||
/** @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.serverIsMalformed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
short commandLength = buf.readShort();
|
||||
if(commandLength > 32) {
|
||||
|
||||
short commandLength = byteBuf.readShort();
|
||||
if (commandLength > 32)
|
||||
{
|
||||
LOGGER.error("Server sent command > 32");
|
||||
ClientApi.INSTANCE.serverIsMalformed = true;
|
||||
return;
|
||||
}
|
||||
String eventType = buf.readCharSequence(commandLength, Charset.forName("UTF-8")).toString();
|
||||
switch(eventType) {
|
||||
|
||||
String eventType = byteBuf.readCharSequence(commandLength, StandardCharsets.UTF_8).toString();
|
||||
switch (eventType)
|
||||
{
|
||||
case "ServerCommsEnabled":
|
||||
LOGGER.info("Server supports DH protocol.");
|
||||
ClientApi.INSTANCE.isServerCommunicationEnabled = true;
|
||||
ENHANCED_SERVER_MANAGER.setUseOverrideWrapper(true);
|
||||
MC.execute(() -> {
|
||||
KEYED_CLIENT_LEVEL_MANAGER.setUseOverrideWrapper(true);
|
||||
MC.executeOnRenderThread(() -> {
|
||||
// Go ahead and unload the current world, because it may be wrong. We expect
|
||||
// a followup WorldChanged event from the server soon anyways.
|
||||
clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
|
||||
this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
|
||||
});
|
||||
break;
|
||||
|
||||
case "WorldChanged":
|
||||
short worldKeyLength = buf.readShort();
|
||||
if(worldKeyLength > 128) {
|
||||
short worldKeyLength = byteBuf.readShort();
|
||||
if (worldKeyLength > 128)
|
||||
{
|
||||
LOGGER.error("Server sent worldKey > 128");
|
||||
this.serverIsMalformed = true;
|
||||
return;
|
||||
}
|
||||
String worldKey = buf.readCharSequence(worldKeyLength, Charset.forName("UTF-8")).toString();
|
||||
if(!worldKey.matches("[a-zA-Z0-9_]+")) {
|
||||
|
||||
String worldKey = byteBuf.readCharSequence(worldKeyLength, StandardCharsets.UTF_8).toString();
|
||||
if (!worldKey.matches("[a-zA-Z0-9_]+"))
|
||||
{
|
||||
LOGGER.error("Server sent invalid world key name, and is being ignored.");
|
||||
this.isServerCommunicationEnabled = false;
|
||||
this.serverIsMalformed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Server sent world change event: " + worldKey);
|
||||
MC.execute(() -> {
|
||||
if(MC.getWrappedClientWorld() != null) {
|
||||
clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
|
||||
MC.executeOnRenderThread(() -> {
|
||||
if (MC.getWrappedClientWorld() != null)
|
||||
{
|
||||
this.clientLevelUnloadEvent((IClientLevelWrapper) MC.getWrappedClientWorld());
|
||||
}
|
||||
IServerEnhancedClientLevel clientLevel
|
||||
= ENHANCED_SERVER_MANAGER.getServerEnhancedLevel(MC.getWrappedClientWorld(), worldKey);
|
||||
ENHANCED_SERVER_MANAGER.registerServerEnhancedLevel(clientLevel);
|
||||
serverLevelLoadEvent(clientLevel);
|
||||
IServerKeyedClientLevel clientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel(MC.getWrappedClientWorld(), worldKey);
|
||||
KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel);
|
||||
this.serverLevelLoadEvent(clientLevel);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
+57
-38
@@ -5,10 +5,11 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
|
||||
import com.seibel.distanthorizons.core.file.subDimMatching.SubDimensionLevelMatcher;
|
||||
import com.seibel.distanthorizons.core.config.Config;
|
||||
import com.seibel.distanthorizons.api.enums.config.EServerFolderNameMode;
|
||||
import com.seibel.distanthorizons.core.level.IServerEnhancedClientLevel;
|
||||
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
|
||||
import com.seibel.distanthorizons.core.util.objects.ParsedIp;
|
||||
import com.seibel.distanthorizons.core.util.LodUtil;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -26,17 +27,17 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
|
||||
{
|
||||
final File folder;
|
||||
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
|
||||
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
|
||||
public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
|
||||
|
||||
SubDimensionLevelMatcher fileMatcher = null;
|
||||
final HashMap<ILevelWrapper, File> levelToFileMap = new HashMap<>();
|
||||
SubDimensionLevelMatcher subDimMatcher = null;
|
||||
final HashMap<ILevelWrapper, File> levelWrapperToFileMap = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
public ClientOnlySaveStructure()
|
||||
{
|
||||
this.folder = new File(MC_CLIENT.getGameDirectory().getPath() +
|
||||
File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + getServerFolderName());
|
||||
this.folder = new File(getSaveStructureFolderPath());
|
||||
|
||||
if (!this.folder.exists())
|
||||
{
|
||||
@@ -55,42 +56,51 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
|
||||
//================//
|
||||
|
||||
@Override
|
||||
public File getLevelFolder(ILevelWrapper level)
|
||||
public File getLevelFolder(ILevelWrapper levelWrapper)
|
||||
{
|
||||
return this.levelToFileMap.computeIfAbsent(level, (newLevel) ->
|
||||
return this.levelWrapperToFileMap.computeIfAbsent(levelWrapper, (newLevelWrapper) ->
|
||||
{
|
||||
if (newLevel instanceof IServerEnhancedClientLevel) {
|
||||
IServerEnhancedClientLevel secl = (IServerEnhancedClientLevel) newLevel;
|
||||
// Use the server provided key if one was provided
|
||||
if (newLevelWrapper instanceof IServerKeyedClientLevel)
|
||||
{
|
||||
IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper;
|
||||
LOGGER.info("Loading level "+newLevelWrapper.getDimensionType().getDimensionName()+" with key: "+keyedClientLevel.getServerLevelKey());
|
||||
// This world was identified by the server directly, so we can know for sure which folder to use.
|
||||
File seclFolder = new File(this.folder.getParent(), MC_CLIENT.getCurrentServerIp().toString());
|
||||
seclFolder = new File(seclFolder, secl.getServerWorldKey());
|
||||
return seclFolder;
|
||||
return new File(getSaveStructureFolderPath() + File.separatorChar + keyedClientLevel.getServerLevelKey());
|
||||
}
|
||||
|
||||
if (Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent.get() == 0)
|
||||
|
||||
|
||||
// use multiverse matching if enabled
|
||||
if (Config.Client.Advanced.Multiplayer.multiverseSimilarityRequiredPercent.get() != 0)
|
||||
{
|
||||
if (this.fileMatcher != null)
|
||||
// create the matcher if one doesn't exist
|
||||
if (this.subDimMatcher == null || !this.subDimMatcher.isFindingLevel(newLevelWrapper))
|
||||
{
|
||||
this.fileMatcher.close();
|
||||
this.fileMatcher = null;
|
||||
LOGGER.info("Loading level " + newLevelWrapper.getDimensionType().getDimensionName());
|
||||
this.subDimMatcher = new SubDimensionLevelMatcher(newLevelWrapper, this.folder,
|
||||
this.getMatchingLevelFolders(newLevelWrapper).toArray(new File[0] /* surprisingly we don't need to create an array of any specific size for this to work */));
|
||||
}
|
||||
return this.getLevelFolderWithoutSimilarityMatching(newLevel);
|
||||
|
||||
File levelFile = this.subDimMatcher.tryGetLevel();
|
||||
if (levelFile != null)
|
||||
{
|
||||
this.subDimMatcher.close();
|
||||
this.subDimMatcher = null;
|
||||
}
|
||||
return levelFile;
|
||||
}
|
||||
|
||||
if (this.fileMatcher == null || !this.fileMatcher.isFindingLevel(newLevel))
|
||||
|
||||
// we aren't using multiverse matching, shut down the matcher
|
||||
// TODO this additional call may not be needed
|
||||
if (this.subDimMatcher != null)
|
||||
{
|
||||
LOGGER.info("Loading level for world " + newLevel.getDimensionType().getDimensionName());
|
||||
this.fileMatcher = new SubDimensionLevelMatcher(newLevel, this.folder,
|
||||
this.getMatchingLevelFolders(newLevel).toArray(new File[0] /* surprisingly we don't need to create an array of any specific size for this to work */));
|
||||
this.subDimMatcher.close();
|
||||
this.subDimMatcher = null;
|
||||
}
|
||||
|
||||
File levelFile = this.fileMatcher.tryGetLevel();
|
||||
if (levelFile != null)
|
||||
{
|
||||
this.fileMatcher.close();
|
||||
this.fileMatcher = null;
|
||||
}
|
||||
return levelFile;
|
||||
|
||||
|
||||
// get the default folder
|
||||
return this.getLevelFolderWithoutSimilarityMatching(newLevelWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,7 +148,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
|
||||
@Override
|
||||
public File getRenderCacheFolder(ILevelWrapper level)
|
||||
{
|
||||
File levelFolder = this.levelToFileMap.get(level);
|
||||
File levelFolder = this.levelWrapperToFileMap.get(level);
|
||||
if (levelFolder == null)
|
||||
{
|
||||
return null;
|
||||
@@ -150,7 +160,7 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
|
||||
@Override
|
||||
public File getFullDataFolder(ILevelWrapper level)
|
||||
{
|
||||
File levelFolder = this.levelToFileMap.get(level);
|
||||
File levelFolder = this.levelWrapperToFileMap.get(level);
|
||||
if (levelFolder == null)
|
||||
{
|
||||
return null;
|
||||
@@ -182,7 +192,16 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
|
||||
// a valid level folder needs to have DH specific folders in it
|
||||
return files != null && files.length != 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String getSaveStructureFolderPath()
|
||||
{
|
||||
String path = MC_SHARED.getInstallationDirectory().getPath() + File.separatorChar
|
||||
+ "Distant_Horizons_server_data" + File.separatorChar
|
||||
+ getServerFolderName();
|
||||
return path;
|
||||
}
|
||||
|
||||
/** Generated from the server the client is currently connected to. */
|
||||
private static String getServerFolderName()
|
||||
{
|
||||
@@ -222,15 +241,15 @@ public class ClientOnlySaveStructure extends AbstractSaveStructure
|
||||
// This fixes some issues when the server is named something in other languages
|
||||
return new PercentEscaper("", true).escape(folderName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//==================//
|
||||
// override methods //
|
||||
//==================//
|
||||
|
||||
@Override
|
||||
public void close() { this.fileMatcher.close(); }
|
||||
public void close() { this.subDimMatcher.close(); }
|
||||
|
||||
@Override
|
||||
public String toString() { return "[" + this.getClass().getSimpleName() + "@" + this.folder.getName() + "]"; }
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.seibel.distanthorizons.core.level;
|
||||
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
|
||||
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
|
||||
|
||||
public interface IEnhancedServerManager extends IBindable {
|
||||
/**
|
||||
* Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals.
|
||||
* @param clientLevel
|
||||
*/
|
||||
void registerServerEnhancedLevel(IServerEnhancedClientLevel clientLevel);
|
||||
|
||||
/**
|
||||
* Returns a new instance of a ServerEnhancedClientLevel.
|
||||
* @param level
|
||||
* @param worldKey
|
||||
* @return
|
||||
*/
|
||||
IServerEnhancedClientLevel getServerEnhancedLevel(ILevelWrapper level, String worldKey);
|
||||
|
||||
/**
|
||||
* Sets the LOD engine to use the override wrapper, if the server has communication enabled.
|
||||
* @param useOverrideWrapper
|
||||
*/
|
||||
void setUseOverrideWrapper(boolean useOverrideWrapper);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.seibel.distanthorizons.core.level;
|
||||
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
|
||||
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
|
||||
|
||||
/**
|
||||
* Handles level overrides initiated by servers that
|
||||
* support differentiating between different levels.
|
||||
*/
|
||||
public interface IKeyedClientLevelManager extends IBindable
|
||||
{
|
||||
/** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */
|
||||
void setServerKeyedLevel(IServerKeyedClientLevel clientLevel);
|
||||
IServerKeyedClientLevel getOverrideWrapper();
|
||||
|
||||
/** Returns a new instance of a ServerEnhancedClientLevel. */
|
||||
IServerKeyedClientLevel getServerKeyedLevel(ILevelWrapper level, String serverLevelKey);
|
||||
|
||||
/** Sets the LOD engine to use the override wrapper, if the server has communication enabled. */
|
||||
void setUseOverrideWrapper(boolean useOverrideWrapper);
|
||||
boolean getUseOverrideWrapper();
|
||||
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.seibel.distanthorizons.core.level;
|
||||
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
|
||||
|
||||
/**
|
||||
* Enhances an IClientLevelWrapper with server provided world information.
|
||||
*/
|
||||
public interface IServerEnhancedClientLevel extends IClientLevelWrapper
|
||||
{
|
||||
/**
|
||||
* Returns the world key, which is used to select the correct folder on the client.
|
||||
* @return
|
||||
*/
|
||||
String getServerWorldKey();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.seibel.distanthorizons.core.level;
|
||||
|
||||
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
|
||||
|
||||
/** Enhances a {@link IClientLevelWrapper} with server provided level information. */
|
||||
public interface IServerKeyedClientLevel extends IClientLevelWrapper
|
||||
{
|
||||
/** Returns the level key, which is used to select the correct folder on the client. */
|
||||
String getServerLevelKey();
|
||||
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Interface that wraps the net.minecraft.network.FriendlyByteBuffer.
|
||||
*/
|
||||
public interface IFriendlyByteBuf {
|
||||
|
||||
short readShort();
|
||||
|
||||
CharSequence readCharSequence(int length, Charset charset);
|
||||
}
|
||||
+2
-5
@@ -139,10 +139,7 @@ public interface IMinecraftClientWrapper extends IBindable
|
||||
|
||||
Object getOptionsObject();
|
||||
|
||||
/**
|
||||
* Executes a task on the Minecraft render thread.
|
||||
* @param runnable
|
||||
*/
|
||||
void execute(Runnable runnable);
|
||||
/** Executes the given task on Minecraft's render thread. */
|
||||
void executeOnRenderThread(Runnable runnable);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user