diff --git a/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java b/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java index f3ef78f8..bf4a193a 100644 --- a/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java +++ b/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java @@ -1,27 +1,35 @@ package me.cortex.voxy.client; import me.cortex.voxy.client.config.VoxyConfig; -import me.cortex.voxy.client.saver.ContextSelectionSystem; import me.cortex.voxy.common.Logger; -import me.cortex.voxy.common.util.Pair; -import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.commonImpl.IVoxyWorld; +import me.cortex.voxy.common.config.ConfigBuildCtx; +import me.cortex.voxy.common.config.Serialization; +import me.cortex.voxy.common.config.compressors.ZSTDCompressor; +import me.cortex.voxy.common.config.section.SectionSerializationStorage; +import me.cortex.voxy.common.config.section.SectionStorage; +import me.cortex.voxy.common.config.section.SectionStorageConfig; +import me.cortex.voxy.common.config.storage.other.CompressionStorageAdaptor; +import me.cortex.voxy.common.config.storage.rocksdb.RocksDBStorageBackend; import me.cortex.voxy.commonImpl.ImportManager; import me.cortex.voxy.commonImpl.VoxyInstance; +import me.cortex.voxy.commonImpl.WorldIdentifier; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.world.World; +import net.minecraft.util.WorldSavePath; -import java.util.Random; -import java.util.concurrent.ConcurrentLinkedDeque; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; public class VoxyClientInstance extends VoxyInstance { public static boolean isInGame = false; - private static final ContextSelectionSystem SELECTOR = new ContextSelectionSystem(); - + private final SectionStorageConfig storageConfig; + private final Path basePath = getBasePath(); public VoxyClientInstance() { super(VoxyConfig.CONFIG.serviceThreads); + this.storageConfig = getCreateStorageConfig(this.basePath); } @Override @@ -29,37 +37,123 @@ public class VoxyClientInstance extends VoxyInstance { return new ClientImportManager(); } + @Override + protected SectionStorage createStorage(WorldIdentifier identifier) { + var ctx = new ConfigBuildCtx(); + ctx.setProperty(ConfigBuildCtx.BASE_SAVE_PATH, this.basePath.toString()); + ctx.setProperty(ConfigBuildCtx.WORLD_IDENTIFIER, getWorldId(identifier)); + ctx.pushPath(ConfigBuildCtx.DEFAULT_STORAGE_PATH); + return this.storageConfig.build(ctx); + } - private WorldEngine getOrCreateEngine(World world) { - /* - ClientWorld cw = null; - if (world instanceof ClientWorld && MinecraftClient.getInstance().isIntegratedServerRunning()) { - cw = (ClientWorld) world; - var world2 = MinecraftClient.getInstance().getServer().getWorld(world.getRegistryKey()); - if (world2 == null) { - Logger.error("could not get server world for client world with registry key: " + world.getRegistryKey()); - } else { - world = world2; + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); } - }*/ - var vworld = ((IVoxyWorld)world).getWorldEngine(); - if (vworld == null) { - vworld = this.createWorld(SELECTOR.getBestSelectionOrCreate(world).createSectionStorageBackend()); - ((IVoxyWorld)world).setWorldEngine(vworld); - //testDbPerformance2(vworld); - } else { - if (!this.activeWorlds.contains(vworld)) { - throw new IllegalStateException("World referenced does not exist in instance"); + hexString.append(hex); + } + return hexString.toString(); + } + + private static String getWorldId(WorldIdentifier identifier) { + String data = identifier.biomeSeed + identifier.key.toString(); + try { + return bytesToHex(MessageDigest.getInstance("SHA-256").digest(data.getBytes())).substring(0, 32); + } catch ( + NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static SectionStorageConfig getCreateStorageConfig(Path path) { + var json = path.resolve("config.json"); + Config config = null; + if (Files.exists(json)) { + try { + config = Serialization.GSON.fromJson(Files.readString(json), Config.class); + if (config == null) { + throw new IllegalStateException("Config deserialization null, reverting to default"); + } + if (config.sectionStorageConfig == null) { + throw new IllegalStateException("Config section storage null, reverting to default"); + } + } catch (Exception e) { + Logger.error("Failed to load the storage configuration file, resetting it to default, this will probably break your save if you used a custom storage config", e); } } - return vworld; + + try { + config = DEFAULT_STORAGE_CONFIG; + + try { + Files.writeString(json, Serialization.GSON.toJson(config)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize the default config, aborting!", e); + } + if (config == null) { + throw new IllegalStateException("Config is still null\n"); + } + return config.sectionStorageConfig; } - public WorldEngine getOrMakeRenderWorld(World world) { - return this.getOrCreateEngine(world); + private static class Config { + public SectionStorageConfig sectionStorageConfig; + } + private static final Config DEFAULT_STORAGE_CONFIG; + static { + var config = new Config(); + + //Load the default config + var baseDB = new RocksDBStorageBackend.Config(); + + var compressor = new ZSTDCompressor.Config(); + compressor.compressionLevel = 1; + + var compression = new CompressionStorageAdaptor.Config(); + compression.delegate = baseDB; + compression.compressor = compressor; + + var serializer = new SectionSerializationStorage.Config(); + serializer.storage = compression; + config.sectionStorageConfig = serializer; + + DEFAULT_STORAGE_CONFIG = config; } + private static Path getBasePath() { + Path basePath = MinecraftClient.getInstance().runDirectory.toPath().resolve(".voxy").resolve("saves"); + var iserver = MinecraftClient.getInstance().getServer(); + if (iserver != null) { + basePath = iserver.getSavePath(WorldSavePath.ROOT).resolve("voxy"); + } else { + var netHandle = MinecraftClient.getInstance().interactionManager; + if (netHandle == null) { + Logger.error("Network handle null"); + basePath = basePath.resolve("UNKNOWN"); + } else { + var info = netHandle.networkHandler.getServerInfo(); + if (info == null) { + Logger.error("Server info null"); + basePath = basePath.resolve("UNKNOWN"); + } else { + if (info.isRealm()) { + basePath = basePath.resolve("realms"); + } else { + basePath = basePath.resolve(info.address.replace(":", "_")); + } + } + } + } + return basePath.toAbsolutePath(); + } + /* private static void testDbPerformance(WorldEngine engine) { Random r = new Random(123456); r.nextLong(); @@ -149,4 +243,5 @@ public class VoxyClientInstance extends VoxyInstance { } } } + */ } diff --git a/src/main/java/me/cortex/voxy/client/VoxyCommands.java b/src/main/java/me/cortex/voxy/client/VoxyCommands.java index 7edaf22d..6c8ec6f3 100644 --- a/src/main/java/me/cortex/voxy/client/VoxyCommands.java +++ b/src/main/java/me/cortex/voxy/client/VoxyCommands.java @@ -6,8 +6,9 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import me.cortex.voxy.client.core.IGetVoxyRenderSystem; -import me.cortex.voxy.commonImpl.IVoxyWorld; +import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.commonImpl.VoxyCommon; +import me.cortex.voxy.commonImpl.WorldIdentifier; import me.cortex.voxy.commonImpl.importers.DHImporter; import me.cortex.voxy.commonImpl.importers.WorldImporter; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; @@ -67,8 +68,6 @@ public class VoxyCommands { if (wr!=null) { ((IGetVoxyRenderSystem)wr).shutdownRenderer(); } - var w = ((IVoxyWorld)MinecraftClient.getInstance().world); - if (w != null) w.shutdownEngine(); VoxyCommon.shutdownInstance(); VoxyCommon.createInstance(); @@ -98,9 +97,10 @@ public class VoxyCommands { } File dbFile_ = dbFile; - var engine = instance.getOrMakeRenderWorld(MinecraftClient.getInstance().player.clientWorld); + var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().player.clientWorld); + if (engine==null)return 1; return instance.getImportManager().makeAndRunIfNone(engine, ()-> - new DHImporter(dbFile_, engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.getSavingService()))?0:1; + new DHImporter(dbFile_, engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.savingServiceRateLimiter))?0:1; } private static boolean fileBasedImporter(File directory) { @@ -108,9 +108,11 @@ public class VoxyCommands { if (instance == null) { return false; } - var engine = instance.getOrMakeRenderWorld(MinecraftClient.getInstance().player.clientWorld); + + var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().player.clientWorld); + if (engine==null) return false; return instance.getImportManager().makeAndRunIfNone(engine, ()->{ - var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.getSavingService()); + var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.savingServiceRateLimiter); importer.importRegionDirectoryAsync(directory); return importer; }); @@ -200,12 +202,15 @@ public class VoxyCommands { } String finalInnerDir = innerDir; - var engine = instance.getOrMakeRenderWorld(MinecraftClient.getInstance().player.clientWorld); - return instance.getImportManager().makeAndRunIfNone(engine, ()->{ - var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.getSavingService()); - importer.importZippedRegionDirectoryAsync(zip, finalInnerDir); - return importer; - })?0:1; + var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().player.clientWorld); + if (engine != null) { + return instance.getImportManager().makeAndRunIfNone(engine, () -> { + var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.savingServiceRateLimiter); + importer.importZippedRegionDirectoryAsync(zip, finalInnerDir); + return importer; + }) ? 0 : 1; + } + return 1; } private static int cancelImport(CommandContext fabricClientCommandSourceCommandContext) { @@ -213,7 +218,10 @@ public class VoxyCommands { if (instance == null) { return 1; } - var world = instance.getOrMakeRenderWorld(MinecraftClient.getInstance().player.clientWorld); - return instance.getImportManager().cancelImport(world)?0:1; + var world = WorldIdentifier.ofEngineNullable(MinecraftClient.getInstance().player.clientWorld); + if (world != null) { + return instance.getImportManager().cancelImport(world)?0:1; + } + return 1; } } \ No newline at end of file diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenPages.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenPages.java index 392a69c4..73735460 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenPages.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenPages.java @@ -1,16 +1,11 @@ package me.cortex.voxy.client.config; import com.google.common.collect.ImmutableList; -import com.terraformersmc.modmenu.api.ConfigScreenFactory; -import com.terraformersmc.modmenu.api.ModMenuApi; import me.cortex.voxy.client.RenderStatistics; import me.cortex.voxy.client.VoxyClientInstance; import me.cortex.voxy.client.core.IGetVoxyRenderSystem; -import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.util.cpu.CpuLayout; -import me.cortex.voxy.commonImpl.IVoxyWorld; import me.cortex.voxy.commonImpl.VoxyCommon; -import net.caffeinemc.mods.sodium.client.gui.SodiumOptionsGUI; import net.caffeinemc.mods.sodium.client.gui.options.*; import net.caffeinemc.mods.sodium.client.gui.options.control.SliderControl; import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl; @@ -50,10 +45,6 @@ public abstract class VoxyConfigScreenPages { if (vrsh != null) { vrsh.shutdownRenderer(); } - var world = (IVoxyWorld) MinecraftClient.getInstance().world; - if (world != null) { - world.shutdownEngine(); - } VoxyCommon.shutdownInstance(); } }, s -> s.enabled) @@ -72,11 +63,6 @@ public abstract class VoxyConfigScreenPages { if (vrsh != null) { vrsh.shutdownRenderer(); } - var world = (IVoxyWorld) MinecraftClient.getInstance().world; - if (world != null) { - world.shutdownEngine(); - } - VoxyCommon.shutdownInstance(); } diff --git a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java index cbac4ba3..ef686e30 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java @@ -42,9 +42,13 @@ import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; +import static org.lwjgl.opengl.GL11.GL_ONE; +import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA; +import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA; import static org.lwjgl.opengl.GL11.GL_VIEWPORT; import static org.lwjgl.opengl.GL11.glGetIntegerv; import static org.lwjgl.opengl.GL11C.*; +import static org.lwjgl.opengl.GL14.glBlendFuncSeparate; import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER_BINDING; import static org.lwjgl.opengl.GL30C.glBindFramebuffer; import static org.lwjgl.opengl.GL33.glBindSampler; @@ -82,6 +86,9 @@ public class VoxyRenderSystem { this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance); this.chunkBoundRenderer = new ChunkBoundRenderer(); + + //Keep the world loaded + this.worldIn.acquireRef(); } public void setRenderDistance(int renderDistance) { @@ -285,6 +292,9 @@ public class VoxyRenderSystem { try {this.renderer.shutdown();this.chunkBoundRenderer.free();} catch (Exception e) {Logger.error("Error shutting down renderer", e);} Logger.info("Shutting down post processor"); if (this.postProcessing!=null){try {this.postProcessing.shutdown();} catch (Exception e) {Logger.error("Error shutting down post processor", e);}} + + //Release hold on the world + this.worldIn.releaseRef(); } diff --git a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinWorldRenderer.java b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinWorldRenderer.java index 60c7b4a5..fea51c57 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinWorldRenderer.java +++ b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinWorldRenderer.java @@ -6,8 +6,8 @@ import me.cortex.voxy.client.core.IGetVoxyRenderSystem; import me.cortex.voxy.client.core.VoxyRenderSystem; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.commonImpl.IVoxyWorld; import me.cortex.voxy.commonImpl.VoxyCommon; +import me.cortex.voxy.commonImpl.WorldIdentifier; import net.minecraft.client.render.*; import net.minecraft.client.util.ObjectAllocator; import net.minecraft.client.world.ClientWorld; @@ -50,10 +50,6 @@ public abstract class MixinWorldRenderer implements IGetVoxyRenderSystem { private void voxy$captureSetWorld(ClientWorld world, CallbackInfo ci) { if (this.world != world) { this.shutdownRenderer(); - - if (this.world != null) { - ((IVoxyWorld)this.world).shutdownEngine(); - } } } @@ -86,7 +82,7 @@ public abstract class MixinWorldRenderer implements IGetVoxyRenderSystem { Logger.error("Not creating renderer due to null instance"); return; } - WorldEngine world = instance.getOrMakeRenderWorld(this.world); + WorldEngine world = WorldIdentifier.ofEngine(this.world); if (world == null) { Logger.error("Null world selected"); return; diff --git a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java index 99263614..5fcefec1 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java +++ b/src/main/java/me/cortex/voxy/client/mixin/sodium/MixinRenderSectionManager.java @@ -5,7 +5,10 @@ import me.cortex.voxy.client.VoxyClientInstance; import me.cortex.voxy.client.config.VoxyConfig; import me.cortex.voxy.client.core.IGetVoxyRenderSystem; import me.cortex.voxy.client.core.VoxyRenderSystem; +import me.cortex.voxy.common.world.WorldEngine; +import me.cortex.voxy.common.world.service.VoxelIngestService; import me.cortex.voxy.commonImpl.VoxyCommon; +import me.cortex.voxy.commonImpl.WorldIdentifier; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; @@ -61,16 +64,8 @@ public class MixinRenderSectionManager { @Inject(method = "onChunkRemoved", at = @At("HEAD")) private void injectIngest(int x, int z, CallbackInfo ci) { //TODO: Am not quite sure if this is right - var instance = VoxyCommon.getInstance(); - if (instance != null && VoxyConfig.CONFIG.ingestEnabled) { - var chunk = this.level.getChunk(x, z); - var world = chunk.getWorld(); - if (world instanceof ClientWorld cw) { - var engine = ((VoxyClientInstance)instance).getOrMakeRenderWorld(cw); - if (engine != null) { - instance.getIngestService().enqueueIngest(engine, chunk); - } - } + if (VoxyConfig.CONFIG.ingestEnabled) { + VoxelIngestService.tryAutoIngestChunk(this.level.getChunk(x, z)); } } @@ -100,13 +95,4 @@ public class MixinRenderSectionManager { } return true; } - - /* - @ModifyReturnValue(method = "getSearchDistance", at = @At("RETURN")) - private float voxy$increaseSearchDistanceFix(float searchDistance) { - if (((IGetVoxyRenderSystem)(this.level.worldRenderer)).getVoxyRenderSystem() == null) { - return searchDistance; - } - return searchDistance + 32; - }*/ } diff --git a/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java b/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java deleted file mode 100644 index d744d83e..00000000 --- a/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java +++ /dev/null @@ -1,188 +0,0 @@ -package me.cortex.voxy.client.saver; - -import me.cortex.voxy.client.config.VoxyConfig; -import me.cortex.voxy.common.Logger; -import me.cortex.voxy.common.config.section.SectionStorageConfig; -import me.cortex.voxy.common.config.section.SectionSerializationStorage; -import me.cortex.voxy.common.config.section.SectionStorage; -import me.cortex.voxy.common.config.compressors.ZSTDCompressor; -import me.cortex.voxy.common.config.ConfigBuildCtx; -import me.cortex.voxy.common.config.Serialization; -import me.cortex.voxy.common.config.storage.other.CompressionStorageAdaptor; -import me.cortex.voxy.common.config.storage.rocksdb.RocksDBStorageBackend; -import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.common.thread.ServiceThreadPool; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.util.WorldSavePath; -import net.minecraft.world.World; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -//Sets up a world engine with respect to the world the client is currently loaded into -// this is a bit tricky as each world has its own config, e.g. storage configuration -public class ContextSelectionSystem { - public static class WorldConfig { - public int minYOverride = Integer.MAX_VALUE; - public int maxYOverride = Integer.MIN_VALUE; - public SectionStorageConfig sectionStorageConfig; - } - public static final WorldConfig DEFAULT_STORAGE_CONFIG; - static { - var config = new WorldConfig(); - - //Load the default config - var baseDB = new RocksDBStorageBackend.Config(); - - var compressor = new ZSTDCompressor.Config(); - compressor.compressionLevel = 1; - - var compression = new CompressionStorageAdaptor.Config(); - compression.delegate = baseDB; - compression.compressor = compressor; - - var serializer = new SectionSerializationStorage.Config(); - serializer.storage = compression; - config.sectionStorageConfig = serializer; - - DEFAULT_STORAGE_CONFIG = config; - } - - public static class Selection { - private final Path selectionFolder; - private final String worldId; - - private WorldConfig config; - - public Selection(Path selectionFolder, String worldId) { - this.selectionFolder = selectionFolder; - this.worldId = worldId; - loadStorageConfigOrDefault(); - } - - private void loadStorageConfigOrDefault() { - var json = this.selectionFolder.resolve("config.json"); - - if (Files.exists(json)) { - try { - this.config = Serialization.GSON.fromJson(Files.readString(json), WorldConfig.class); - if (this.config == null) { - throw new IllegalStateException("Config deserialization null, reverting to default"); - } - if (this.config.sectionStorageConfig == null) { - throw new IllegalStateException("Config section storage null, reverting to default"); - } - return; - } catch (Exception e) { - Logger.error("Failed to load the storage configuration file, resetting it to default, this will probably break your save if you used a custom storage config", e); - } - } - - try { - this.config = DEFAULT_STORAGE_CONFIG; - this.save(); - } catch (Exception e) { - throw new RuntimeException("Failed to deserialize the default config, aborting!", e); - } - if (this.config == null) { - throw new IllegalStateException("Config is still null\n"); - } - } - - public SectionStorage createSectionStorageBackend() { - var ctx = new ConfigBuildCtx(); - ctx.setProperty(ConfigBuildCtx.BASE_SAVE_PATH, this.selectionFolder.toString()); - ctx.setProperty(ConfigBuildCtx.WORLD_IDENTIFIER, this.worldId); - ctx.pushPath(ConfigBuildCtx.DEFAULT_STORAGE_PATH); - return this.config.sectionStorageConfig.build(ctx); - } - - //Saves the config for the world selection or something, need to figure out how to make it work with dimensional configs maybe? - // or just have per world config, cause when creating the world engine doing the string substitution would - // make it automatically select the right id - public void save() { - var file = this.selectionFolder.resolve("config.json"); - var json = Serialization.GSON.toJson(this.config); - try { - Files.writeString(file, json); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public WorldConfig getConfig() { - return this.config; - } - } - - //Gets dimension independent base world, if singleplayer, its the world name, if multiplayer, its the server ip - private static Path getBasePath() { - //TODO: improve this - Path basePath = MinecraftClient.getInstance().runDirectory.toPath().resolve(".voxy").resolve("saves"); - var iserver = MinecraftClient.getInstance().getServer(); - if (iserver != null) { - basePath = iserver.getSavePath(WorldSavePath.ROOT).resolve("voxy"); - } else { - var netHandle = MinecraftClient.getInstance().interactionManager; - if (netHandle == null) { - Logger.error("Network handle null"); - basePath = basePath.resolve("UNKNOWN"); - } else { - var info = netHandle.networkHandler.getServerInfo(); - if (info == null) { - Logger.error("Server info null"); - basePath = basePath.resolve("UNKNOWN"); - } else { - if (info.isRealm()) { - basePath = basePath.resolve("realms"); - } else { - basePath = basePath.resolve(info.address.replace(":", "_")); - } - } - } - } - return basePath; - } - - private static String bytesToHex(byte[] hash) { - StringBuilder hexString = new StringBuilder(2 * hash.length); - for (byte b : hash) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - return hexString.toString(); - } - - private static String getWorldId(World world) { - String data = world.getBiomeAccess().seed + world.getRegistryKey().toString(); - try { - return bytesToHex(MessageDigest.getInstance("SHA-256").digest(data.getBytes())).substring(0, 32); - } catch ( - NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - //The way this works is saves are segmented into base worlds, e.g. server ip, local save etc - // these are then segmented into subsaves for different worlds within the parent - public ContextSelectionSystem() { - } - - - public Selection getBestSelectionOrCreate(World world) { - var path = getBasePath(); - try { - Files.createDirectories(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - return new Selection(path, getWorldId(world)); - } -} diff --git a/src/main/java/me/cortex/voxy/common/config/Serialization.java b/src/main/java/me/cortex/voxy/common/config/Serialization.java index 529ac1dd..01552154 100644 --- a/src/main/java/me/cortex/voxy/common/config/Serialization.java +++ b/src/main/java/me/cortex/voxy/common/config/Serialization.java @@ -152,7 +152,8 @@ public class Serialization { } } - var builder = new GsonBuilder(); + var builder = new GsonBuilder() + .setPrettyPrinting(); for (var entry : serializers.entrySet()) { builder.registerTypeAdapterFactory(entry.getValue()); } diff --git a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java index c447c1b3..ee51f984 100644 --- a/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java +++ b/src/main/java/me/cortex/voxy/common/world/ActiveSectionTracker.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.Nullable; import java.lang.invoke.VarHandle; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.StampedLock; @@ -18,6 +19,7 @@ public class ActiveSectionTracker { //Loaded section world cache, TODO: get rid of VolatileHolder and use something more sane + private final AtomicInteger loadedSections = new AtomicInteger(); private final Long2ObjectOpenHashMap>[] loadedSectionCache; private final StampedLock[] locks; private final SectionLoader loader; @@ -53,6 +55,7 @@ public class ActiveSectionTracker { } public WorldSection acquire(long key, boolean nullOnEmpty) { + if (this.engine != null) this.engine.lastActiveTime = System.currentTimeMillis(); int index = this.getCacheArrayIndex(key); var cache = this.loadedSectionCache[index]; final var lock = this.locks[index]; @@ -91,6 +94,7 @@ public class ActiveSectionTracker { } if (isLoader) { + this.loadedSections.incrementAndGet(); long stamp = this.lruLock.writeLock(); section = this.lruSecondaryCache.remove(key); this.lruLock.unlockWrite(stamp); @@ -155,6 +159,7 @@ public class ActiveSectionTracker { } void tryUnload(WorldSection section) { + if (this.engine != null) this.engine.lastActiveTime = System.currentTimeMillis(); int index = this.getCacheArrayIndex(section.key); final var cache = this.loadedSectionCache[index]; WorldSection sec = null; @@ -194,6 +199,10 @@ public class ActiveSectionTracker { if (aa != null) { aa._releaseArray(); } + + if (sec != null) { + this.loadedSections.decrementAndGet(); + } } private int getCacheArrayIndex(long pos) { @@ -207,11 +216,7 @@ public class ActiveSectionTracker { } public int getLoadedCacheCount() { - int res = 0; - for (var cache : this.loadedSectionCache) { - res += cache.size(); - } - return res; + return this.loadedSections.get(); } public int getSecondaryCacheSize() { diff --git a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java index 3b29d966..4d7e37cb 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java @@ -3,12 +3,14 @@ package me.cortex.voxy.common.world; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.config.section.SectionStorage; import me.cortex.voxy.common.util.TrackedObject; -import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.world.other.Mapper; import me.cortex.voxy.commonImpl.VoxyInstance; import org.jetbrains.annotations.Nullable; +import java.lang.invoke.VarHandle; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + public class WorldEngine { public static final int MAX_LOD_LAYER = 4; @@ -40,6 +42,8 @@ public class WorldEngine { public boolean isLive() {return this.isLive;} public final @Nullable VoxyInstance instanceIn; + private final AtomicInteger refCount = new AtomicInteger(); + volatile long lastActiveTime = System.currentTimeMillis();//Time in millis the world was last "active" i.e. had a total ref count or active section count of != 0 public WorldEngine(SectionStorage storage) { this(storage, null); @@ -127,18 +131,51 @@ public class WorldEngine { return this.sectionTracker.getLoadedCacheCount(); } - public void free() { + if (!this.isLive) throw new IllegalStateException(); + this.isLive = false; + VarHandle.fullFence(); //Cannot free while there are loaded sections if (this.sectionTracker.getLoadedCacheCount() != 0) { throw new IllegalStateException(); } this.thisTracker.free(); - this.isLive = false; try {this.mapper.close();} catch (Exception e) {Logger.error(e);} try {this.storage.flush();} catch (Exception e) {Logger.error(e);} //Shutdown in this order to preserve as much data as possible try {this.storage.close();} catch (Exception e) {Logger.error(e);} } + + private static final long TIMEOUT_MILLIS = 10_000;//10 second timeout (is to long? or to short??) + public boolean isWorldUsed() { + if (!this.isLive) throw new IllegalStateException(); + return this.refCount.get() != 0 || this.sectionTracker.getLoadedCacheCount() != 0; + } + + public boolean isWorldIdle() { + if (this.isWorldUsed()) { + this.lastActiveTime = System.currentTimeMillis();//Force an update if is not active + VarHandle.fullFence(); + return false; + } + return TIMEOUT_MILLIS<(System.currentTimeMillis()-this.lastActiveTime); + } + + public void markActive() { + this.lastActiveTime = System.currentTimeMillis(); + } + + public void acquireRef() { + this.refCount.incrementAndGet(); + this.lastActiveTime = System.currentTimeMillis(); + } + + public void releaseRef() { + if (this.refCount.decrementAndGet()<0) { + throw new IllegalStateException("ref count less than 0"); + } + //TODO: maybe dont need to tick the last active time? + this.lastActiveTime = System.currentTimeMillis(); + } } diff --git a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java index 091d7f32..12e2ec15 100644 --- a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java +++ b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java @@ -8,9 +8,11 @@ import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.world.WorldUpdater; -import me.cortex.voxy.commonImpl.IVoxyWorld; +import me.cortex.voxy.commonImpl.VoxyCommon; +import me.cortex.voxy.commonImpl.WorldIdentifier; import net.minecraft.util.math.ChunkSectionPos; import net.minecraft.world.LightType; +import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.ChunkNibbleArray; import net.minecraft.world.chunk.ChunkSection; import net.minecraft.world.chunk.WorldChunk; @@ -83,17 +85,6 @@ public class VoxelIngestService { return true; } - public void enqueueIngest(WorldChunk chunk, boolean ignoreOnNullWorld) { - var engine = ((IVoxyWorld)chunk.getWorld()).getWorldEngine(); - if (engine == null) { - if (!ignoreOnNullWorld) { - Logger.error("Could not ingest chunk as does not have world engine"); - } - return; - } - this.enqueueIngest(engine, chunk); - } - public void enqueueIngest(WorldEngine engine, WorldChunk chunk) { if (!engine.isLive()) { throw new IllegalStateException("Tried inserting chunk into WorldEngine that was not alive"); @@ -137,4 +128,20 @@ public class VoxelIngestService { public void shutdown() { this.threads.shutdown(); } + + //Utility method to ingest a chunk into the given WorldIdentifier or world + public static boolean tryIngestChunk(WorldIdentifier worldId, WorldChunk chunk) { + if (worldId == null) return false; + var instance = VoxyCommon.getInstance(); + if (instance == null) return false; + var engine = instance.getOrCreate(worldId); + if (engine == null) return false; + instance.getIngestService().enqueueIngest(engine, chunk); + return true; + } + + //Try to automatically ingest the chunk into the correct world + public static boolean tryAutoIngestChunk(WorldChunk chunk) { + return tryIngestChunk(WorldIdentifier.of(chunk.getWorld()), chunk); + } } diff --git a/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java b/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java deleted file mode 100644 index 6dd70726..00000000 --- a/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.cortex.voxy.commonImpl; - -import me.cortex.voxy.common.world.WorldEngine; - -public interface IVoxyWorld { - WorldEngine getWorldEngine(); - void setWorldEngine(WorldEngine engine); - void shutdownEngine(); -} diff --git a/src/main/java/me/cortex/voxy/commonImpl/IWorldGetIdentifier.java b/src/main/java/me/cortex/voxy/commonImpl/IWorldGetIdentifier.java new file mode 100644 index 00000000..1b9bbbd6 --- /dev/null +++ b/src/main/java/me/cortex/voxy/commonImpl/IWorldGetIdentifier.java @@ -0,0 +1,11 @@ +package me.cortex.voxy.commonImpl; + +import me.cortex.voxy.common.world.WorldEngine; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; + +public interface IWorldGetIdentifier { + WorldIdentifier voxy$getIdentifier(); +} diff --git a/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java b/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java index efe7fd99..46f79768 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java +++ b/src/main/java/me/cortex/voxy/commonImpl/VoxyCommon.java @@ -41,6 +41,7 @@ public class VoxyCommon implements ModInitializer { @Override public void onInitialize() { + } public interface IInstanceFactory {VoxyInstance create();} diff --git a/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java b/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java index 496c4752..bd8f7073 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java +++ b/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java @@ -8,17 +8,23 @@ import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.service.SectionSavingService; import me.cortex.voxy.common.world.service.VoxelIngestService; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.concurrent.locks.StampedLock; +import java.util.function.BooleanSupplier; import java.util.stream.Collectors; //TODO: add thread access verification (I.E. only accessible on a single thread) -public class VoxyInstance { +public abstract class VoxyInstance { + private volatile boolean isRunning = true; + private final Thread worldCleaner; + public final BooleanSupplier savingServiceRateLimiter;//Can run if this returns true protected final ServiceThreadPool threadPool; protected final SectionSavingService savingService; protected final VoxelIngestService ingestService; - protected final Set activeWorlds = new HashSet<>(); + + private final StampedLock activeWorldLock = new StampedLock(); + private final HashMap activeWorlds = new HashMap<>(); protected final ImportManager importManager; @@ -28,35 +34,188 @@ public class VoxyInstance { this.savingService = new SectionSavingService(this.threadPool); this.ingestService = new VoxelIngestService(this.threadPool); this.importManager = this.createImportManager(); + this.savingServiceRateLimiter = ()->this.savingService.getTaskCount()<1200; + this.worldCleaner = new Thread(()->{ + try { + while (this.isRunning) { + //noinspection BusyWait + Thread.sleep(1000); + this.cleanIdle(); + } + } catch (InterruptedException e) { + //We are exiting, so just exit + } catch (Exception e) { + Logger.error("Exception in world cleaner",e); + } + }); + this.worldCleaner.setPriority(Thread.MIN_PRIORITY); + this.worldCleaner.setName("Active world cleaner"); + this.worldCleaner.setDaemon(true); + this.worldCleaner.start(); } protected ImportManager createImportManager() { return new ImportManager(); } + public ServiceThreadPool getThreadPool() { + return this.threadPool; + } + public VoxelIngestService getIngestService() { + return this.ingestService; + } + public ImportManager getImportManager() { + return this.importManager; + } + + //TODO: reference count the world object + // have automatic world cleanup after ~1 minute of inactivity and the reference count equaling zero possibly + // note, the reference count should be separate from the number of active chunks to prevent many issues + // a world is no longer active once it has no reference counts and no active chunks associated with it + public WorldEngine getNullable(WorldIdentifier identifier) { + var cache = identifier.cachedEngineObject; + WorldEngine world; + if (cache == null) { + world = null; + } else { + world = cache.get(); + if (world == null) { + identifier.cachedEngineObject = null; + } else { + if (world.isLive()) { + if (world.instanceIn != this) { + throw new IllegalStateException("World cannot be in identifier cache, alive and not part of this instance"); + } + //Successful cache hit + } else { + identifier.cachedEngineObject = null; + world = null; + } + } + } + if (world == null) {//If the cached world is null, try get from the active worlds + long stamp = this.activeWorldLock.readLock(); + world = this.activeWorlds.get(identifier); + this.activeWorldLock.unlockRead(stamp); + if (world != null) {//Setup cache + identifier.cachedEngineObject = new WeakReference<>(world); + } + } + if (world != null) { + //Mark the world as active + world.markActive(); + } + return world; + } + + public WorldEngine getOrCreate(WorldIdentifier identifier) { + var world = this.getNullable(identifier); + if (world != null) { + return world; + } + long stamp = this.activeWorldLock.writeLock(); + world = this.activeWorlds.get(identifier); + if (world == null) { + //Create world here + world = this.createWorld(identifier); + } + this.activeWorldLock.unlockWrite(stamp); + identifier.cachedEngineObject = new WeakReference<>(world); + return world; + } + + + protected abstract SectionStorage createStorage(WorldIdentifier identifier); + + private WorldEngine createWorld(WorldIdentifier identifier) { + if (this.activeWorlds.containsKey(identifier)) { + throw new IllegalStateException("Existing world with identifier"); + } + Logger.info("Creating new world engine"); + var world = new WorldEngine(this.createStorage(identifier), this); + world.setSaveCallback(this.savingService::enqueueSave); + this.activeWorlds.put(identifier, world); + return world; + } + + public void cleanIdle() { + List idleWorlds = null; + { + long stamp = this.activeWorldLock.readLock(); + for (var pair : this.activeWorlds.entrySet()) { + if (pair.getValue().isWorldIdle()) { + if (idleWorlds == null) idleWorlds = new ArrayList<>(); + idleWorlds.add(pair.getKey()); + } + } + this.activeWorldLock.unlockRead(stamp); + } + + if (idleWorlds != null) { + //Shutdown and clear all idle worlds + long stamp = this.activeWorldLock.writeLock(); + for (var id : idleWorlds) { + var world = this.activeWorlds.remove(id); + if (world == null) continue;//Race condition between unlock read and acquire write + if (!world.isWorldIdle()) {this.activeWorlds.put(id, world); continue;}//No longer idle + //If is here close and free the world + world.free(); + } + this.activeWorldLock.unlockWrite(stamp); + } + } + public void addDebug(List debug) { debug.add("Voxy Core: " + VoxyCommon.MOD_VERSION); debug.add("MemoryBuffer, Count/Size (mb): " + MemoryBuffer.getCount() + "/" + (MemoryBuffer.getTotalSize()/1_000_000)); - debug.add("I/S/AWSC: " + this.ingestService.getTaskCount() + "/" + this.savingService.getTaskCount() + "/[" + this.activeWorlds.stream().map(a->""+a.getActiveSectionCount()).collect(Collectors.joining(", ")) + "]");//Active world section count + debug.add("I/S/AWSC: " + this.ingestService.getTaskCount() + "/" + this.savingService.getTaskCount() + "/[" + this.activeWorlds.values().stream().map(a->""+a.getActiveSectionCount()).collect(Collectors.joining(", ")) + "]");//Active world section count } public void shutdown() { - Logger.info("Shutdown voxy instance"); + Logger.info("Shutting down voxy instance"); + this.isRunning = false; + try { + this.worldCleaner.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + this.cleanIdle(); if (!this.activeWorlds.isEmpty()) { - for (var world : this.activeWorlds) { + long stamp = this.activeWorldLock.readLock(); + for (var world : this.activeWorlds.values()) { this.importManager.cancelImport(world); } + this.activeWorldLock.unlockRead(stamp); } try {this.ingestService.shutdown();} catch (Exception e) {Logger.error(e);} try {this.savingService.shutdown();} catch (Exception e) {Logger.error(e);} + + long stamp = this.activeWorldLock.writeLock(); + if (!this.activeWorlds.isEmpty()) { - Logger.error("Not all worlds shutdown, force closing " + this.activeWorlds.size() + " worlds"); - for (var world : new HashSet<>(this.activeWorlds)) {//Create a clone - this.stopWorld(world); + boolean printedNotice = false; + for (var world : this.activeWorlds.values()) { + if (world.isWorldUsed()) { + if (!printedNotice) { + printedNotice = true; + Logger.error("Not all worlds shutdown, force closing worlds"); + } + while (world.isWorldUsed()) { + try { + //noinspection BusyWait + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + //Free the world + world.free(); } + this.activeWorlds.clear(); } try {this.threadPool.shutdown();} catch (Exception e) {Logger.error(e);} @@ -65,82 +224,6 @@ public class VoxyInstance { throw new IllegalStateException("Not all worlds shutdown"); } Logger.info("Instance shutdown"); - } - - public ServiceThreadPool getThreadPool() { - return this.threadPool; - } - - public VoxelIngestService getIngestService() { - return this.ingestService; - } - - public SectionSavingService getSavingService() { - return this.savingService; - } - - public ImportManager getImportManager() { - return this.importManager; - } - - public void flush() { - try { - while (this.ingestService.getTaskCount() != 0) { - Thread.sleep(10); - } - while (this.savingService.getTaskCount() != 0) { - Thread.sleep(10); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - protected WorldEngine createWorld(SectionStorage storage) { - var world = new WorldEngine(storage, this); - world.setSaveCallback(this.savingService::enqueueSave); - this.activeWorlds.add(world); - return world; - } - - //There are 4 possible "states" for world selection/management - // 1) dedicated server - // 2) client singleplayer - // 3) client singleplayer as lan host (so also a server) - // 4) client multiplayer (remote server) - - //The thing with singleplayer is that it is more efficent to make it bound to clientworld (think) - // so if make into singleplayer as host, would need to reload the system into that mode - // so that the world renderer uses the WorldEngine of the server - - public void stopWorld(WorldEngine world) { - if (!this.activeWorlds.contains(world)) { - if (world.isLive()) { - throw new IllegalStateException("World cannot be live and not in world set"); - } - throw new IllegalStateException("Cannot close world which is not part of instance"); - } - if (!world.isLive()) { - throw new IllegalStateException("World cannot be in world set and not alive"); - } - - this.importManager.cancelImport(world); - - if (world.getActiveSectionCount() != 0) { - Logger.warn("Waiting for world to finish use: " + world ); - while (world.getActiveSectionCount() != 0) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - - //TODO: maybe replace the flush with an atomic "in queue" counter that is per world - this.flush(); - - world.free(); - this.activeWorlds.remove(world); + this.activeWorldLock.unlockWrite(stamp); } } \ No newline at end of file diff --git a/src/main/java/me/cortex/voxy/commonImpl/WorldIdentifier.java b/src/main/java/me/cortex/voxy/commonImpl/WorldIdentifier.java new file mode 100644 index 00000000..c5eaa593 --- /dev/null +++ b/src/main/java/me/cortex/voxy/commonImpl/WorldIdentifier.java @@ -0,0 +1,104 @@ +package me.cortex.voxy.commonImpl; + +import me.cortex.voxy.common.world.WorldEngine; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; +import net.minecraft.world.dimension.DimensionType; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +public class WorldIdentifier { + private static final RegistryKey NULL_DIM_KEY = RegistryKey.of(RegistryKeys.DIMENSION_TYPE, Identifier.of("voxy:null_dimension_id")); + + public final RegistryKey key; + public final long biomeSeed; + public final RegistryKey dimension;//Maybe? + private final transient long hashCode; + @Nullable transient WeakReference cachedEngineObject; + + public WorldIdentifier(RegistryKey key, long biomeSeed, @Nullable RegistryKey dimension) { + dimension = dimension==null?NULL_DIM_KEY:dimension; + this.key = key; + this.biomeSeed = biomeSeed; + this.dimension = dimension; + this.hashCode = mixStafford13(key.hashCode()^biomeSeed)^mixStafford13(dimension.hashCode()^biomeSeed); + } + + @Override + public int hashCode() { + return (int) this.hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof WorldIdentifier other) { + return other.hashCode == this.hashCode && + other.biomeSeed == this.biomeSeed && + other.key == this.key &&//other.key.equals(this.key) && + other.dimension == this.dimension//other.dimension.equals(this.dimension) + ; + } + return false; + } + + //Quick access utility method to get or create a world object in the current instance + public WorldEngine getOrCreateEngine() { + var instance = VoxyCommon.getInstance(); + if (instance == null) { + this.cachedEngineObject = null; + return null; + } + var engine = instance.getOrCreate(this); + if (engine==null) { + throw new IllegalStateException("Engine null on creation"); + } + return engine; + } + + public WorldEngine getNullable() { + var instance = VoxyCommon.getInstance(); + if (instance == null) { + this.cachedEngineObject = null; + return null; + } + return instance.getNullable(this); + } + + public static WorldIdentifier of(World world) { + //Gets or makes an identifier for world + if (world == null) { + return null; + } + return ((IWorldGetIdentifier)world).voxy$getIdentifier(); + } + + //Common utility function to get or create a world engine + public static WorldEngine ofEngine(World world) { + var id = of(world); + if (id == null) { + return null; + } + return id.getOrCreateEngine(); + } + + public static WorldEngine ofEngineNullable(World world) { + var id = of(world); + if (id == null) { + return null; + } + return id.getNullable(); + } + + public static long mixStafford13(long seed) { + seed += 918759875987111L; + seed = (seed ^ seed >>> 30) * -4658895280553007687L; + seed = (seed ^ seed >>> 27) * -7723592293110705685L; + return seed ^ seed >>> 31; + } +} diff --git a/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java b/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java index bf0b7239..0cee02fe 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java +++ b/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java @@ -39,6 +39,7 @@ import java.sql.SQLException; import java.util.*; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; public class DHImporter implements IDataImporter { private final Connection db; @@ -68,7 +69,7 @@ public class DHImporter implements IDataImporter { } } - public DHImporter(File file, WorldEngine worldEngine, World mcWorld, ServiceThreadPool servicePool, SectionSavingService savingService) { + public DHImporter(File file, WorldEngine worldEngine, World mcWorld, ServiceThreadPool servicePool, BooleanSupplier rateLimiter) { this.engine = worldEngine; this.world = mcWorld; this.biomeRegistry = mcWorld.getRegistryManager().getOrThrow(RegistryKeys.BIOME); @@ -101,13 +102,14 @@ public class DHImporter implements IDataImporter { } catch (SQLException e) { throw new RuntimeException(e); } - }, ()->savingService.getTaskCount() < 500); + }, rateLimiter); } public void runImport(IUpdateCallback updateCallback, ICompletionCallback completionCallback) { if (this.isRunning()) { throw new IllegalStateException(); } + this.engine.acquireRef(); this.updateCallback = updateCallback; this.runner = new Thread(()-> { Queue taskQ = new PriorityQueue<>(Comparator.comparingLong(Task::distanceFromZero)); @@ -356,6 +358,7 @@ public class DHImporter implements IDataImporter { throw new RuntimeException(e); } this.threadPool.shutdown(); + this.engine.releaseRef(); try { this.db.close(); } catch (SQLException e) { diff --git a/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java b/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java index 8b97f81a..e1e7e839 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java +++ b/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java @@ -55,9 +55,6 @@ public class WorldImporter implements IDataImporter { private final ServiceSlice threadPool; private volatile boolean isRunning; - public WorldImporter(WorldEngine worldEngine, World mcWorld, ServiceThreadPool servicePool, SectionSavingService savingService) { - this(worldEngine, mcWorld, servicePool, ()->savingService.getTaskCount() < 4000); - } public WorldImporter(WorldEngine worldEngine, World mcWorld, ServiceThreadPool servicePool, BooleanSupplier runChecker) { this.world = worldEngine; @@ -128,6 +125,7 @@ public class WorldImporter implements IDataImporter { return; } this.isRunning = true; + this.world.acquireRef(); this.updateCallback = updateCallback; this.completionCallback = completionCallback; this.worker.start(); @@ -148,6 +146,7 @@ public class WorldImporter implements IDataImporter { } } if (!this.threadPool.isFreed()) { + this.world.releaseRef(); this.threadPool.shutdown(); } } @@ -260,6 +259,7 @@ public class WorldImporter implements IDataImporter { } } this.worker = null; + this.world.releaseRef(); this.threadPool.shutdown(); this.completionCallback.onCompletion(this.totalChunks.get()); }); diff --git a/src/main/java/me/cortex/voxy/commonImpl/mixin/chunky/MixinFabricWorld.java b/src/main/java/me/cortex/voxy/commonImpl/mixin/chunky/MixinFabricWorld.java index daaef788..1ed72c3c 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/mixin/chunky/MixinFabricWorld.java +++ b/src/main/java/me/cortex/voxy/commonImpl/mixin/chunky/MixinFabricWorld.java @@ -2,7 +2,9 @@ package me.cortex.voxy.commonImpl.mixin.chunky; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import me.cortex.voxy.common.world.service.VoxelIngestService; import me.cortex.voxy.commonImpl.VoxyCommon; +import me.cortex.voxy.commonImpl.WorldIdentifier; import net.minecraft.server.world.OptionalChunk; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.ChunkStatus; @@ -19,18 +21,13 @@ public class MixinFabricWorld { @WrapOperation(method = "getChunkAtAsync", at = @At(value = "INVOKE", target = "Lorg/popcraft/chunky/mixin/ServerChunkCacheMixin;invokeGetChunkFutureMainThread(IILnet/minecraft/world/chunk/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture;")) private CompletableFuture> captureGeneratedChunk(ServerChunkCacheMixin instance, int i, int j, ChunkStatus chunkStatus, boolean b, Operation>> original) { var future = original.call(instance, i, j, chunkStatus, b); - if (true) {//TODO: ADD SERVER CONFIG THING + if (false) {//TODO: ADD SERVER CONFIG THING return future; } else { - return future.thenApplyAsync(res -> { + return future.thenApply(res -> { res.ifPresent(chunk -> { - var voxyInstance = VoxyCommon.getInstance(); - if (voxyInstance != null) { - try { - voxyInstance.getIngestService().enqueueIngest((WorldChunk) chunk, true); - } catch (Exception e) { - - } + if (chunk instanceof WorldChunk worldChunk) { + VoxelIngestService.tryAutoIngestChunk(worldChunk); } }); return res; diff --git a/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java b/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java index 5e3fdabd..75664703 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java +++ b/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java @@ -1,36 +1,40 @@ package me.cortex.voxy.commonImpl.mixin.minecraft; -import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.commonImpl.IVoxyWorld; +import me.cortex.voxy.commonImpl.IWorldGetIdentifier; +import me.cortex.voxy.commonImpl.WorldIdentifier; +import net.minecraft.registry.DynamicRegistryManager; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.world.MutableWorldProperties; import net.minecraft.world.World; -import net.minecraft.world.block.NeighborUpdater; +import net.minecraft.world.dimension.DimensionType; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(World.class) -public class MixinWorld implements IVoxyWorld { - @Unique private WorldEngine voxyWorld; +public class MixinWorld implements IWorldGetIdentifier { + @Unique + private WorldIdentifier identifier; - @Override - public WorldEngine getWorldEngine() { - return this.voxyWorld; + @Inject(method = "", at = @At("RETURN")) + private void voxy$injectIdentifier(MutableWorldProperties properties, + RegistryKey key, + DynamicRegistryManager registryManager, + RegistryEntry dimensionEntry, + boolean isClient, + boolean debugWorld, + long seed, + int maxChainedNeighborUpdates, + CallbackInfo ci) { + this.identifier = new WorldIdentifier(key, seed, dimensionEntry.getKey().orElse(null)); } @Override - public void setWorldEngine(WorldEngine engine) { - if (engine != null && this.voxyWorld != null) { - throw new IllegalStateException("WorldEngine not null"); - } - this.voxyWorld = engine; - } - - @Override - public void shutdownEngine() { - if (this.voxyWorld != null && this.voxyWorld.instanceIn != null) { - this.voxyWorld.instanceIn.stopWorld(this.voxyWorld); - this.setWorldEngine(null); - } + public WorldIdentifier voxy$getIdentifier() { + return this.identifier; } }