Refactored voxy frontend to use "WorldIdentifier" system

This commit is contained in:
mcrcortex
2025-05-29 22:03:39 +10:00
parent ea930ad917
commit fe2e6522ed
20 changed files with 565 additions and 428 deletions

View File

@@ -1,27 +1,35 @@
package me.cortex.voxy.client; package me.cortex.voxy.client;
import me.cortex.voxy.client.config.VoxyConfig; 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.Logger;
import me.cortex.voxy.common.util.Pair; import me.cortex.voxy.common.config.ConfigBuildCtx;
import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.config.Serialization;
import me.cortex.voxy.commonImpl.IVoxyWorld; 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.ImportManager;
import me.cortex.voxy.commonImpl.VoxyInstance; import me.cortex.voxy.commonImpl.VoxyInstance;
import me.cortex.voxy.commonImpl.WorldIdentifier;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.world.ClientWorld; import net.minecraft.util.WorldSavePath;
import net.minecraft.world.World;
import java.util.Random; import java.io.IOException;
import java.util.concurrent.ConcurrentLinkedDeque; import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class VoxyClientInstance extends VoxyInstance { public class VoxyClientInstance extends VoxyInstance {
public static boolean isInGame = false; public static boolean isInGame = false;
private static final ContextSelectionSystem SELECTOR = new ContextSelectionSystem(); private final SectionStorageConfig storageConfig;
private final Path basePath = getBasePath();
public VoxyClientInstance() { public VoxyClientInstance() {
super(VoxyConfig.CONFIG.serviceThreads); super(VoxyConfig.CONFIG.serviceThreads);
this.storageConfig = getCreateStorageConfig(this.basePath);
} }
@Override @Override
@@ -29,37 +37,123 @@ public class VoxyClientInstance extends VoxyInstance {
return new ClientImportManager(); 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 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(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);
}
}
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;
}
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 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;
}
}*/
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");
}
}
return vworld;
}
public WorldEngine getOrMakeRenderWorld(World world) {
return this.getOrCreateEngine(world);
}
private static void testDbPerformance(WorldEngine engine) { private static void testDbPerformance(WorldEngine engine) {
Random r = new Random(123456); Random r = new Random(123456);
r.nextLong(); r.nextLong();
@@ -149,4 +243,5 @@ public class VoxyClientInstance extends VoxyInstance {
} }
} }
} }
*/
} }

View File

@@ -6,8 +6,9 @@ import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import me.cortex.voxy.client.core.IGetVoxyRenderSystem; 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.VoxyCommon;
import me.cortex.voxy.commonImpl.WorldIdentifier;
import me.cortex.voxy.commonImpl.importers.DHImporter; import me.cortex.voxy.commonImpl.importers.DHImporter;
import me.cortex.voxy.commonImpl.importers.WorldImporter; import me.cortex.voxy.commonImpl.importers.WorldImporter;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
@@ -67,8 +68,6 @@ public class VoxyCommands {
if (wr!=null) { if (wr!=null) {
((IGetVoxyRenderSystem)wr).shutdownRenderer(); ((IGetVoxyRenderSystem)wr).shutdownRenderer();
} }
var w = ((IVoxyWorld)MinecraftClient.getInstance().world);
if (w != null) w.shutdownEngine();
VoxyCommon.shutdownInstance(); VoxyCommon.shutdownInstance();
VoxyCommon.createInstance(); VoxyCommon.createInstance();
@@ -98,9 +97,10 @@ public class VoxyCommands {
} }
File dbFile_ = dbFile; 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, ()-> 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) { private static boolean fileBasedImporter(File directory) {
@@ -108,9 +108,11 @@ public class VoxyCommands {
if (instance == null) { if (instance == null) {
return false; 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, ()->{ 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); importer.importRegionDirectoryAsync(directory);
return importer; return importer;
}); });
@@ -200,12 +202,15 @@ public class VoxyCommands {
} }
String finalInnerDir = innerDir; String finalInnerDir = innerDir;
var engine = instance.getOrMakeRenderWorld(MinecraftClient.getInstance().player.clientWorld); var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().player.clientWorld);
return instance.getImportManager().makeAndRunIfNone(engine, ()->{ if (engine != null) {
var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.getSavingService()); return instance.getImportManager().makeAndRunIfNone(engine, () -> {
var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.savingServiceRateLimiter);
importer.importZippedRegionDirectoryAsync(zip, finalInnerDir); importer.importZippedRegionDirectoryAsync(zip, finalInnerDir);
return importer; return importer;
})?0:1; }) ? 0 : 1;
}
return 1;
} }
private static int cancelImport(CommandContext<FabricClientCommandSource> fabricClientCommandSourceCommandContext) { private static int cancelImport(CommandContext<FabricClientCommandSource> fabricClientCommandSourceCommandContext) {
@@ -213,7 +218,10 @@ public class VoxyCommands {
if (instance == null) { if (instance == null) {
return 1; return 1;
} }
var world = instance.getOrMakeRenderWorld(MinecraftClient.getInstance().player.clientWorld); var world = WorldIdentifier.ofEngineNullable(MinecraftClient.getInstance().player.clientWorld);
if (world != null) {
return instance.getImportManager().cancelImport(world)?0:1; return instance.getImportManager().cancelImport(world)?0:1;
} }
return 1;
}
} }

View File

@@ -1,16 +1,11 @@
package me.cortex.voxy.client.config; package me.cortex.voxy.client.config;
import com.google.common.collect.ImmutableList; 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.RenderStatistics;
import me.cortex.voxy.client.VoxyClientInstance; import me.cortex.voxy.client.VoxyClientInstance;
import me.cortex.voxy.client.core.IGetVoxyRenderSystem; 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.common.util.cpu.CpuLayout;
import me.cortex.voxy.commonImpl.IVoxyWorld;
import me.cortex.voxy.commonImpl.VoxyCommon; 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.*;
import net.caffeinemc.mods.sodium.client.gui.options.control.SliderControl; import net.caffeinemc.mods.sodium.client.gui.options.control.SliderControl;
import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl; import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl;
@@ -50,10 +45,6 @@ public abstract class VoxyConfigScreenPages {
if (vrsh != null) { if (vrsh != null) {
vrsh.shutdownRenderer(); vrsh.shutdownRenderer();
} }
var world = (IVoxyWorld) MinecraftClient.getInstance().world;
if (world != null) {
world.shutdownEngine();
}
VoxyCommon.shutdownInstance(); VoxyCommon.shutdownInstance();
} }
}, s -> s.enabled) }, s -> s.enabled)
@@ -72,11 +63,6 @@ public abstract class VoxyConfigScreenPages {
if (vrsh != null) { if (vrsh != null) {
vrsh.shutdownRenderer(); vrsh.shutdownRenderer();
} }
var world = (IVoxyWorld) MinecraftClient.getInstance().world;
if (world != null) {
world.shutdownEngine();
}
VoxyCommon.shutdownInstance(); VoxyCommon.shutdownInstance();
} }

View File

@@ -42,9 +42,13 @@ import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger; 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.GL_VIEWPORT;
import static org.lwjgl.opengl.GL11.glGetIntegerv; import static org.lwjgl.opengl.GL11.glGetIntegerv;
import static org.lwjgl.opengl.GL11C.*; 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.GL_DRAW_FRAMEBUFFER_BINDING;
import static org.lwjgl.opengl.GL30C.glBindFramebuffer; import static org.lwjgl.opengl.GL30C.glBindFramebuffer;
import static org.lwjgl.opengl.GL33.glBindSampler; import static org.lwjgl.opengl.GL33.glBindSampler;
@@ -82,6 +86,9 @@ public class VoxyRenderSystem {
this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance); this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance);
this.chunkBoundRenderer = new ChunkBoundRenderer(); this.chunkBoundRenderer = new ChunkBoundRenderer();
//Keep the world loaded
this.worldIn.acquireRef();
} }
public void setRenderDistance(int renderDistance) { 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);} try {this.renderer.shutdown();this.chunkBoundRenderer.free();} catch (Exception e) {Logger.error("Error shutting down renderer", e);}
Logger.info("Shutting down post processor"); 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);}} 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();
} }

View File

@@ -6,8 +6,8 @@ import me.cortex.voxy.client.core.IGetVoxyRenderSystem;
import me.cortex.voxy.client.core.VoxyRenderSystem; import me.cortex.voxy.client.core.VoxyRenderSystem;
import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.commonImpl.IVoxyWorld;
import me.cortex.voxy.commonImpl.VoxyCommon; import me.cortex.voxy.commonImpl.VoxyCommon;
import me.cortex.voxy.commonImpl.WorldIdentifier;
import net.minecraft.client.render.*; import net.minecraft.client.render.*;
import net.minecraft.client.util.ObjectAllocator; import net.minecraft.client.util.ObjectAllocator;
import net.minecraft.client.world.ClientWorld; import net.minecraft.client.world.ClientWorld;
@@ -50,10 +50,6 @@ public abstract class MixinWorldRenderer implements IGetVoxyRenderSystem {
private void voxy$captureSetWorld(ClientWorld world, CallbackInfo ci) { private void voxy$captureSetWorld(ClientWorld world, CallbackInfo ci) {
if (this.world != world) { if (this.world != world) {
this.shutdownRenderer(); 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"); Logger.error("Not creating renderer due to null instance");
return; return;
} }
WorldEngine world = instance.getOrMakeRenderWorld(this.world); WorldEngine world = WorldIdentifier.ofEngine(this.world);
if (world == null) { if (world == null) {
Logger.error("Null world selected"); Logger.error("Null world selected");
return; return;

View File

@@ -5,7 +5,10 @@ import me.cortex.voxy.client.VoxyClientInstance;
import me.cortex.voxy.client.config.VoxyConfig; import me.cortex.voxy.client.config.VoxyConfig;
import me.cortex.voxy.client.core.IGetVoxyRenderSystem; import me.cortex.voxy.client.core.IGetVoxyRenderSystem;
import me.cortex.voxy.client.core.VoxyRenderSystem; 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.VoxyCommon;
import me.cortex.voxy.commonImpl.WorldIdentifier;
import net.caffeinemc.mods.sodium.client.gl.device.CommandList; 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.RenderSection;
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags;
@@ -61,16 +64,8 @@ public class MixinRenderSectionManager {
@Inject(method = "onChunkRemoved", at = @At("HEAD")) @Inject(method = "onChunkRemoved", at = @At("HEAD"))
private void injectIngest(int x, int z, CallbackInfo ci) { private void injectIngest(int x, int z, CallbackInfo ci) {
//TODO: Am not quite sure if this is right //TODO: Am not quite sure if this is right
var instance = VoxyCommon.getInstance(); if (VoxyConfig.CONFIG.ingestEnabled) {
if (instance != null && VoxyConfig.CONFIG.ingestEnabled) { VoxelIngestService.tryAutoIngestChunk(this.level.getChunk(x, z));
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);
}
}
} }
} }
@@ -100,13 +95,4 @@ public class MixinRenderSectionManager {
} }
return true; 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;
}*/
} }

View File

@@ -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));
}
}

View File

@@ -152,7 +152,8 @@ public class Serialization {
} }
} }
var builder = new GsonBuilder(); var builder = new GsonBuilder()
.setPrettyPrinting();
for (var entry : serializers.entrySet()) { for (var entry : serializers.entrySet()) {
builder.registerTypeAdapterFactory(entry.getValue()); builder.registerTypeAdapterFactory(entry.getValue());
} }

View File

@@ -8,6 +8,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.StampedLock; 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 //Loaded section world cache, TODO: get rid of VolatileHolder and use something more sane
private final AtomicInteger loadedSections = new AtomicInteger();
private final Long2ObjectOpenHashMap<VolatileHolder<WorldSection>>[] loadedSectionCache; private final Long2ObjectOpenHashMap<VolatileHolder<WorldSection>>[] loadedSectionCache;
private final StampedLock[] locks; private final StampedLock[] locks;
private final SectionLoader loader; private final SectionLoader loader;
@@ -53,6 +55,7 @@ public class ActiveSectionTracker {
} }
public WorldSection acquire(long key, boolean nullOnEmpty) { public WorldSection acquire(long key, boolean nullOnEmpty) {
if (this.engine != null) this.engine.lastActiveTime = System.currentTimeMillis();
int index = this.getCacheArrayIndex(key); int index = this.getCacheArrayIndex(key);
var cache = this.loadedSectionCache[index]; var cache = this.loadedSectionCache[index];
final var lock = this.locks[index]; final var lock = this.locks[index];
@@ -91,6 +94,7 @@ public class ActiveSectionTracker {
} }
if (isLoader) { if (isLoader) {
this.loadedSections.incrementAndGet();
long stamp = this.lruLock.writeLock(); long stamp = this.lruLock.writeLock();
section = this.lruSecondaryCache.remove(key); section = this.lruSecondaryCache.remove(key);
this.lruLock.unlockWrite(stamp); this.lruLock.unlockWrite(stamp);
@@ -155,6 +159,7 @@ public class ActiveSectionTracker {
} }
void tryUnload(WorldSection section) { void tryUnload(WorldSection section) {
if (this.engine != null) this.engine.lastActiveTime = System.currentTimeMillis();
int index = this.getCacheArrayIndex(section.key); int index = this.getCacheArrayIndex(section.key);
final var cache = this.loadedSectionCache[index]; final var cache = this.loadedSectionCache[index];
WorldSection sec = null; WorldSection sec = null;
@@ -194,6 +199,10 @@ public class ActiveSectionTracker {
if (aa != null) { if (aa != null) {
aa._releaseArray(); aa._releaseArray();
} }
if (sec != null) {
this.loadedSections.decrementAndGet();
}
} }
private int getCacheArrayIndex(long pos) { private int getCacheArrayIndex(long pos) {
@@ -207,11 +216,7 @@ public class ActiveSectionTracker {
} }
public int getLoadedCacheCount() { public int getLoadedCacheCount() {
int res = 0; return this.loadedSections.get();
for (var cache : this.loadedSectionCache) {
res += cache.size();
}
return res;
} }
public int getSecondaryCacheSize() { public int getSecondaryCacheSize() {

View File

@@ -3,12 +3,14 @@ package me.cortex.voxy.common.world;
import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.config.section.SectionStorage; import me.cortex.voxy.common.config.section.SectionStorage;
import me.cortex.voxy.common.util.TrackedObject; 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.common.world.other.Mapper;
import me.cortex.voxy.commonImpl.VoxyInstance; import me.cortex.voxy.commonImpl.VoxyInstance;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.invoke.VarHandle;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class WorldEngine { public class WorldEngine {
public static final int MAX_LOD_LAYER = 4; public static final int MAX_LOD_LAYER = 4;
@@ -40,6 +42,8 @@ public class WorldEngine {
public boolean isLive() {return this.isLive;} public boolean isLive() {return this.isLive;}
public final @Nullable VoxyInstance instanceIn; 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) { public WorldEngine(SectionStorage storage) {
this(storage, null); this(storage, null);
@@ -127,18 +131,51 @@ public class WorldEngine {
return this.sectionTracker.getLoadedCacheCount(); return this.sectionTracker.getLoadedCacheCount();
} }
public void free() { public void free() {
if (!this.isLive) throw new IllegalStateException();
this.isLive = false;
VarHandle.fullFence();
//Cannot free while there are loaded sections //Cannot free while there are loaded sections
if (this.sectionTracker.getLoadedCacheCount() != 0) { if (this.sectionTracker.getLoadedCacheCount() != 0) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
this.thisTracker.free(); this.thisTracker.free();
this.isLive = false;
try {this.mapper.close();} catch (Exception e) {Logger.error(e);} try {this.mapper.close();} catch (Exception e) {Logger.error(e);}
try {this.storage.flush();} 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 //Shutdown in this order to preserve as much data as possible
try {this.storage.close();} catch (Exception e) {Logger.error(e);} 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();
}
} }

View File

@@ -8,9 +8,11 @@ import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceSlice;
import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.thread.ServiceThreadPool;
import me.cortex.voxy.common.world.WorldUpdater; 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.util.math.ChunkSectionPos;
import net.minecraft.world.LightType; import net.minecraft.world.LightType;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkNibbleArray; import net.minecraft.world.chunk.ChunkNibbleArray;
import net.minecraft.world.chunk.ChunkSection; import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.chunk.WorldChunk; import net.minecraft.world.chunk.WorldChunk;
@@ -83,17 +85,6 @@ public class VoxelIngestService {
return true; 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) { public void enqueueIngest(WorldEngine engine, WorldChunk chunk) {
if (!engine.isLive()) { if (!engine.isLive()) {
throw new IllegalStateException("Tried inserting chunk into WorldEngine that was not alive"); throw new IllegalStateException("Tried inserting chunk into WorldEngine that was not alive");
@@ -137,4 +128,20 @@ public class VoxelIngestService {
public void shutdown() { public void shutdown() {
this.threads.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);
}
} }

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -41,6 +41,7 @@ public class VoxyCommon implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
} }
public interface IInstanceFactory {VoxyInstance create();} public interface IInstanceFactory {VoxyInstance create();}

View File

@@ -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.SectionSavingService;
import me.cortex.voxy.common.world.service.VoxelIngestService; import me.cortex.voxy.common.world.service.VoxelIngestService;
import java.util.HashSet; import java.lang.ref.WeakReference;
import java.util.List; import java.util.*;
import java.util.Set; import java.util.concurrent.locks.StampedLock;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
//TODO: add thread access verification (I.E. only accessible on a single thread) //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 ServiceThreadPool threadPool;
protected final SectionSavingService savingService; protected final SectionSavingService savingService;
protected final VoxelIngestService ingestService; protected final VoxelIngestService ingestService;
protected final Set<WorldEngine> activeWorlds = new HashSet<>();
private final StampedLock activeWorldLock = new StampedLock();
private final HashMap<WorldIdentifier, WorldEngine> activeWorlds = new HashMap<>();
protected final ImportManager importManager; protected final ImportManager importManager;
@@ -28,35 +34,188 @@ public class VoxyInstance {
this.savingService = new SectionSavingService(this.threadPool); this.savingService = new SectionSavingService(this.threadPool);
this.ingestService = new VoxelIngestService(this.threadPool); this.ingestService = new VoxelIngestService(this.threadPool);
this.importManager = this.createImportManager(); 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() { protected ImportManager createImportManager() {
return new ImportManager(); 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<WorldIdentifier> 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<String> debug) { public void addDebug(List<String> debug) {
debug.add("Voxy Core: " + VoxyCommon.MOD_VERSION); debug.add("Voxy Core: " + VoxyCommon.MOD_VERSION);
debug.add("MemoryBuffer, Count/Size (mb): " + MemoryBuffer.getCount() + "/" + (MemoryBuffer.getTotalSize()/1_000_000)); 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() { 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()) { 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.importManager.cancelImport(world);
} }
this.activeWorldLock.unlockRead(stamp);
} }
try {this.ingestService.shutdown();} catch (Exception e) {Logger.error(e);} try {this.ingestService.shutdown();} catch (Exception e) {Logger.error(e);}
try {this.savingService.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()) { if (!this.activeWorlds.isEmpty()) {
Logger.error("Not all worlds shutdown, force closing " + this.activeWorlds.size() + " worlds"); boolean printedNotice = false;
for (var world : new HashSet<>(this.activeWorlds)) {//Create a clone for (var world : this.activeWorlds.values()) {
this.stopWorld(world); 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);} try {this.threadPool.shutdown();} catch (Exception e) {Logger.error(e);}
@@ -65,82 +224,6 @@ public class VoxyInstance {
throw new IllegalStateException("Not all worlds shutdown"); throw new IllegalStateException("Not all worlds shutdown");
} }
Logger.info("Instance shutdown"); Logger.info("Instance shutdown");
} this.activeWorldLock.unlockWrite(stamp);
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);
} }
} }

View File

@@ -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<DimensionType> NULL_DIM_KEY = RegistryKey.of(RegistryKeys.DIMENSION_TYPE, Identifier.of("voxy:null_dimension_id"));
public final RegistryKey<World> key;
public final long biomeSeed;
public final RegistryKey<DimensionType> dimension;//Maybe?
private final transient long hashCode;
@Nullable transient WeakReference<WorldEngine> cachedEngineObject;
public WorldIdentifier(RegistryKey<World> key, long biomeSeed, @Nullable RegistryKey<DimensionType> 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;
}
}

View File

@@ -39,6 +39,7 @@ import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
public class DHImporter implements IDataImporter { public class DHImporter implements IDataImporter {
private final Connection db; 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.engine = worldEngine;
this.world = mcWorld; this.world = mcWorld;
this.biomeRegistry = mcWorld.getRegistryManager().getOrThrow(RegistryKeys.BIOME); this.biomeRegistry = mcWorld.getRegistryManager().getOrThrow(RegistryKeys.BIOME);
@@ -101,13 +102,14 @@ public class DHImporter implements IDataImporter {
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}, ()->savingService.getTaskCount() < 500); }, rateLimiter);
} }
public void runImport(IUpdateCallback updateCallback, ICompletionCallback completionCallback) { public void runImport(IUpdateCallback updateCallback, ICompletionCallback completionCallback) {
if (this.isRunning()) { if (this.isRunning()) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
this.engine.acquireRef();
this.updateCallback = updateCallback; this.updateCallback = updateCallback;
this.runner = new Thread(()-> { this.runner = new Thread(()-> {
Queue<Task> taskQ = new PriorityQueue<>(Comparator.comparingLong(Task::distanceFromZero)); Queue<Task> taskQ = new PriorityQueue<>(Comparator.comparingLong(Task::distanceFromZero));
@@ -356,6 +358,7 @@ public class DHImporter implements IDataImporter {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
this.threadPool.shutdown(); this.threadPool.shutdown();
this.engine.releaseRef();
try { try {
this.db.close(); this.db.close();
} catch (SQLException e) { } catch (SQLException e) {

View File

@@ -55,9 +55,6 @@ public class WorldImporter implements IDataImporter {
private final ServiceSlice threadPool; private final ServiceSlice threadPool;
private volatile boolean isRunning; 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) { public WorldImporter(WorldEngine worldEngine, World mcWorld, ServiceThreadPool servicePool, BooleanSupplier runChecker) {
this.world = worldEngine; this.world = worldEngine;
@@ -128,6 +125,7 @@ public class WorldImporter implements IDataImporter {
return; return;
} }
this.isRunning = true; this.isRunning = true;
this.world.acquireRef();
this.updateCallback = updateCallback; this.updateCallback = updateCallback;
this.completionCallback = completionCallback; this.completionCallback = completionCallback;
this.worker.start(); this.worker.start();
@@ -148,6 +146,7 @@ public class WorldImporter implements IDataImporter {
} }
} }
if (!this.threadPool.isFreed()) { if (!this.threadPool.isFreed()) {
this.world.releaseRef();
this.threadPool.shutdown(); this.threadPool.shutdown();
} }
} }
@@ -260,6 +259,7 @@ public class WorldImporter implements IDataImporter {
} }
} }
this.worker = null; this.worker = null;
this.world.releaseRef();
this.threadPool.shutdown(); this.threadPool.shutdown();
this.completionCallback.onCompletion(this.totalChunks.get()); this.completionCallback.onCompletion(this.totalChunks.get());
}); });

View File

@@ -2,7 +2,9 @@ package me.cortex.voxy.commonImpl.mixin.chunky;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 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.VoxyCommon;
import me.cortex.voxy.commonImpl.WorldIdentifier;
import net.minecraft.server.world.OptionalChunk; import net.minecraft.server.world.OptionalChunk;
import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkStatus; 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;")) @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<OptionalChunk<Chunk>> captureGeneratedChunk(ServerChunkCacheMixin instance, int i, int j, ChunkStatus chunkStatus, boolean b, Operation<CompletableFuture<OptionalChunk<Chunk>>> original) { private CompletableFuture<OptionalChunk<Chunk>> captureGeneratedChunk(ServerChunkCacheMixin instance, int i, int j, ChunkStatus chunkStatus, boolean b, Operation<CompletableFuture<OptionalChunk<Chunk>>> original) {
var future = original.call(instance, i, j, chunkStatus, b); 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; return future;
} else { } else {
return future.thenApplyAsync(res -> { return future.thenApply(res -> {
res.ifPresent(chunk -> { res.ifPresent(chunk -> {
var voxyInstance = VoxyCommon.getInstance(); if (chunk instanceof WorldChunk worldChunk) {
if (voxyInstance != null) { VoxelIngestService.tryAutoIngestChunk(worldChunk);
try {
voxyInstance.getIngestService().enqueueIngest((WorldChunk) chunk, true);
} catch (Exception e) {
}
} }
}); });
return res; return res;

View File

@@ -1,36 +1,40 @@
package me.cortex.voxy.commonImpl.mixin.minecraft; package me.cortex.voxy.commonImpl.mixin.minecraft;
import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.commonImpl.IWorldGetIdentifier;
import me.cortex.voxy.commonImpl.IVoxyWorld; 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.World;
import net.minecraft.world.block.NeighborUpdater; import net.minecraft.world.dimension.DimensionType;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique; 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) @Mixin(World.class)
public class MixinWorld implements IVoxyWorld { public class MixinWorld implements IWorldGetIdentifier {
@Unique private WorldEngine voxyWorld; @Unique
private WorldIdentifier identifier;
@Override @Inject(method = "<init>", at = @At("RETURN"))
public WorldEngine getWorldEngine() { private void voxy$injectIdentifier(MutableWorldProperties properties,
return this.voxyWorld; RegistryKey<World> key,
DynamicRegistryManager registryManager,
RegistryEntry<DimensionType> dimensionEntry,
boolean isClient,
boolean debugWorld,
long seed,
int maxChainedNeighborUpdates,
CallbackInfo ci) {
this.identifier = new WorldIdentifier(key, seed, dimensionEntry.getKey().orElse(null));
} }
@Override @Override
public void setWorldEngine(WorldEngine engine) { public WorldIdentifier voxy$getIdentifier() {
if (engine != null && this.voxyWorld != null) { return this.identifier;
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);
}
} }
} }