diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/world/IDhApiWorldProxy.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/world/IDhApiWorldProxy.java
index 8daba387a..e95cb82df 100644
--- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/world/IDhApiWorldProxy.java
+++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/world/IDhApiWorldProxy.java
@@ -24,14 +24,39 @@ package com.seibel.distanthorizons.api.interfaces.world;
* A world is equivalent to a single server connection or a singleplayer world.
*
* @author James Seibel
- * @version 2022-11-20
+ * @version 2024-9-27
* @since API 1.0.0
*/
public interface IDhApiWorldProxy
{
+ //===================//
+ // getters / setters //
+ //===================//
+
/** Returns true if a world is loaded. */
boolean worldLoaded();
+ /**
+ * Defaults to false.
+ * Setting this to true will prevent DH from updating or creating new LODs.
+ *
+ * @since API 4.0.0
+ * @see IDhApiWorldProxy#getReadOnly()
+ * @throws IllegalStateException if no world is loaded
+ */
+ void setReadOnly(boolean readOnly) throws IllegalStateException;
+ /**
+ * @since API 4.0.0
+ * @see IDhApiWorldProxy#setReadOnly(boolean)
+ * @throws IllegalStateException if no world is loaded
+ */
+ boolean getReadOnly() throws IllegalStateException;
+
+
+
+ //================//
+ // level handlers //
+ //================//
/**
* In singleplayer this will return the level the player is currently in.
diff --git a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiWorldLoadEvent.java b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiWorldLoadEvent.java
new file mode 100644
index 000000000..310b2df5c
--- /dev/null
+++ b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiWorldLoadEvent.java
@@ -0,0 +1,65 @@
+/*
+ * This file is part of the Distant Horizons mod
+ * licensed under the GNU LGPL v3 License.
+ *
+ * Copyright (C) 2020-2023 James Seibel
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.seibel.distanthorizons.api.methods.events.abstractEvents;
+
+import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
+import com.seibel.distanthorizons.api.interfaces.world.IDhApiWorldProxy;
+import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
+import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
+import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
+
+/**
+ * Called after Distant Horizons finishes loading a new level.
+ * Note: this may be fired before Minecraft has loaded in the player.
+ *
+ * @see IDhApiWorldProxy
+ *
+ * @author James Seibel
+ * @version 2024-9-27
+ * @since API 4.0.0
+ */
+public abstract class DhApiWorldLoadEvent implements IDhApiEvent
+{
+ /** Fired after Distant Horizons loads a new level. */
+ public abstract void onLevelLoad(DhApiEventParam input);
+
+
+ //=========================//
+ // internal DH API methods //
+ //=========================//
+
+ @Override
+ public final void fireEvent(DhApiEventParam input) { this.onLevelLoad(input); }
+
+
+ //==================//
+ // parameter object //
+ //==================//
+
+ public static class EventParam implements IDhApiEventParam
+ {
+ public EventParam() { }
+
+
+ @Override
+ public EventParam copy() { return new EventParam(); }
+ }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiWorldUnloadEvent.java b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiWorldUnloadEvent.java
new file mode 100644
index 000000000..5276fff31
--- /dev/null
+++ b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiWorldUnloadEvent.java
@@ -0,0 +1,64 @@
+/*
+ * This file is part of the Distant Horizons mod
+ * licensed under the GNU LGPL v3 License.
+ *
+ * Copyright (C) 2020-2023 James Seibel
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.seibel.distanthorizons.api.methods.events.abstractEvents;
+
+import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
+import com.seibel.distanthorizons.api.interfaces.world.IDhApiWorldProxy;
+import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
+import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
+import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
+
+/**
+ * Called after Distant Horizons has finished unloading a level.
+ *
+ * @see IDhApiWorldProxy
+ *
+ * @author James Seibel
+ * @version 2024-9-27
+ * @since API 4.0.0
+ */
+public abstract class DhApiWorldUnloadEvent implements IDhApiEvent
+{
+ /** Fired before Distant Horizons unloads a level. */
+ public abstract void onLevelUnload(DhApiEventParam input);
+
+
+ //=========================//
+ // internal DH API methods //
+ //=========================//
+
+ @Override
+ public final void fireEvent(DhApiEventParam input) { this.onLevelUnload(input); }
+
+
+ //==================//
+ // parameter object //
+ //==================//
+
+ public static class EventParam implements IDhApiEventParam
+ {
+ public EventParam() { }
+
+
+ @Override
+ public DhApiWorldLoadEvent.EventParam copy() { return new DhApiWorldLoadEvent.EventParam(); }
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
index 97b6ba83f..d68ed239b 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java
@@ -19,6 +19,8 @@
package com.seibel.distanthorizons.core.api.internal;
+import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent;
+import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldUnloadEvent;
import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
@@ -39,6 +41,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
+import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -89,6 +92,8 @@ public class SharedApi
if (currentWorld != null)
{
ThreadPoolUtil.setupThreadPools();
+
+ ApiEventInjector.INSTANCE.fireAllEvents(DhApiWorldLoadEvent.class, new DhApiWorldLoadEvent.EventParam());
}
else
{
@@ -102,6 +107,11 @@ public class SharedApi
// recommend that the garbage collector cleans up any objects from the old world and thread pools
System.gc();
+
+ ApiEventInjector.INSTANCE.fireAllEvents(DhApiWorldUnloadEvent.class, new DhApiWorldUnloadEvent.EventParam());
+
+ // fired after the unload event so API users can't change the read-only for any new worlds
+ DhApiWorldProxy.INSTANCE.setReadOnly(false, false);
}
}
@@ -170,6 +180,13 @@ public class SharedApi
return;
}
+ // ignore updates if the world is read-only
+ if (DhApiWorldProxy.INSTANCE.getReadOnly())
+ {
+ return;
+ }
+
+
// only continue if the level is loaded
IDhLevel dhLevel = dhWorld.getLevel(level);
if (dhLevel == null)
@@ -261,10 +278,7 @@ public class SharedApi
}
}
}
-
-
/** returning a {@link CompletableFuture} isn't necessary, but allows Intellij to properly show the full stack trace when debugging. */
- @SuppressWarnings("UnusedReturnValue")
private static void processQueuedChunkUpdate()
{
//LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount());
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java
index 67e90d450..36123a2da 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java
@@ -40,6 +40,7 @@ import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
+import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
@@ -211,7 +212,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
try
{
// loop until the generator is shutdown
- while (!Thread.interrupted())
+ while (!Thread.interrupted() && !DhApiWorldProxy.INSTANCE.getReadOnly())
{
this.generator.preGeneratorTaskStart();
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java
index 9b0c4716d..847b186e5 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
+import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -60,7 +61,7 @@ public class WorldGenModule implements Closeable
GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener,
GeneratedFullDataSourceProvider dataSourceProvider,
Supplier extends AbstractWorldGenState> worldGenStateSupplier
- )
+ )
{
this.onWorldGenCompleteListener = onWorldGenCompleteListener;
this.dataSourceProvider = dataSourceProvider;
@@ -111,6 +112,8 @@ public class WorldGenModule implements Closeable
public void worldGenTick()
{
boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen();
+ // if the world is read only don't generate anything
+ shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.getReadOnly();
boolean isWorldGenRunning = this.isWorldGenRunning();
if (shouldDoWorldGen && !isWorldGenRunning)
@@ -188,8 +191,8 @@ public class WorldGenModule implements Closeable
String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount());
String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount());
String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount());
- messageList.add("World Gen Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress: "+inProgressCountStr+")");
-
+ messageList.add("World Gen Tasks: ${waitingCountStr}/${totalCountEstimateStr} (in progress: ${inProgressCountStr})");
+
worldGenState.worldGenerationQueue.addDebugMenuStringsToList(messageList);
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java
index b162ba5ad..feb720750 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/multiplayer/client/AbstractFullDataNetworkRequestQueue.java
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateLimiter;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
+import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import org.apache.logging.log4j.LogManager;
@@ -133,6 +134,11 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
public synchronized boolean tick(DhBlockPos2D targetPos)
{
+ if (DhApiWorldProxy.INSTANCE.getReadOnly())
+ {
+ return false;
+ }
+
if (this.closingFuture != null || !this.networkState.isReady())
{
return false;
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhWorld.java b/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhWorld.java
index ffa051290..17615a238 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhWorld.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/world/AbstractDhWorld.java
@@ -59,6 +59,18 @@ public abstract class AbstractDhWorld implements IDhWorld, Closeable
* by overriding children.
*/
public void addDebugMenuStringsToList(List messageList)
- { messageList.add(this.environment + " World with " + F3Screen.NUMBER_FORMAT.format(this.getLoadedLevelCount()) + " levels"); }
+ {
+ EWorldEnvironment environment = this.environment;
+ String levelCountStr = F3Screen.NUMBER_FORMAT.format(this.getLoadedLevelCount());
+
+ String readOnlyStr = "";
+ if (DhApiWorldProxy.INSTANCE.getReadOnly())
+ {
+ readOnlyStr += " - ReadOnly";
+ }
+
+ String message = "${environment} World with ${levelCountStr} levels${readOnlyStr}";
+ messageList.add(message);
+ }
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/world/DhApiWorldProxy.java b/core/src/main/java/com/seibel/distanthorizons/core/world/DhApiWorldProxy.java
index 3d61c026c..ce17bf93c 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/world/DhApiWorldProxy.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/world/DhApiWorldProxy.java
@@ -25,9 +25,11 @@ import com.seibel.distanthorizons.api.interfaces.world.IDhApiWorldProxy;
import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhLevel;
+import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
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 org.apache.logging.log4j.Logger;
import java.util.ArrayList;
@@ -38,16 +40,20 @@ import java.util.ArrayList;
* to be loaded/unloaded.
*
* @author James Seibel
- * @version 2022-11-20
+ * @version 2024-9-27
*/
public class DhApiWorldProxy implements IDhApiWorldProxy
{
public static DhApiWorldProxy INSTANCE = new DhApiWorldProxy();
+ private static final Logger LOGGER = DhLoggerBuilder.getLogger();
+
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
private static final String NO_WORLD_EXCEPTION_STRING = "No world loaded";
+ private boolean isReadOnly = false;
+
//=============//
@@ -58,13 +64,61 @@ public class DhApiWorldProxy implements IDhApiWorldProxy
- //=========//
- // methods //
- //=========//
+ //===================//
+ // getters / setters //
+ //===================//
@Override
public boolean worldLoaded() { return SharedApi.getAbstractDhWorld() != null; }
+ @Override
+ public void setReadOnly(boolean readOnly) { this.setReadOnly(readOnly, true); }
+ /**
+ * Not part of the public API.
+ * Normal API users shouldn't be able to change the upcoming world state
+ * this is only here so DH can revert the readonly value after the world is unloaded
+ */
+ public void setReadOnly(boolean readOnly, boolean throwIfWorldUnloaded)
+ {
+ if (throwIfWorldUnloaded && SharedApi.getAbstractDhWorld() == null)
+ {
+ throw new IllegalStateException(NO_WORLD_EXCEPTION_STRING);
+ }
+
+ boolean valueChanged = (this.isReadOnly != readOnly);
+ this.isReadOnly = readOnly;
+
+ if (valueChanged)
+ {
+ if (this.isReadOnly)
+ {
+ LOGGER.info("DH world set to read-only. LODs will not update while this API flag is active.");
+ }
+ else
+ {
+ LOGGER.info("DH world is no longer in read-only mode. LODs will update like normal.");
+ }
+ }
+ }
+
+ @Override
+ public boolean getReadOnly()
+ {
+ if (SharedApi.getAbstractDhWorld() == null)
+ {
+ throw new IllegalStateException(NO_WORLD_EXCEPTION_STRING);
+ }
+
+
+ return this.isReadOnly;
+ }
+
+
+
+ //================//
+ // level handlers //
+ //================//
+
@Override
public IDhApiLevelWrapper getSinglePlayerLevel()
{
@@ -146,4 +200,5 @@ public class DhApiWorldProxy implements IDhApiWorldProxy
return returnList;
}
+
}
diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/ILevelWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/ILevelWrapper.java
index 907c68b25..550a88428 100644
--- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/ILevelWrapper.java
+++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/world/ILevelWrapper.java
@@ -27,12 +27,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
-/**
- * Can be either a Server world or a Client world.
- *
- * @author James Seibel
- * @version 2023-6-17
- */
+/** Can be either a Server world or a Client world. */
public interface ILevelWrapper extends IDhApiLevelWrapper, IBindable
{