Add Api world load/unload events and DhApiWorldProxy.get/setReadOnly()

This commit is contained in:
James Seibel
2024-09-28 08:33:19 -05:00
parent d89d99f126
commit 566b536c8d
10 changed files with 259 additions and 19 deletions
@@ -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. <br>
* 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. <br>
@@ -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 <https://www.gnu.org/licenses/>.
*/
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. <br>
* 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<DhApiWorldLoadEvent.EventParam>
{
/** Fired after Distant Horizons loads a new level. */
public abstract void onLevelLoad(DhApiEventParam<EventParam> input);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<EventParam> input) { this.onLevelLoad(input); }
//==================//
// parameter object //
//==================//
public static class EventParam implements IDhApiEventParam
{
public EventParam() { }
@Override
public EventParam copy() { return new EventParam(); }
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<DhApiWorldUnloadEvent.EventParam>
{
/** Fired before Distant Horizons unloads a level. */
public abstract void onLevelUnload(DhApiEventParam<EventParam> input);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<EventParam> input) { this.onLevelUnload(input); }
//==================//
// parameter object //
//==================//
public static class EventParam implements IDhApiEventParam
{
public EventParam() { }
@Override
public DhApiWorldLoadEvent.EventParam copy() { return new DhApiWorldLoadEvent.EventParam(); }
}
}
@@ -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());
@@ -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();
@@ -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);
}
@@ -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;
@@ -59,6 +59,18 @@ public abstract class AbstractDhWorld implements IDhWorld, Closeable
* by overriding children.
*/
public void addDebugMenuStringsToList(List<String> 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);
}
}
@@ -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;
}
}
@@ -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
{