Compare commits

..

30 Commits

Author SHA1 Message Date
James Seibel 1b066327a8 remove dev from the version number 2026-04-24 06:50:47 -05:00
James Seibel 43d0a971f7 add todo commented code 2026-04-24 06:50:00 -05:00
James Seibel 9e60c698de move before render pass events into render api 2026-04-23 17:54:33 -05:00
James Seibel bf2affa6d1 Fix "fog" rendering when underwater with Iris 2026-04-23 17:39:40 -05:00
James Seibel 98f6cea86a Fix near clip plane to close with shaders 2026-04-23 17:09:24 -05:00
James Seibel 9ae01dc1f8 up api version 6.0.0 -> 6.1.0 2026-04-23 07:42:21 -05:00
James Seibel 40efc5cbf3 Add alpha to DhApiBlockColorOverrideEvent 2026-04-23 07:42:07 -05:00
James Seibel 66bba1c80a add opacity to API block state wrapper 2026-04-23 07:41:51 -05:00
James Seibel d9f3b31cc5 Add timeout to CSV block culling configs 2026-04-22 18:48:21 -05:00
James Seibel e465ef5325 Fix flashing when moving over root node boundaries 2026-04-22 18:36:09 -05:00
s809 225385a43f Clean up received payload buffer check a bit 2026-04-23 00:26:32 +05:00
James Seibel 7d7d07416b Fix quad tree unit tests 2026-04-22 07:41:44 -05:00
James Seibel 5ef308cbee fix rare race condition preventing world gen 2026-04-21 22:19:57 -05:00
James Seibel d61b601c14 fix potential exceptions after world shutdown 2026-04-21 22:19:46 -05:00
James Seibel 246c679a97 Maybe fix native GL crash due to buffer free 2026-04-21 21:40:20 -05:00
James Seibel 4b317a8e00 Fix garbage collector warning not using config 2026-04-21 19:59:17 -05:00
James Seibel 1debd4b875 Improve node out-of-bound logic
This fixes some overlapping rendering issues, fixes LOD generating outside of render distance, and fixes low-detail LODs flashing when moving into previously-explored LODs
2026-04-21 19:49:50 -05:00
James Seibel 5dcda31990 Try fixing LOD flashing/stuck low details 2026-04-21 07:48:07 -05:00
James Seibel ae16ed2341 Revert "Fix LODs loading outside render distance"
This reverts commit 2c266d2495.
2026-04-20 21:32:00 -05:00
James Seibel 2c266d2495 Fix LODs loading outside render distance
Fixes !1233
2026-04-19 21:48:26 -05:00
James Seibel 7e40546bc5 fix world gen not canceling for far away pos 2026-04-19 21:10:20 -05:00
James Seibel 5d391c83ea quad tree region/comment cleanup 2026-04-19 21:07:42 -05:00
James Seibel 0895bf53e3 renderDataPointUtil toString cleanup 2026-04-18 21:45:24 -05:00
James Seibel a7203f8f33 up version number 3.0.1 -> 3.0.2 2026-04-18 21:44:17 -05:00
James Seibel 22efbb211a remove dev from version number 2026-04-18 21:43:37 -05:00
James Seibel 95c4459d8a compile separate combined API jar 2026-04-18 15:47:23 -05:00
James Seibel 0ef11caaf2 API jar auto include sources 2026-04-18 12:07:24 -05:00
James Seibel 2e3dfab6c3 fix api javadoc compiling 2026-04-18 12:06:14 -05:00
James Seibel 42be139e94 remove google-collect 2026-04-18 11:07:12 -05:00
James Seibel f866e7f8e3 up version number 3.0.0 -> 3.0.1 2026-04-18 10:36:52 -05:00
26 changed files with 628 additions and 264 deletions
+39 -19
View File
@@ -23,43 +23,42 @@ dependencies {
testImplementation "junit:junit:4.13" testImplementation "junit:junit:4.13"
} }
shadowJar { java {
// required for basic shadowJar setup withSourcesJar()
configurations = [project.configurations.shadow]
} }
task addSourcesToCompiledJar(type: ShadowJar) { task createReleaseApiJar(type: ShadowJar) {
mustRunAfter sourcesJar
dependsOn shadowJar
def sourceJarPath = "build/libs/DistantHorizons-api-${rootProject.versionStr}-sources.jar" // the compiled "-all" jar is used since it's available at the time this task is run
def secondJarFile = file(sourceJarPath) def compiledJarPath = "build/libs/DistantHorizonsApi-${rootProject.api_version}-all.jar"
def compiledJarFile = file(compiledJarPath)
// doFirst is so these only run when the task is actually executed // doFirst is so these only run when the task is actually executed
doFirst { doFirst {
System.out.println("Adding source files from: \n" + System.out.println("Adding class files from: \n" +
"[" + sourceJarPath + "] to compiled API jar: \n" + "[" + compiledJarPath + "] to source API jar: \n" +
"[" + shadowJar.archiveFile.get().asFile + "]") "[" + shadowJar.archiveFile.get().asFile + "]")
// Validate the input JAR file // Validate the input JAR file
if (!secondJarFile.exists()) { if (!compiledJarFile.exists()) {
throw new GradleException("Second JAR file not found: [${secondJarFile}]") throw new GradleException("Compiled JAR file not found: [${compiledJarFile}]")
} }
} }
// Set the name of the combined JAR file archiveFileName.set("DistantHorizonsApi-${rootProject.api_version}-combined.jar") // jar name
archiveFileName.set("DistantHorizonsApi-${rootProject.api_version}.jar") destinationDirectory = file('build/libs/') // jar location
// Set the destination directory for the combined JAR file
destinationDirectory = file('build/libs/merged/')
// Set the input JAR files to be combined // Set the input JAR files to be combined
from sourceSets.main.allJava from sourceSets.main.allJava
from { from {
configurations.shadow.collect { it.isDirectory() ? it : zipTree(it) } project.configurations.shadow.collect { it.isDirectory() ? it : zipTree(it) }
} }
// set the jars to merge // add the class files
from shadowJar.archiveFile.get().asFile from zipTree(compiledJarFile)
from secondJarFile
// alternative method to Include the source files in the combined JAR // alternative method to Include the source files in the combined JAR
// and/or see which files are being included // and/or see which files are being included
@@ -93,6 +92,13 @@ task addSourcesToCompiledJar(type: ShadowJar) {
} }
} }
// always create a combined jar for easy deployment
assemble.dependsOn(createReleaseApiJar)
shadowJar {
// required for basic shadowJar setup
configurations = [project.configurations.shadow]
}
javadoc { javadoc {
options { options {
@@ -103,3 +109,17 @@ javadoc {
addStringOption('Xdoclint:all,-missing', '-quiet') addStringOption('Xdoclint:all,-missing', '-quiet')
} }
} }
// set the jar name
def configureJar = { task ->
// outputs in the format:
// "DistantHorizonsApi-6.0.0.jar"
// "DistantHorizonsApi-6.0.0-sources.jar"
task.archiveBaseName = rootProject.api_name
task.archiveVersion = rootProject.api_version
}
configureJar(tasks.named("jar").get())
configureJar(tasks.named("sourcesJar").get())
configureJar(tasks.named("shadowJar").get())
@@ -42,6 +42,12 @@ public interface IDhApiBlockStateWrapper extends IDhApiUnsafeWrapper
/** @since API 1.0.0 */ /** @since API 1.0.0 */
boolean isLiquid(); boolean isLiquid();
/**
* Returns a value between 0 (fully transparent) and 16 (fully opaque).
* @since 6.1.0
*/
int getOpacity();
/** /**
* Returns the full serialized form of the given block * Returns the full serialized form of the given block
* as defined by DH's serialization methods. * as defined by DH's serialization methods.
@@ -107,16 +107,24 @@ public abstract class DhApiBlockColorOverrideEvent implements IDhApiEvent<DhApiB
public IDhApiLevelWrapper getLevelWrapper() { return levelWrapper; } public IDhApiLevelWrapper getLevelWrapper() { return levelWrapper; }
public int getColorAsInt() { return this.colorAsInt; } public int getColorAsInt() { return this.colorAsInt; }
public int getAlpha() { return ColorUtil.getAlpha(this.colorAsInt); }
public int getRed() { return ColorUtil.getRed(this.colorAsInt); } public int getRed() { return ColorUtil.getRed(this.colorAsInt); }
public int getGreen() { return ColorUtil.getGreen(this.colorAsInt); } public int getGreen() { return ColorUtil.getGreen(this.colorAsInt); }
public int getBlue() { return ColorUtil.getBlue(this.colorAsInt); } public int getBlue() { return ColorUtil.getBlue(this.colorAsInt); }
public void setColor(int red, int green, int blue) throws IllegalArgumentException public void setColor(int red, int green, int blue) throws IllegalArgumentException { setColor(255, red, green, blue); }
/**
* Note: when if you set a partially transparent alpha channel the underlying {@link IDhApiBlockStateWrapper#getOpacity()}
* method should also return a non-opaque value.
* Otherwise LODs may behave incorrectly.
*/
public void setColor(int alpha, int red, int green, int blue) throws IllegalArgumentException
{ {
ColorUtil.throwIfColorValueOutOfIntRange("alpha", alpha);
ColorUtil.throwIfColorValueOutOfIntRange("red", red); ColorUtil.throwIfColorValueOutOfIntRange("red", red);
ColorUtil.throwIfColorValueOutOfIntRange("green", green); ColorUtil.throwIfColorValueOutOfIntRange("green", green);
ColorUtil.throwIfColorValueOutOfIntRange("blue", blue); ColorUtil.throwIfColorValueOutOfIntRange("blue", blue);
this.colorAsInt = ColorUtil.rgbToInt(red, green, blue); this.colorAsInt = ColorUtil.argbToInt(alpha, red, green, blue);
} }
/** @return the block's X value in the world */ /** @return the block's X value in the world */
@@ -33,21 +33,24 @@ public final class ModInfo
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */ /** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 13; public static final int PROTOCOL_VERSION = 13;
/** The full plugin channel name (RESOURCE_NAMESPACE:WRAPPER_PACKET_PATH) must be 20 characters or fewer for compatibility with <1.13. */ /**
* The full plugin channel name (RESOURCE_NAMESPACE:WRAPPER_PACKET_PATH)
* must be 20 characters or fewer for compatibility with MC 1.13 and older.
*/
public static final String WRAPPER_PACKET_PATH = "msg"; public static final String WRAPPER_PACKET_PATH = "msg";
/** The internal mod name */ /** The internal mod name */
public static final String NAME = "DistantHorizons"; public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */ /** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons"; public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "3.0.0-b"; public static final String VERSION = "3.0.2-b";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */ /** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 6; public static final int API_MAJOR_VERSION = 6;
/** This version should be updated whenever new methods are added to the DH API */ /** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0; public static final int API_MINOR_VERSION = 1;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0; public static final int API_PATCH_VERSION = 0;
-1
View File
@@ -50,7 +50,6 @@ dependencies {
compileOnly("io.netty:netty-buffer:${rootProject.netty_version}") compileOnly("io.netty:netty-buffer:${rootProject.netty_version}")
compileOnly("org.jetbrains:annotations:16.0.2") compileOnly("org.jetbrains:annotations:16.0.2")
compileOnly("com.google.code.findbugs:jsr305:3.0.2") compileOnly("com.google.code.findbugs:jsr305:3.0.2")
compileOnly("com.google.common:google-collect:0.5")
compileOnly("com.google.guava:guava:31.1-jre") compileOnly("com.google.guava:guava:31.1-jre")
// DH's bundled libraries (shadowed + relocated in loader jars) // DH's bundled libraries (shadowed + relocated in loader jars)
@@ -54,7 +54,7 @@ public class Initializer
public static void init() public static void preConfigInit()
{ {
//============================// //============================//
// check referenced libraries // // check referenced libraries //
@@ -177,6 +177,11 @@ public class Initializer
//endregion //endregion
}
/** fired after DH's config has been set up */
public static void postConfigInit()
{
//==============================// //==============================//
@@ -238,8 +243,7 @@ public class Initializer
//endregion //endregion
} }
} }
@@ -122,7 +122,7 @@ public class ServerApi
public void serverPlayerJoinEvent(IServerPlayerWrapper player) public void serverPlayerJoinEvent(IServerPlayerWrapper player)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
return; return;
} }
@@ -136,7 +136,7 @@ public class ServerApi
} }
public void serverPlayerDisconnectEvent(IServerPlayerWrapper player) public void serverPlayerDisconnectEvent(IServerPlayerWrapper player)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
return; return;
} }
@@ -150,7 +150,7 @@ public class ServerApi
} }
public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel) public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
return; return;
} }
@@ -170,7 +170,7 @@ public class ServerApi
*/ */
public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message) public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
return; return;
} }
@@ -74,7 +74,6 @@ public class SharedApi
//region //region
private SharedApi() { } private SharedApi() { }
public static void init() { Initializer.init(); }
//endregion //endregion
@@ -212,7 +211,7 @@ public class SharedApi
} }
// ignore updates if the world is read-only // ignore updates if the world is read-only
if (DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
return; return;
} }
@@ -0,0 +1,84 @@
package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.util.TimerUtil;
import java.util.Timer;
import java.util.TimerTask;
public abstract class AbstractDelayedConfigEventHandler implements IConfigListener
{
public static final long DEFAULT_TIMEOUT_IN_MS = 2_000L;
/** how long to wait in milliseconds before applying the config changes */
private final long timeoutInMs;
private Timer timer;
//=============//
// constructor //
//=============//
//region
public AbstractDelayedConfigEventHandler(long timeoutInMs) { this.timeoutInMs = timeoutInMs; }
//endregion
//==================//
// abstract methods //
//==================//
//region
public abstract void onConfigTimeout();
//endregion
//========//
// events //
//========//
//region
@Override
public void onConfigValueSet()
{
if (this.timeoutInMs > 0)
{
this.refreshRenderDataAfterTimeout();
}
else
{
this.onConfigTimeout();
}
}
/** Calling this method multiple times will reset the timer */
private synchronized void refreshRenderDataAfterTimeout() // synchronized to prevent potential threading issues when adding/removing the timer
{
// stop the previous timer if one exists
if (this.timer != null)
{
this.timer.cancel();
}
// create a new timer task
TimerTask timerTask = new TimerTask()
{
public void run()
{
AbstractDelayedConfigEventHandler.this.onConfigTimeout();
}
};
this.timer = TimerUtil.CreateTimer("AbstractDelayedConfigTimer");
this.timer.schedule(timerTask, this.timeoutInMs);
}
//endregion
}
@@ -27,72 +27,36 @@ import com.seibel.distanthorizons.core.util.TimerUtil;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
public class ReloadLodsConfigEventHandler implements IConfigListener public class ReloadLodsConfigEventHandler extends AbstractDelayedConfigEventHandler
{ {
/** /**
* should be used for user facing UI options * should be used for user facing UI options
* this allows the user a second to click through options before they're applied * this allows the user a second to click through options before they're applied
*/ */
public static ReloadLodsConfigEventHandler DELAYED_INSTANCE = new ReloadLodsConfigEventHandler(2_000L); public static ReloadLodsConfigEventHandler DELAYED_INSTANCE = new ReloadLodsConfigEventHandler(AbstractDelayedConfigEventHandler.DEFAULT_TIMEOUT_IN_MS);
/** should be used for debug options so their change can be seen instantly */ /** should be used for debug options so their change can be seen instantly */
public static ReloadLodsConfigEventHandler INSTANT_INSTANCE = new ReloadLodsConfigEventHandler(0); public static ReloadLodsConfigEventHandler INSTANT_INSTANCE = new ReloadLodsConfigEventHandler(0);
/** how long to wait in milliseconds before applying the config changes */
private final long timeoutInMs;
private Timer cacheClearingTimer;
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
public ReloadLodsConfigEventHandler(long timeoutInMs) public ReloadLodsConfigEventHandler(long timeoutInMs) { super(timeoutInMs); }
{
this.timeoutInMs = timeoutInMs; //endregion
}
//========// //========//
// events // // events //
//========// //========//
//region
@Override @Override
public void onConfigValueSet() public void onConfigTimeout()
{
if (this.timeoutInMs > 0)
{
this.refreshRenderDataAfterTimeout();
}
else
{
clearRenderDataCache();
}
}
/** Calling this method multiple times will reset the timer */
private synchronized void refreshRenderDataAfterTimeout() // synchronized to prevent potential threading issues when adding/removing the timer
{
// stop the previous timer if one exists
if (this.cacheClearingTimer != null)
{
this.cacheClearingTimer.cancel();
}
// create a new timer task
TimerTask timerTask = new TimerTask()
{
public void run()
{
clearRenderDataCache();
}
};
this.cacheClearingTimer = TimerUtil.CreateTimer("RenderCacheClearConfigTimer");
this.cacheClearingTimer.schedule(timerTask, this.timeoutInMs);
}
private static void clearRenderDataCache()
{ {
IDhApiRenderProxy renderProxy = DhApi.Delayed.renderProxy; IDhApiRenderProxy renderProxy = DhApi.Delayed.renderProxy;
if (renderProxy != null) if (renderProxy != null)
@@ -101,5 +65,8 @@ public class ReloadLodsConfigEventHandler implements IConfigListener
} }
} }
//endregion
} }
@@ -32,7 +32,9 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
public class RenderBlockCacheCsvHandler implements IConfigListener import java.util.Timer;
public class RenderBlockCacheCsvHandler extends AbstractDelayedConfigEventHandler
{ {
private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@@ -43,18 +45,22 @@ public class RenderBlockCacheCsvHandler implements IConfigListener
//=============// //=============//
// constructor // // constructor //
//=============// //=============//
//region
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
private RenderBlockCacheCsvHandler() { } private RenderBlockCacheCsvHandler() { super(AbstractDelayedConfigEventHandler.DEFAULT_TIMEOUT_IN_MS); }
//endregion
//=================// //=================//
// config handling // // config handling //
//=================// //=================//
//region
@Override @Override
public void onConfigValueSet() public void onConfigTimeout()
{ {
IWrapperFactory wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class); IWrapperFactory wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
if (wrapperFactory != null) if (wrapperFactory != null)
@@ -64,6 +70,8 @@ public class RenderBlockCacheCsvHandler implements IConfigListener
} }
} }
//endregion
} }
@@ -91,10 +91,27 @@ public class LodRequestModule implements Closeable
{ {
try try
{ {
// Initial wait is to prevent an issue
// where this starts before the child object's constructor finishes,
// causing null pointers on final non-null references.
// The try-catch in the while loop should also handle this
// but this way we shouldn't have error logs.
Thread.sleep(500);
// run until the threadpool is shut down
while (!Thread.interrupted()) while (!Thread.interrupted())
{ {
Thread.sleep(20); try
this.tick(); {
Thread.sleep(20);
this.tick();
}
catch (InterruptedException e) { throw e; }
catch (Exception e)
{
LOGGER.error("Unexpected error in [" + LodRequestModule.class.getSimpleName() + "] tick loop, error: [" + e.getMessage() + "].", e);
}
} }
} }
catch (InterruptedException ignore) { } catch (InterruptedException ignore) { }
@@ -104,7 +121,7 @@ public class LodRequestModule implements Closeable
boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen(); boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen();
// if the world is read only don't generate anything // if the world is read only don't generate anything
shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.getReadOnly(); shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.tryGetReadOnly();
// don't generate chunks for client levels that aren't being rendered // don't generate chunks for client levels that aren't being rendered
// (this can happen when moving between dimensions) // (this can happen when moving between dimensions)
@@ -202,7 +202,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private synchronized void tryQueueNewWorldGenRequestsAsync() private synchronized void tryQueueNewWorldGenRequestsAsync()
{ {
if (!DhApiWorldProxy.INSTANCE.worldLoaded() if (!DhApiWorldProxy.INSTANCE.worldLoaded()
|| DhApiWorldProxy.INSTANCE.getReadOnly()) || DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
return; return;
} }
@@ -151,8 +151,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
public synchronized boolean tick(DhBlockPos2D targetPos) public synchronized boolean tick(DhBlockPos2D targetPos)
{ {
if (DhApiWorldProxy.INSTANCE.worldLoaded() if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
&& DhApiWorldProxy.INSTANCE.getReadOnly())
{ {
return false; return false;
} }
@@ -7,10 +7,10 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil;
import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -21,7 +21,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
.build(); .build();
private final ConcurrentMap<Integer, CompositeByteBuf> buffersById = CacheBuilder.newBuilder() private final ConcurrentMap<Integer, CompositeByteBuf> buffersById = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS) .expireAfterAccess(30, TimeUnit.SECONDS)
.<Integer, CompositeByteBuf>build().asMap(); .<Integer, CompositeByteBuf>build().asMap();
@Override @Override
@@ -56,7 +56,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload) public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload)
{ {
CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId); CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId);
LodUtil.assertTrue(compositeByteBuffer != null, "decoded data source missing byte buffer"); Objects.requireNonNull(compositeByteBuffer, "Unable to get a complete buffer for a received payload. Ignore this if it doesn't spam similar errors");
try try
{ {
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.render.QuadTree;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener; import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
@@ -148,7 +149,8 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
int initialPlayerBlockX, int initialPlayerBlockZ, int initialPlayerBlockX, int initialPlayerBlockZ,
FullDataSourceProviderV2 fullDataSourceProvider) FullDataSourceProviderV2 fullDataSourceProvider)
{ {
super(viewDiameterInBlocks, new DhBlockPos2D(initialPlayerBlockX, initialPlayerBlockZ), DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); super(viewDiameterInBlocks, FullDataSourceV2.WIDTH,
new DhBlockPos2D(initialPlayerBlockX, initialPlayerBlockZ), DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
DEBUG_RENDERER.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus); DEBUG_RENDERER.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus);
@@ -245,17 +247,38 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//===================// //===================//
//region //region
this.setCenterBlockPos(playerPos, (renderSection) -> // remove out of bound sections
{ this.setCenterBlockPos(playerPos,
// removing out of bounds sections // remove completely out of bound nodes
if (renderSection != null) // (the root node is no longer in bounds)
(renderSection) ->
{ {
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos)); if (renderSection != null)
this.missingGenerationPosSet.remove(renderSection.pos); {
this.queuedGenerationPosSet.remove(renderSection.pos); this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
renderSection.close();
// unfortunately we have to fully go through each set
// since a removed position may be larger than the multiple generated positions
// it contains
this.missingGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.queuedGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
renderSection.close();
}
},
// mutate partially out of bound nodes
// (the root node is still in bounds, but this individual child node isn't)
(renderSection) ->
{
if (renderSection != null)
{
// when this node comes back into render distance
// we'll need to re-load it since the full data
// may have been modified while it was out of bounds
renderSection.renderDataDirty = true;
}
} }
}); );
//endregion //endregion
@@ -304,7 +327,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
continue; continue;
} }
node.value.retreivedMissingSectionsForRetreival = false; node.value.queuedMissingSectionsForRetrieval = false;
} }
} }
@@ -372,8 +395,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=========================// //=========================//
//region //region
// also handles disabling beacons
for (QuadNode<LodRenderSection> node : this.tickNodeHolder.getDisableNodes()) for (QuadNode<LodRenderSection> node : this.tickNodeHolder.getDisableNodes())
{ {
if (node == null || node.value == null) { continue; } if (node == null || node.value == null) { continue; }
@@ -449,9 +470,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// since this section wants to render // since this section wants to render
// check if it needs any generation to do so // check if it needs any generation to do so
if (!node.value.retreivedMissingSectionsForRetreival) if (!node.value.queuedMissingSectionsForRetrieval)
{ {
node.value.retreivedMissingSectionsForRetreival = true; node.value.queuedMissingSectionsForRetrieval = true;
this.tryQueuePosForRetrieval(node.value.pos); // can be quite slow this.tryQueuePosForRetrieval(node.value.pos); // can be quite slow
} }
} }
@@ -476,9 +497,10 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=========================// //=========================//
// tick - recursive update // // tick - recursive update //
//=========================// //=========================//
///region //region
private void recursivelyUpdateRenderSectionNode( /** @return true if the node at this position has uploaded its render data */
private boolean recursivelyUpdateRenderSectionNode(
@NotNull DhBlockPos2D playerPos, @NotNull DhBlockPos2D playerPos,
@NotNull QuadNode<LodRenderSection> rootNode, @NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> parentNode, @Nullable QuadNode<LodRenderSection> parentNode,
@@ -490,19 +512,22 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// get/create the node // // get/create the node //
// and render section // // and render section //
//=====================// //=====================//
///region //region
// create the node quadNode = this.tryAddNodeToTree(rootNode, quadNode, sectionPos);
if (quadNode == null)
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider)); // Skip sections that are out-of-bounds.
quadNode = rootNode.getNode(sectionPos); // If not done some sections will appear and/or generate
} // outside the desired render distance
if (quadNode == null) if (!this.isSectionPosInBounds(quadNode.sectionPos))
{ {
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"]."); this.tickNodeHolder.addDisableNode(quadNode);
this.recursivelyDisableChildNodes(quadNode);
return true;
} }
// make sure the render section is created (shouldn't be necessary, but just in case) // make sure the render section is created (shouldn't be necessary, but just in case)
LodRenderSection renderSection = quadNode.value; LodRenderSection renderSection = quadNode.value;
if (renderSection == null) if (renderSection == null)
@@ -511,7 +536,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
quadNode.setValue(sectionPos, renderSection); quadNode.setValue(sectionPos, renderSection);
} }
///endregion //endregion
@@ -519,7 +544,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// handle enabling, loading, // // handle enabling, loading, //
// and disabling render sections // // and disabling render sections //
//===============================// //===============================//
///region //region
// load every node for rendering // load every node for rendering
if (!renderSection.gpuUploadInProgress() if (!renderSection.gpuUploadInProgress()
@@ -535,94 +560,153 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
if (DhSectionPos.getDetailLevel(quadNode.sectionPos) > expectedDetailLevel) if (DhSectionPos.getDetailLevel(quadNode.sectionPos) > expectedDetailLevel)
{ {
this.onDetailLevelTooHigh(playerPos, rootNode, quadNode); return this.onDetailLevelTooLow(playerPos, rootNode, quadNode);
} }
// the (expectedDetailLevel-1) fixes corners being cut out due to distance calculations using the LOD center // the (expectedDetailLevel-1) fixes corners being cut out due to distance calculations using the LOD center
else if (DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel else if (DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel
|| DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel - 1) || DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel - 1)
{ {
this.onDesiredDetailLevel(quadNode, parentNode); return this.onDesiredDetailLevel(quadNode, parentNode);
} }
else else
{ {
throw new IllegalStateException("LodQuadTree shouldn't be updating renderSections below the expected detail level: [" + expectedDetailLevel + "]."); throw new IllegalStateException("LodQuadTree shouldn't be updating renderSections below the expected detail level: [" + expectedDetailLevel + "].");
} }
///endregion //endregion
} }
private void onDetailLevelTooHigh( /** @return true if the node at this position has uploaded its render data */
private boolean onDetailLevelTooLow(
@NotNull DhBlockPos2D playerPos, @NotNull DhBlockPos2D playerPos,
@NotNull QuadNode<LodRenderSection> rootNode, @NotNull QuadNode<LodRenderSection> quadNode) @NotNull QuadNode<LodRenderSection> rootNode,
@NotNull QuadNode<LodRenderSection> quadNode)
{ {
// recursively update each child node // recursively update each child node
boolean allChildNodesCanRender = true; int childNodeRenderCount = 0;
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
long childPos = DhSectionPos.getChildByIndex(quadNode.sectionPos, i); long childPos = DhSectionPos.getChildByIndex(quadNode.sectionPos, i);
this.recursivelyUpdateRenderSectionNode( QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
playerPos,
rootNode, quadNode, childNode, childPos);
childNode = quadNode.getChildByIndex(i); // needs to be gotten again in case a new node was added to the tree (this will often happen when moving into new areas where the children were deleted)
// nodes shouldn't be null, but just in case boolean childCanRender = this.recursivelyUpdateRenderSectionNode(
if (childNode != null playerPos,
&& childNode.value != null rootNode, quadNode, childNode, childPos);
&& !childNode.value.gpuUploadComplete()) if (childCanRender)
{ {
// the node is present but not uploaded yet // node can be rendered
allChildNodesCanRender = false; childNodeRenderCount++;
} }
} }
boolean isRootNode = (quadNode == rootNode);
if (allChildNodesCanRender) if (isRootNode)
{ {
// all child nodes can render, this node isn't needed // Never render the root node.
// This is done to prevent flashing when moving across root node
// boundaries.
// Otherwise, when moving, new empty nodes will be added at the edge of the tree
// which will require the root node to render to cover the "empty" area.
this.tickNodeHolder.addDisableNode(quadNode); this.tickNodeHolder.addDisableNode(quadNode);
return false;
}
else if (childNodeRenderCount >= 4)
{
this.tickNodeHolder.addDisableNode(quadNode);
// all children can render,
// the area will be filled when rendering
return true;
} }
else else
{ {
// not all child positions are loaded yet, this one should be rendered instead boolean nodeCanRender = quadNode.value != null
this.tickNodeHolder.addEnableNode(quadNode); && quadNode.value.canRender();
if (nodeCanRender)
{
// not all child positions are loaded yet, this one should be rendered instead
this.tickNodeHolder.addEnableNode(quadNode);
this.recursivelyDisableChildNodes(quadNode);
}
else
{
this.tickNodeHolder.addDisableNode(quadNode);
}
return nodeCanRender;
} }
} }
private void onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode, @Nullable QuadNode<LodRenderSection> parentNode) /** @return true if the node at this position has uploaded its render data */
private boolean onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode,
@Nullable QuadNode<LodRenderSection> parentNode)
{ {
boolean allAdjNodesCanRender = true; // Skip sections that are out-of-bounds.
// If not done some sections will appear and/or generate
// if the parent node is null, that means we're at the root node // outside the desired render distance
// and we should always render if (!this.isSectionPosInBounds(quadNode.sectionPos))
if (parentNode != null)
{ {
// check if all adjacent nodes are ready to render return true;
// this check is done to prevent some overlapping due to the parent node }
// still being active
for (int i = 0; i < 4; i++) if (quadNode.value != null
&& quadNode.value.canRender())
{
if (!this.tickNodeHolder.getEnabledNodes().contains(parentNode))
{ {
QuadNode<LodRenderSection> adjNode = parentNode.getChildByIndex(i); this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
// nodes shouldn't be null, but just in case there's an issue return true;
if (adjNode != null }
&& adjNode.value != null else
&& !adjNode.value.gpuUploadComplete()) {
{ this.tickNodeHolder.addDisableNode(quadNode);
// the node is present but not uploaded yet return false;
allAdjNodesCanRender = false;
}
} }
} }
else
if (allAdjNodesCanRender
&& quadNode.value != null
&& quadNode.value.gpuUploadComplete())
{ {
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode); this.tickNodeHolder.addDisableNode(quadNode);
return false;
} }
} }
///endregion @NotNull
private QuadNode<LodRenderSection> tryAddNodeToTree(
@NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> quadNode,
long sectionPos // section pos is needed here since the quad node may be null
)
{
// create the node
if (quadNode == null)
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
quadNode = rootNode.getNode(sectionPos);
}
if (quadNode == null)
{
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
}
return quadNode;
}
private void recursivelyDisableChildNodes(@NotNull QuadNode<LodRenderSection> quadNode)
{
for (int i = 0; i < 4; i++)
{
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
this.tickNodeHolder.removeEnableAndDisableNode(childNode);
if (childNode != null)
{
this.recursivelyDisableChildNodes(childNode);
}
}
}
//endregion
//=====================// //=====================//
// tick - work queuing // // tick - work queuing //
@@ -1118,6 +1202,17 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
{ {
this.populateListWithEnabledRenderSections(this.debugNodeList); this.populateListWithEnabledRenderSections(this.debugNodeList);
//// can be uncommented for debugging/finding a specific position
//debugRenderer.makeParticle(
// new AbstractDebugWireframeRenderer.BoxParticle(
// new AbstractDebugWireframeRenderer.Box(
// DhSectionPos.encode((byte)7, 3,-1)
// , -64, 400,
// 0.1f,
// Color.YELLOW),
// 0.5, 0f
// ));
for (int i = 0; i < this.debugNodeList.size(); i++) for (int i = 0; i < this.debugNodeList.size(); i++)
{ {
LodRenderSection renderSection = this.debugNodeList.get(i); LodRenderSection renderSection = this.debugNodeList.get(i);
@@ -1127,7 +1222,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
{ {
color = Color.ORANGE; color = Color.ORANGE;
} }
else if (!renderSection.gpuUploadComplete()) else if (!renderSection.canRender())
{ {
// uploaded but the buffer is missing // uploaded but the buffer is missing
color = Color.PINK; color = Color.PINK;
@@ -34,11 +34,8 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer; import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.render.renderer.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
@@ -48,10 +45,8 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose; import javax.annotation.WillNotClose;
import java.awt.*; import java.awt.*;
import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* A render section represents an area that could be rendered. * A render section represents an area that could be rendered.
@@ -75,8 +70,16 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private boolean renderingEnabled = false; private boolean renderingEnabled = false;
private boolean beaconsRendering = false; /**
public boolean retreivedMissingSectionsForRetreival = false; * Used when a node goes out of render distance
* but isn't removed from the underlying quad tree structure. <br><br>
*
* In those cases we should act as if the node was removed
* for cached render data caching purposes, but not
* for re-creating missing nodes.
*/
public boolean renderDataDirty = false;
public boolean queuedMissingSectionsForRetrieval = false;
/** this reference is necessary so we can determine what VBO to render */ /** this reference is necessary so we can determine what VBO to render */
public LodBufferContainer renderBufferContainer; public LodBufferContainer renderBufferContainer;
@@ -320,6 +323,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// upload complete // upload complete
this.renderBufferContainer = buffer.buffersUploaded ? buffer : null; this.renderBufferContainer = buffer.buffersUploaded ? buffer : null;
this.renderDataDirty = false;
if (previousContainer != null) if (previousContainer != null)
{ {
@@ -345,7 +349,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=================// //=================//
//region //region
public boolean gpuUploadComplete() { return this.renderBufferContainer != null; } public boolean canRender() { return this.renderBufferContainer != null; }
public boolean gpuUploadComplete()
{
return this.renderBufferContainer != null
// render dirty is here so we can trigger new GPU uploads
// even if the render data is present
&& !this.renderDataDirty;
}
public boolean getRenderingEnabled() { return this.renderingEnabled; } public boolean getRenderingEnabled() { return this.renderingEnabled; }
public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;} public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;}
@@ -374,7 +385,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{ {
color = Color.yellow; color = Color.yellow;
} }
else if (this.gpuUploadComplete()) else if (this.canRender())
{ {
//color = Color.cyan; //color = Color.cyan;
return; return;
@@ -418,6 +429,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
} }
this.setRenderingEnabled(false);
if (this.renderBufferContainer != null) if (this.renderBufferContainer != null)
{ {
this.renderBufferContainer.close(); this.renderBufferContainer.close();
@@ -63,21 +63,39 @@ public class QuadTreeTickNodeHolder
{ {
if(this.presentNodes.add(node)) if(this.presentNodes.add(node))
{ {
// not a big fan of having to check every node to prevent overlaps, but it does work // in James testing as of 4-21-2026
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) -> // this should no longer be needed to prevent overlaps,
// however I'm keeping it here as a quick fix solution if
// the problem comes up again
if (false)
{ {
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos); // not a big fan of having to check every node to prevent overlaps, but it does work
if (contained) this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
{ {
this.nodesToDisable.add(checkNode); boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
} if (contained)
{
return contained; this.nodesToDisable.add(checkNode);
}); }
return contained;
});
}
this.nodesToEnable.add(node); this.nodesToEnable.add(node);
} }
} }
/** */
public void removeEnableAndDisableNode(QuadNode<LodRenderSection> node)
{
this.nodesToEnable.remove(node);
this.nodesToEnableDeleteChildrenList.remove(node);
this.presentNodes.add(node); // should already be present, but re-added just in case
this.nodesToDisable.add(node); // node shouldn't be rendered since it's being disabled by a parent
}
public HashSet<QuadNode<LodRenderSection>> getEnabledNodes() { return this.nodesToEnable; } public HashSet<QuadNode<LodRenderSection>> getEnabledNodes() { return this.nodesToEnable; }
@@ -180,6 +180,24 @@ public class LodRenderer
renderBufferHandler.buildRenderList(renderParams); renderBufferHandler.buildRenderList(renderParams);
} }
boolean renderFog;
Boolean apiFogOverride = Config.Client.Advanced.Graphics.Fog.enableDhFog.getApiValue();
if (apiFogOverride != null)
{
// use whatever the API dictates if set
// (this could cause issues when underwater if a shader or something
// doesn't add their own, but that's relatively unlikely)
renderFog = apiFogOverride;
}
else
{
renderFog = Config.Client.Advanced.Graphics.Fog.enableDhFog.get();
// allow enabling fog when: underwater fog, blind, etc.
// otherwise LODs won't appear correctly
renderFog |= renderParams.vanillaFogEnabled;
}
//endregion //endregion
@@ -207,8 +225,6 @@ public class LodRenderer
// opaque LODs // opaque LODs
profiler.popPush("LOD Opaque"); profiler.popPush("LOD Opaque");
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderPassEvent.class, renderParams);
this.renderTerrain(this.terrainRenderer, renderBufferHandler, renderParams, /*opaquePass*/ true, profiler); this.renderTerrain(this.terrainRenderer, renderBufferHandler, renderParams, /*opaquePass*/ true, profiler);
// custom objects with SSAO // custom objects with SSAO
@@ -249,9 +265,8 @@ public class LodRenderer
} }
// fog // fog
if (Config.Client.Advanced.Graphics.Fog.enableDhFog.get()
// this is done to fix issues with: underwater fog, blindness effect, etc. if (renderFog)
|| renderParams.vanillaFogEnabled)
{ {
profiler.popPush("LOD Fog"); profiler.popPush("LOD Fog");
@@ -294,15 +309,11 @@ public class LodRenderer
if (Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled) if (Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled)
{ {
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderPassEvent.class, renderParams);
profiler.popPush("LOD Transparent"); profiler.popPush("LOD Transparent");
this.renderTerrain(this.terrainRenderer, renderBufferHandler, renderParams, /*opaquePass*/ false, profiler); this.renderTerrain(this.terrainRenderer, renderBufferHandler, renderParams, /*opaquePass*/ false, profiler);
if (Config.Client.Advanced.Graphics.Fog.enableDhFog.get() if (renderFog)
// this is done to fix issues with: underwater fog, blindness effect, etc.
|| renderParams.vanillaFogEnabled)
{ {
profiler.popPush("LOD Fog"); profiler.popPush("LOD Fog");
@@ -247,10 +247,7 @@ public class RenderDataPointUtil
{ {
return "Y+:" + getYMax(dataPoint) + return "Y+:" + getYMax(dataPoint) +
" Y-:" + getYMin(dataPoint) + " Y-:" + getYMin(dataPoint) +
" argb:" + getAlpha(dataPoint) + " " + " argb:" + getAlpha(dataPoint) + "," + getRed(dataPoint) + "," + getGreen(dataPoint) + "," + getBlue(dataPoint) +
getRed(dataPoint) + " " +
getGreen(dataPoint) + " " +
getBlue(dataPoint) +
" BL:" + getLightBlock(dataPoint) + " BL:" + getLightBlock(dataPoint) +
" SL:" + getLightSky(dataPoint) + " SL:" + getLightSky(dataPoint) +
" MAT:" + getBlockMaterialId(dataPoint) + "["+ EDhApiBlockMaterial.getFromIndex(getBlockMaterialId(dataPoint))+"]"; " MAT:" + getBlockMaterialId(dataPoint) + "["+ EDhApiBlockMaterial.getFromIndex(getBlockMaterialId(dataPoint))+"]";
@@ -123,7 +123,16 @@ public class RenderUtil
// At low render distances this hides the vanilla RD border // At low render distances this hides the vanilla RD border
int chunkRenderDistance = MC_RENDER.getRenderDistance(); int chunkRenderDistance = MC_RENDER.getRenderDistance();
if (chunkRenderDistance <= 2)
if (IRIS_ACCESSOR != null
&& IRIS_ACCESSOR.isShaderPackInUse())
{
// shaders handle the near clip plane/overdraw differently, best to play it
// safe and have the plane really close otherwise
// there might be cutouts on the screen edges
overdraw = 0.2f;
}
else if (chunkRenderDistance <= 2)
{ {
overdraw = 0.2f; overdraw = 0.2f;
} }
@@ -325,7 +325,7 @@ public class QuadNode<T>
public void deleteAllChildren() { this.deleteAllChildren(null); } public void deleteAllChildren() { this.deleteAllChildren(null); }
/** @param removedItemConsumer is only fired for non-null nodes, however the value passed in may be null */ /** @param removedItemConsumer is only fired for non-null nodes, however the value passed in may be null */
public void deleteAllChildren(Consumer<? super T> removedItemConsumer) public void deleteAllChildren(@Nullable Consumer<? super T> removedItemConsumer)
{ {
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
@@ -19,10 +19,12 @@
package com.seibel.distanthorizons.core.util.objects.quadTree; package com.seibel.distanthorizons.core.util.objects.quadTree;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.QuadTree.LodQuadTree;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
@@ -61,6 +63,11 @@ public class QuadTree<T>
private final MovableGridRingList<QuadNode<T>> topRingList; private final MovableGridRingList<QuadNode<T>> topRingList;
private DhBlockPos2D centerBlockPos; private DhBlockPos2D centerBlockPos;
/**
* defines how many blocks the center needs to move in blocks
* before we check for out-of-bound nodes.
*/
private int blockDistanceForNodeClearing;
@@ -74,10 +81,13 @@ public class QuadTree<T>
* *
* @param diameterInBlocks equivalent to the distance between the two opposing sides * @param diameterInBlocks equivalent to the distance between the two opposing sides
*/ */
public QuadTree(int diameterInBlocks, DhBlockPos2D centerBlockPos, byte treeLeafDetailLevel) public QuadTree(
int diameterInBlocks, int blockDistanceForNodeClearing,
DhBlockPos2D centerBlockPos, byte treeLeafDetailLevel)
{ {
this.centerBlockPos = centerBlockPos; this.centerBlockPos = centerBlockPos;
this.diameterInBlocks = diameterInBlocks; this.diameterInBlocks = diameterInBlocks;
this.blockDistanceForNodeClearing = blockDistanceForNodeClearing;
this.treeLeafDetailLevel = treeLeafDetailLevel; this.treeLeafDetailLevel = treeLeafDetailLevel;
// the min detail level must be greater than 0 (to prevent divide by 0 errors) and greater than the maximum detail level // the min detail level must be greater than 0 (to prevent divide by 0 errors) and greater than the maximum detail level
@@ -130,18 +140,22 @@ public class QuadTree<T>
public int leafNodeCount() public int leafNodeCount()
{ {
int count = 0; int count = 0;
for (QuadNode<T> node : this.topRingList) for (QuadNode<T> rootNode : this.topRingList)
{ {
if (node == null) if (rootNode == null)
{ {
continue; continue;
} }
Iterator<QuadNode<T>> leafNodeIterator = node.getLeafNodeIterator(); Iterator<QuadNode<T>> leafNodeIterator = rootNode.getLeafNodeIterator();
while (leafNodeIterator.hasNext()) while (leafNodeIterator.hasNext())
{ {
leafNodeIterator.next(); QuadNode<T> node = leafNodeIterator.next();
count++; if (node != null
&& this.isSectionPosInBounds(node.sectionPos))
{
count++;
}
} }
} }
@@ -236,32 +250,32 @@ public class QuadTree<T>
int ringListPosX = DhSectionPos.getX(rootPos); int ringListPosX = DhSectionPos.getX(rootPos);
int ringListPosZ = DhSectionPos.getZ(rootPos); int ringListPosZ = DhSectionPos.getZ(rootPos);
QuadNode<T> topQuadNode = this.topRingList.get(ringListPosX, ringListPosZ); QuadNode<T> rootQuadNode = this.topRingList.get(ringListPosX, ringListPosZ);
if (topQuadNode == null) if (rootQuadNode == null)
{ {
if (!setNewValue) if (!setNewValue)
{ {
return null; return null;
} }
topQuadNode = new QuadNode<T>(rootPos, this.treeLeafDetailLevel); rootQuadNode = new QuadNode<T>(rootPos, this.treeLeafDetailLevel);
boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode); boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, rootQuadNode);
if (!successfullyAdded) if (!successfullyAdded)
{ {
LodUtil.assertNotReach("Failed to add top quadTree node at position: " + rootPos); LodUtil.assertNotReach("Failed to add root quadTree node at position: ["+DhSectionPos.toString(rootPos)+"]");
} }
} }
if (!DhSectionPos.contains(topQuadNode.sectionPos, pos)) if (!DhSectionPos.contains(rootQuadNode.sectionPos, pos))
{ {
LodUtil.assertNotReach("failed to get a root node that contains the input position: " + pos + " root node pos: " + topQuadNode.sectionPos); LodUtil.assertNotReach("failed to get a root node that contains the input position: " + pos + " root node pos: " + rootQuadNode.sectionPos);
} }
QuadNode<T> returnNode = topQuadNode.getNode(pos); QuadNode<T> returnNode = rootQuadNode.getNode(pos);
if (setNewValue) if (setNewValue)
{ {
topQuadNode.setValue(pos, newValue); rootQuadNode.setValue(pos, newValue);
} }
return returnNode; return returnNode;
} }
@@ -354,32 +368,103 @@ public class QuadTree<T>
//================// //================//
//region //region
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); } public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null, null); }
public void setCenterBlockPos(DhBlockPos2D newCenterPos, Consumer<? super T> removedItemConsumer) /**
* @param removedConsumer fired when a root node is completely removed from the underlying data structure
* @param mutateOutOfBoundConsumer fired when a child node is out of bounds, but not removed from the underlying data structure
*/
public void setCenterBlockPos(
DhBlockPos2D newCenterPos,
@Nullable Consumer<? super T> removedConsumer,
@Nullable Consumer<? super T> mutateOutOfBoundConsumer)
{ {
this.centerBlockPos = newCenterPos; // did we move significantly?
boolean ringListMoved = false;
MovableGridRingList.Pos2D expectedCenterPos = new MovableGridRingList.Pos2D( int newCenterPosX = BitShiftUtil.divideByPowerOfTwo(newCenterPos.x, this.treeRootDetailLevel);
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel), int newCenterPosZ = BitShiftUtil.divideByPowerOfTwo(newCenterPos.z, this.treeRootDetailLevel);
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel)); if (this.topRingList.getCenter().getX() != newCenterPosX
|| this.topRingList.getCenter().getY() != newCenterPosZ)
if (this.topRingList.getCenter().equals(expectedCenterPos))
{ {
// tree doesn't need to be moved ringListMoved = true;
}
// did we move a little bit?
boolean recalculateOutOfBoundNodes = false;
int centerBlockDistance = this.centerBlockPos.manhattanDist(newCenterPos);
if (centerBlockDistance >= this.blockDistanceForNodeClearing)
{
recalculateOutOfBoundNodes = true;
}
if (!ringListMoved
&& !recalculateOutOfBoundNodes)
{
// the tree didn't move enough that we need
// to re-calculate anything
return; return;
} }
// remove out of bounds root nodes
this.topRingList.moveTo(expectedCenterPos.getX(), expectedCenterPos.getY(), (quadNode) -> this.centerBlockPos = newCenterPos;
// remove out of bound root nodes
this.topRingList.moveTo(newCenterPosX, newCenterPosZ, (quadNode) ->
{ {
if (quadNode != null && removedItemConsumer != null) if (quadNode != null)
{ {
quadNode.deleteAllChildren(removedItemConsumer); quadNode.deleteAllChildren(removedConsumer);
removedItemConsumer.accept(quadNode.value); if (removedConsumer != null)
{
removedConsumer.accept(quadNode.value);
}
} }
}); });
// mutate out of bound child nodes
this.topRingList.forEach((rootNode) ->
{
this.mutateOutOfBoundChildNodes(rootNode, mutateOutOfBoundConsumer);
});
}
/**
* we don't want to actually remove nodes or node data
* since that can cause the {@link LodQuadTree} to
* flash low-detail LODs when moving into previously-loaded
* LODs, which is really disorienting.
*/
private void mutateOutOfBoundChildNodes(@Nullable QuadNode<T> quadNode, @Nullable Consumer<? super T> mutateOutOfBoundConsumer)
{
// nodes shouldn't be null, but just in case
if (quadNode == null)
{
return;
}
// go over each child node
for (int i = 0; i < 4; i++)
{
QuadNode<T> childNode = quadNode.getChildByIndex(i);
if (childNode == null
|| childNode.value == null)
{
// no need to go any deeper if this node is already empty
continue;
}
// mutate nodes from the bottom up
this.mutateOutOfBoundChildNodes(childNode, mutateOutOfBoundConsumer);
// mutate this node if out of bounds
if (!this.isSectionPosInBounds(childNode.sectionPos))
{
if (mutateOutOfBoundConsumer != null)
{
mutateOutOfBoundConsumer.accept(childNode.value);
}
}
}
} }
public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; } public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; }
@@ -484,7 +569,7 @@ public class QuadTree<T>
private class QuadTreeNodeIterator implements Iterator<QuadNode<T>> private class QuadTreeNodeIterator implements Iterator<QuadNode<T>>
{ {
private final QuadTreeRootPosIterator rootNodeIterator; private final QuadTreeRootPosIterator rootNodePosIterator;
private Iterator<QuadNode<T>> currentNodeIterator; private Iterator<QuadNode<T>> currentNodeIterator;
private QuadNode<T> lastNode = null; private QuadNode<T> lastNode = null;
@@ -497,7 +582,7 @@ public class QuadTree<T>
public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc<T> stopIteratingFunc) public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc<T> stopIteratingFunc)
{ {
this.rootNodeIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc); this.rootNodePosIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
this.onlyReturnLeaves = onlyReturnLeaves; this.onlyReturnLeaves = onlyReturnLeaves;
this.stopIteratingFunc = stopIteratingFunc; this.stopIteratingFunc = stopIteratingFunc;
@@ -508,7 +593,7 @@ public class QuadTree<T>
@Override @Override
public boolean hasNext() public boolean hasNext()
{ {
if (!this.rootNodeIterator.hasNext() if (!this.rootNodePosIterator.hasNext()
&& this.currentNodeIterator != null && this.currentNodeIterator != null
&& !this.currentNodeIterator.hasNext()) && !this.currentNodeIterator.hasNext())
{ {
@@ -544,9 +629,9 @@ public class QuadTree<T>
{ {
Iterator<QuadNode<T>> nodeIterator = null; Iterator<QuadNode<T>> nodeIterator = null;
while ((nodeIterator == null || !nodeIterator.hasNext()) while ((nodeIterator == null || !nodeIterator.hasNext())
&& this.rootNodeIterator.hasNext()) && this.rootNodePosIterator.hasNext())
{ {
long sectionPos = this.rootNodeIterator.nextLong(); long sectionPos = this.rootNodePosIterator.nextLong();
// try-get to prevent concurrency errors if the tree is being moved while we walk through it // try-get to prevent concurrency errors if the tree is being moved while we walk through it
QuadNode<T> rootNode = QuadTree.this.tryGetNode(sectionPos); QuadNode<T> rootNode = QuadTree.this.tryGetNode(sectionPos);
@@ -64,7 +64,7 @@ public abstract class AbstractDhWorld implements IDhWorld, Closeable
String levelCountStr = F3Screen.NUMBER_FORMAT.format(this.getLoadedLevelCount()); String levelCountStr = F3Screen.NUMBER_FORMAT.format(this.getLoadedLevelCount());
String readOnlyStr = ""; String readOnlyStr = "";
if (DhApiWorldProxy.INSTANCE.getReadOnly()) if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{ {
readOnlyStr += " - ReadOnly"; readOnlyStr += " - ReadOnly";
} }
@@ -113,6 +113,23 @@ public class DhApiWorldProxy implements IDhApiWorldProxy
return this.isReadOnly; return this.isReadOnly;
} }
/**
* Returns false if no world is loaded.
* Can be used in places where the world state might be a bit more questionable
* without having to worry about the {@link IllegalStateException} thrown by
* {@link DhApiWorldProxy#getReadOnly()}
*/
public boolean tryGetReadOnly()
{
if (SharedApi.getAbstractDhWorld() == null)
{
// no world is loaded, use the default value for the next world
return false;
}
return this.isReadOnly;
}
//================// //================//
+33 -27
View File
@@ -57,7 +57,7 @@ public class QuadTreeTest
public void BasicPositiveQuadTreeTest() public void BasicPositiveQuadTreeTest()
{ {
AbstractTestTreeParams treeParams = new LargeTestTree(); AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertTrue("Tree min/max detail level out of expected bounds: " + tree, tree.treeRootDetailLevel >= 10 && tree.treeLeafDetailLevel <= 10 - 4); Assert.assertTrue("Tree min/max detail level out of expected bounds: " + tree, tree.treeRootDetailLevel >= 10 && tree.treeLeafDetailLevel <= 10 - 4);
@@ -93,7 +93,7 @@ public class QuadTreeTest
public void BasicNegativeQuadTreeTest() public void BasicNegativeQuadTreeTest()
{ {
AbstractTestTreeParams treeParams = new LargeTestTree(); AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL);
// root node // // root node //
@@ -128,7 +128,7 @@ public class QuadTreeTest
public void OutOfBoundsQuadTreeTest() public void OutOfBoundsQuadTreeTest()
{ {
AbstractTestTreeParams treeParams = new LargeTestTree(); AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertEquals("tree diameter incorrect", treeParams.getWidthInBlocks(), tree.diameterInBlocks()); Assert.assertEquals("tree diameter incorrect", treeParams.getWidthInBlocks(), tree.diameterInBlocks());
@@ -170,7 +170,7 @@ public class QuadTreeTest
public void outOfBoundsInTreeTest() public void outOfBoundsInTreeTest()
{ {
// very specific tree parameters to match test results // very specific tree parameters to match test results
QuadTree<Integer> tree = new QuadTree<>(512, new DhBlockPos2D(125, -516), (byte) 6); QuadTree<Integer> tree = new QuadTree<>(512, 8, new DhBlockPos2D(125, -516), (byte) 6);
Assert.assertEquals("Test may need to be re-calculated for different max detail level.", 9, tree.treeRootDetailLevel); Assert.assertEquals("Test may need to be re-calculated for different max detail level.", 9, tree.treeRootDetailLevel);
@@ -192,7 +192,7 @@ public class QuadTreeTest
public void QuadTreeRootAlignedMovingTest() public void QuadTreeRootAlignedMovingTest()
{ {
AbstractTestTreeParams treeParams = new LargeTestTree(); AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
int pseudoRootNodeWidthInBlocks = BitShiftUtil.powerOfTwo(10); int pseudoRootNodeWidthInBlocks = BitShiftUtil.powerOfTwo(10);
@@ -210,7 +210,7 @@ public class QuadTreeTest
testSet(tree, ne, 3); testSet(tree, ne, 3);
testSet(tree, sw, 4); testSet(tree, sw, 4);
testSet(tree, se, 5); testSet(tree, se, 5);
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4); Assert.assertEquals("incorrect leaf node count", 4, tree.leafNodeCount());
// fake move // // fake move //
@@ -237,7 +237,7 @@ public class QuadTreeTest
testGet(tree, ne, 3); testGet(tree, ne, 3);
testGet(tree, sw, 4); testGet(tree, sw, 4);
testGet(tree, se, 5); testGet(tree, se, 5);
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4); Assert.assertEquals("incorrect leaf node count", 4, tree.leafNodeCount());
@@ -252,7 +252,7 @@ public class QuadTreeTest
testGet(tree, sw, null, IndexOutOfBoundsException.class); testGet(tree, sw, null, IndexOutOfBoundsException.class);
testGet(tree, se, null, IndexOutOfBoundsException.class); testGet(tree, se, null, IndexOutOfBoundsException.class);
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 0); Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount());
@@ -276,7 +276,7 @@ public class QuadTreeTest
DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(pseudoRootNodeWidthInBlocks - (treeParams.getWidthInRootNodes() * pseudoRootNodeWidthInBlocks), 0); DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(pseudoRootNodeWidthInBlocks - (treeParams.getWidthInRootNodes() * pseudoRootNodeWidthInBlocks), 0);
tree.setCenterBlockPos(edgeMoveBlockPos); tree.setCenterBlockPos(edgeMoveBlockPos);
Assert.assertEquals("Tree center incorrect", edgeMoveBlockPos, tree.getCenterBlockPos()); Assert.assertEquals("Tree center incorrect", edgeMoveBlockPos, tree.getCenterBlockPos());
Assert.assertEquals("incorrect leaf node count", 2, tree.leafNodeCount()); Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount());
} }
@@ -284,7 +284,7 @@ public class QuadTreeTest
public void QuadTreeIterationTest() public void QuadTreeIterationTest()
{ {
AbstractTestTreeParams treeParams = new LargeTestTree(); AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
// (pseudo) root nodes // // (pseudo) root nodes //
@@ -336,7 +336,7 @@ public class QuadTreeTest
public void QuadTreeIterationFilterTest() public void QuadTreeIterationFilterTest()
{ {
AbstractTestTreeParams treeParams = new TinyTestTree(); AbstractTestTreeParams treeParams = new TinyTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), (byte)0); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), (byte)0);
@@ -418,7 +418,10 @@ public class QuadTreeTest
} }
private static <T> void assertFilterCount(QuadTree<T> tree, String message, int expectedNodeCount, @Nullable QuadTree.INodeIteratorStoppingFunc<T> stoppingFilterFunc) private static <T> void assertFilterCount(
QuadTree<T> tree, String message,
int expectedNodeCount,
@Nullable QuadTree.INodeIteratorStoppingFunc<T> stoppingFilterFunc)
{ {
ArrayList<String> foundNodePositionStrings = new ArrayList<>(); ArrayList<String> foundNodePositionStrings = new ArrayList<>();
@@ -532,7 +535,7 @@ public class QuadTreeTest
public void CenteredGridListIterationTest() public void CenteredGridListIterationTest()
{ {
AbstractTestTreeParams treeParams = new TinyTestTree(); AbstractTestTreeParams treeParams = new TinyTestTree();
final QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); final QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 0), 0); testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 0), 0);
// confirm the root node were added // confirm the root node were added
@@ -573,18 +576,18 @@ public class QuadTreeTest
AbstractTestTreeParams treeParams = new TinyTestTree(); AbstractTestTreeParams treeParams = new TinyTestTree();
// exactly inside (5*0,0) // exactly inside (5*0,0)
testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), 1); testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), 1);
// offset across (5*-1,0) and (5*0,0) // offset across (5*-1,0) and (5*0,0)
testGridListRootCount(treeParams.getWidthInBlocks(), new DhBlockPos2D(-treeParams.getWidthInBlocks() / 4, treeParams.getPositiveEdgeCenterPos().z), 2); testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(-treeParams.getWidthInBlocks() / 4, treeParams.getPositiveEdgeCenterPos().z), 2);
// offset across the origin: (5*0,0), (5*-1,0), (5*0,-1), and (5*-1,-1) // offset across the origin: (5*0,0), (5*-1,0), (5*0,-1), and (5*-1,-1)
testGridListRootCount(treeParams.getWidthInBlocks(), DhBlockPos2D.ZERO, 4); testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), DhBlockPos2D.ZERO, 4);
} }
private static void testGridListRootCount(int treeWidth, DhBlockPos2D treeMovePos, int expectedRootNodeCount) private static void testGridListRootCount(int treeWidth, int treeDistanceForNodeClearing, DhBlockPos2D treeMovePos, int expectedRootNodeCount)
{ {
final QuadTree<Integer> tree = new QuadTree<>(treeWidth, DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL); final QuadTree<Integer> tree = new QuadTree<>(treeWidth, treeDistanceForNodeClearing, DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertEquals("tree creation failed, incorrect initial position", DhBlockPos2D.ZERO, tree.getCenterBlockPos()); Assert.assertEquals("tree creation failed, incorrect initial position", DhBlockPos2D.ZERO, tree.getCenterBlockPos());
tree.setCenterBlockPos(treeMovePos); tree.setCenterBlockPos(treeMovePos);
@@ -615,7 +618,7 @@ public class QuadTreeTest
public void TinyGridAlignedTreeTest() public void TinyGridAlignedTreeTest()
{ {
AbstractTestTreeParams treeParams = new MediumTestTree(); AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
// minimum size tree should be 3 root nodes wide // minimum size tree should be 3 root nodes wide
Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth()); Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth());
Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks()); Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks());
@@ -644,7 +647,7 @@ public class QuadTreeTest
public void TinyGridOffsetTreeTest() public void TinyGridOffsetTreeTest()
{ {
AbstractTestTreeParams treeParams = new MediumTestTree(); AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
// minimum size tree should be 3 root nodes wide // minimum size tree should be 3 root nodes wide
Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth()); Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth());
Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks()); Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks());
@@ -683,7 +686,7 @@ public class QuadTreeTest
public void TreeDetailLevelLimitTest() public void TreeDetailLevelLimitTest()
{ {
AbstractTestTreeParams treeParams = new MediumTestTree(); AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 8); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), (byte) 8);
Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel); Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel);
// valid detail levels // valid detail levels
@@ -705,7 +708,7 @@ public class QuadTreeTest
public void QuadNodeDetailLimitTest() public void QuadNodeDetailLimitTest()
{ {
AbstractTestTreeParams treeParams = new MediumTestTree(); AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 6); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), (byte) 6);
Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel); Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel);
// create the root node // create the root node
@@ -817,8 +820,7 @@ public class QuadTreeTest
@Test @Test
public void quadNodeChildPositionOutOfBoundsTest() public void quadNodeChildPositionOutOfBoundsTest()
{ {
int treeWidthInBlocks = 64; QuadTree<Integer> tree = new QuadTree<>(64, 1, new DhBlockPos2D(-2, 0), (byte) 0);
QuadTree<Integer> tree = new QuadTree<>(treeWidthInBlocks, new DhBlockPos2D(-2, 0), (byte) 0);
@@ -865,7 +867,7 @@ public class QuadTreeTest
public void toStringTest() public void toStringTest()
{ {
AbstractTestTreeParams treeParams = new MediumTestTree(); AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 6); QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), (byte) 6);
String treeString = tree.toString(); String treeString = tree.toString();
Assert.assertNotNull(treeString); Assert.assertNotNull(treeString);
@@ -929,25 +931,28 @@ public class QuadTreeTest
private abstract static class AbstractTestTreeParams private abstract static class AbstractTestTreeParams
{ {
public abstract int getWidthInBlocks(); public abstract int getWidthInBlocks();
public abstract int getBlockDistanceForNodeClearing();
/** the tree should be slightly larger than the width in blocks to account for offset centers */ /** the tree should be slightly larger than the width in blocks to account for offset centers */
public int getWidthInRootNodes() { return MathUtil.log2(this.getWidthInBlocks()) + 2; } public int getWidthInRootNodes() { return MathUtil.log2(this.getWidthInBlocks()) + 2; }
/** the top (root) detail level in the tree */ /** the top (root) detail level in the tree */
public byte getMinDetailLevel() { return (byte) MathUtil.log2(this.getWidthInBlocks()); } public byte getRootDetailLevel() { return (byte) MathUtil.log2(this.getWidthInBlocks()); }
/** @return the block pos so that the tree's negative corner lines up with (0,0) */ /** @return the block pos so that the tree's negative corner lines up with (0,0) */
public DhBlockPos2D getPositiveEdgeCenterPos() { return new DhBlockPos2D(BitShiftUtil.powerOfTwo(this.getMinDetailLevel()) / 2, BitShiftUtil.powerOfTwo(this.getMinDetailLevel()) / 2); } public DhBlockPos2D getPositiveEdgeCenterPos() { return new DhBlockPos2D(BitShiftUtil.powerOfTwo(this.getRootDetailLevel()) / 2, BitShiftUtil.powerOfTwo(this.getRootDetailLevel()) / 2); }
} }
private static class LargeTestTree extends AbstractTestTreeParams private static class LargeTestTree extends AbstractTestTreeParams
{ {
public int getWidthInBlocks() { return 8192; } public int getWidthInBlocks() { return 8192; }
public int getBlockDistanceForNodeClearing() { return 16; }
} }
private static class MediumTestTree extends AbstractTestTreeParams private static class MediumTestTree extends AbstractTestTreeParams
{ {
public int getWidthInBlocks() { return 1024; } public int getWidthInBlocks() { return 1024; }
public int getBlockDistanceForNodeClearing() { return 4; }
} }
@@ -955,6 +960,7 @@ public class QuadTreeTest
{ {
// top detail level = 6 // top detail level = 6
public int getWidthInBlocks() { return 32; } public int getWidthInBlocks() { return 32; }
public int getBlockDistanceForNodeClearing() { return 1; }
} }