This commit is contained in:
@@ -3,6 +3,8 @@ package me.cortex.voxy.common.config;
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
@@ -22,61 +24,76 @@ public class VoxyServerConfig {
|
||||
|
||||
public static VoxyServerConfig CONFIG = loadOrCreate();
|
||||
|
||||
@SerializedName(value = "view_distance", alternate = {"viewDistance"})
|
||||
public int viewDistance = 32;
|
||||
public int dirtyUpdateDelay = 20; // Ticks before sending dirty update
|
||||
@SerializedName(value = "dirty_update_delay", alternate = {"dirtyUpdateDelay"})
|
||||
public int dirtyUpdateDelay = 20;
|
||||
@SerializedName(value = "ingest_enabled", alternate = {"ingestEnabled"})
|
||||
public boolean ingestEnabled = true;
|
||||
|
||||
private static VoxyServerConfig loadOrCreate() {
|
||||
if (VoxyCommon.isAvailable()) {
|
||||
var path = getConfigPath();
|
||||
Path path = getConfigPath();
|
||||
try {
|
||||
if (Files.exists(path)) {
|
||||
try (FileReader reader = new FileReader(path.toFile())) {
|
||||
var conf = GSON.fromJson(reader, VoxyServerConfig.class);
|
||||
JsonReader jr = new JsonReader(reader);
|
||||
jr.setLenient(true);
|
||||
VoxyServerConfig conf = GSON.fromJson(jr, VoxyServerConfig.class);
|
||||
if (conf != null) {
|
||||
// conf.save(); // Don't save immediately on load, just return it. This avoids overwriting.
|
||||
Logger.info("Loaded voxy server config from " + path);
|
||||
return conf;
|
||||
} else {
|
||||
Logger.error("Failed to load voxy server config (null), resetting");
|
||||
}
|
||||
} catch (Exception e) { // Catch all exceptions including syntax errors
|
||||
Logger.error("Could not parse config, resetting to default", e);
|
||||
Logger.error("Failed to load voxy server config (null), using defaults");
|
||||
return new VoxyServerConfig();
|
||||
}
|
||||
}
|
||||
Logger.info("Server config doesnt exist or failed to load, creating new");
|
||||
var config = new VoxyServerConfig();
|
||||
config.save();
|
||||
return config;
|
||||
} else {
|
||||
Logger.info("Config not found, creating default at " + path);
|
||||
VoxyServerConfig config = new VoxyServerConfig();
|
||||
config.saveIfMissing();
|
||||
return config;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.error("Error loading voxy server config, using defaults", e);
|
||||
return new VoxyServerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
if (!VoxyCommon.isAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path path = getConfigPath();
|
||||
try {
|
||||
// Only save if file doesn't exist to prevent overwriting user changes?
|
||||
// Actually, usually save() is called to persist changes.
|
||||
// But if we call save() immediately after load(), it just re-formats.
|
||||
// The issue user reported is "resetting to default".
|
||||
// That happens if GSON.fromJson returns null or exception.
|
||||
// OR if we create new instance and overwrite.
|
||||
|
||||
// If we want to preserve comments or formatting, we shouldn't overwrite unless needed.
|
||||
// But JSON doesn't support comments.
|
||||
|
||||
// If the user says it "resets", maybe loadOrCreate is failing?
|
||||
// loadOrCreate checks if file exists.
|
||||
|
||||
// Let's modify save to be safe?
|
||||
Files.writeString(getConfigPath(), GSON.toJson(this));
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, GSON.toJson(this));
|
||||
} catch (IOException e) {
|
||||
Logger.error("Failed to write config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveIfMissing() {
|
||||
Path path = getConfigPath();
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
if (!Files.exists(path)) {
|
||||
Files.writeString(path, GSON.toJson(this));
|
||||
Logger.info("Created voxy server config at " + path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.error("Failed to write missing config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static VoxyServerConfig reload() {
|
||||
try (FileReader reader = new FileReader(getConfigPath().toFile())) {
|
||||
JsonReader jr = new JsonReader(reader);
|
||||
jr.setLenient(true);
|
||||
VoxyServerConfig conf = GSON.fromJson(jr, VoxyServerConfig.class);
|
||||
return conf != null ? conf : new VoxyServerConfig();
|
||||
} catch (Exception e) {
|
||||
Logger.error("Failed to reload config, using defaults", e);
|
||||
return new VoxyServerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private static Path getConfigPath() {
|
||||
return FabricLoader.getInstance()
|
||||
.getConfigDir()
|
||||
|
||||
@@ -98,6 +98,7 @@ public class DirtyUpdateService {
|
||||
int voxyDistance = VoxyServerConfig.CONFIG.viewDistance * 16;
|
||||
int standardViewDist = server.getPlayerList().getViewDistance() * 16;
|
||||
int m = (1 << (lvl + 1)) - 1;
|
||||
int perPlayerBudget = 25;
|
||||
for (int dx = 0; dx <= m; dx++) {
|
||||
for (int dz = 0; dz <= m; dz++) {
|
||||
double centerX = (((sx << (lvl + 1)) | dx) * sectionSize) + (sectionSize / 2.0);
|
||||
@@ -106,14 +107,19 @@ public class DirtyUpdateService {
|
||||
double dzp = player.getZ() - centerZ;
|
||||
double distSq = dxp * dxp + dzp * dzp;
|
||||
if (distSq > standardViewDist * standardViewDist && distSq < voxyDistance * voxyDistance) {
|
||||
for (int dy = 0; dy <= m; dy++) {
|
||||
int absX = (sx << (lvl + 1)) | dx;
|
||||
int absY = (sy << (lvl + 1)) | dy;
|
||||
int absZ = (sz << (lvl + 1)) | dz;
|
||||
int minAbsY = (sy << (lvl + 1));
|
||||
int maxAbsY = minAbsY + m;
|
||||
int[] yList = VisibleBandUtil.computeVisibleBands(matchLevel, dirty.world, absX, absZ);
|
||||
for (int i = 0; i < yList.length && perPlayerBudget > 0; i++) {
|
||||
int absY = yList[i];
|
||||
if (absY < minAbsY || absY > maxAbsY) continue;
|
||||
var voxelized = WorldSectionToVoxelizedConverter.convert(dirty.section, lvl, absX, absY, absZ);
|
||||
if (voxelized.lvl0NonAirCount > 0) {
|
||||
var payload = VoxyNetwork.LodUpdatePayload.create(voxelized, dirty.world.getMapper());
|
||||
ServerPlayNetworking.send(player, payload);
|
||||
perPlayerBudget--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class PlayerLodTracker {
|
||||
final RingTracker tracker;
|
||||
int lastX = Integer.MIN_VALUE;
|
||||
int lastZ = Integer.MIN_VALUE;
|
||||
final java.util.Map<Long, ColumnProgress> sentColumns = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
PlayerState(int radius, int x, int z) {
|
||||
this.tracker = new RingTracker(radius, x, z, true);
|
||||
@@ -87,7 +88,8 @@ public class PlayerLodTracker {
|
||||
// RingTracker is initialized with maxVoxyDist, so it won't generate events beyond that.
|
||||
// But we should double check here?
|
||||
|
||||
state.tracker.process(50, (x, z) -> { // Process up to 50 chunks per tick per player
|
||||
java.util.concurrent.atomic.AtomicInteger budget = new java.util.concurrent.atomic.AtomicInteger(50);
|
||||
state.tracker.process(20, (x, z) -> {
|
||||
// On Add (Load)
|
||||
double dx = x - state.lastX;
|
||||
double dz = z - state.lastZ;
|
||||
@@ -104,20 +106,36 @@ public class PlayerLodTracker {
|
||||
// 2. Check if inside Voxy Max Distance
|
||||
// This is implicitly handled by RingTracker radius, but good to be explicit.
|
||||
if (distSq > vanillaBoundary * vanillaBoundary && distSq <= maxVoxyDist * maxVoxyDist) {
|
||||
sendLod(player, engine, x, z);
|
||||
long key = (((long)x) << 32) ^ (z & 0xffffffffL);
|
||||
var prog = state.sentColumns.computeIfAbsent(key, k -> new ColumnProgress());
|
||||
if (prog.yList == null) {
|
||||
prog.yList = VisibleBandUtil.computeVisibleBands(level, engine, x, z);
|
||||
prog.nextIdx = 0;
|
||||
}
|
||||
budget.addAndGet(-processVerticalList(player, engine, x, z, prog, budget.get()));
|
||||
}
|
||||
}, (x, z) -> {
|
||||
// On Remove (Unload)
|
||||
// We don't explicit send unload packets for LODs usually, client manages its own cache/LRU?
|
||||
// Or maybe we should? Voxy client has its own tracker.
|
||||
// Sending unloads might be good to free memory.
|
||||
// But currently there is no S2C_LodUnload packet.
|
||||
// Client's RenderDistanceTracker handles unloading when it moves.
|
||||
// So we don't need to do anything here.
|
||||
long key = (((long)x) << 32) ^ (z & 0xffffffffL);
|
||||
state.sentColumns.remove(key);
|
||||
});
|
||||
if (budget.get() > 0) {
|
||||
for (var entry : state.sentColumns.entrySet()) {
|
||||
if (budget.get() <= 0) break;
|
||||
long key = entry.getKey();
|
||||
int x = (int)(key >> 32);
|
||||
int z = (int)key;
|
||||
var prog = entry.getValue();
|
||||
if (prog.yList == null) {
|
||||
prog.yList = VisibleBandUtil.computeVisibleBands(level, engine, x, z);
|
||||
prog.nextIdx = 0;
|
||||
}
|
||||
budget.addAndGet(-processVerticalList(player, engine, x, z, prog, budget.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendLod(ServerPlayer player, WorldEngine engine, int x, int z) {
|
||||
private void sendLodSection(ServerPlayer player, WorldEngine engine, int x, int z, int y) {
|
||||
// Decide LOD level based on distance?
|
||||
// For now, let's just send LOD 0 if it exists.
|
||||
// Optimization: Distant things can be higher LOD.
|
||||
@@ -155,10 +173,6 @@ public class PlayerLodTracker {
|
||||
|
||||
this.instance.getThreadPool().execute(() -> {
|
||||
try {
|
||||
int minSec = player.level().getMinSectionY();
|
||||
int maxSec = player.level().getMaxSectionY();
|
||||
|
||||
for (int y = minSec; y <= maxSec; y++) {
|
||||
var sec = engine.acquire(lvl, x >> (lvl + 1), y >> (lvl + 1), z >> (lvl + 1));
|
||||
if (sec != null) {
|
||||
try {
|
||||
@@ -173,10 +187,30 @@ public class PlayerLodTracker {
|
||||
} else {
|
||||
//Logger.warn("Failed to acquire section " + x + ", " + y + ", " + z);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.error("Error sending LOD to player", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int processVerticalList(ServerPlayer player, WorldEngine engine, int x, int z, ColumnProgress prog, int budget) {
|
||||
if (budget <= 0) return 0;
|
||||
if (prog.yList == null || prog.nextIdx >= prog.yList.length) return 0;
|
||||
int step = Math.min(budget, 8);
|
||||
int sent = 0;
|
||||
while (sent < step && prog.nextIdx < prog.yList.length) {
|
||||
int y = prog.yList[prog.nextIdx++];
|
||||
sendLodSection(player, engine, x, z, y);
|
||||
sent++;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
private static final class ColumnProgress {
|
||||
int[] yList;
|
||||
int nextIdx;
|
||||
ColumnProgress() {}
|
||||
}
|
||||
|
||||
// Visible bands now provided by VisibleBandUtil
|
||||
}
|
||||
|
||||
44
src/main/java/me/cortex/voxy/server/VisibleBandUtil.java
Normal file
44
src/main/java/me/cortex/voxy/server/VisibleBandUtil.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package me.cortex.voxy.server;
|
||||
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
final class VisibleBandUtil {
|
||||
static int[] computeVisibleBands(ServerLevel level, WorldEngine engine, int chunkX, int chunkZ) {
|
||||
int minSec = level.getMinSectionY();
|
||||
int maxSec = level.getMaxSectionY();
|
||||
int lvl = 0;
|
||||
int surfaceY = Integer.MIN_VALUE;
|
||||
for (int y = maxSec; y >= minSec; y--) {
|
||||
var sec = engine.acquire(lvl, chunkX >> (lvl + 1), y >> (lvl + 1), chunkZ >> (lvl + 1));
|
||||
if (sec != null) {
|
||||
int count = sec.getNonEmptyBlockCount();
|
||||
sec.release();
|
||||
if (count > 0) {
|
||||
surfaceY = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
java.util.ArrayList<Integer> ys = new java.util.ArrayList<>();
|
||||
if (surfaceY != Integer.MIN_VALUE) {
|
||||
for (int dy = -2; dy <= 3; dy++) {
|
||||
int yy = surfaceY + dy;
|
||||
if (yy >= minSec && yy <= maxSec) ys.add(yy);
|
||||
}
|
||||
for (int dy = 4; dy <= 6; dy++) {
|
||||
int yy = surfaceY + dy;
|
||||
if (yy >= minSec && yy <= maxSec) ys.add(yy);
|
||||
}
|
||||
for (int dy = -3; dy <= -1; dy++) {
|
||||
int yy = surfaceY + dy;
|
||||
if (yy >= minSec && yy <= maxSec) ys.add(yy);
|
||||
}
|
||||
} else {
|
||||
for (int y = minSec; y <= maxSec; y++) ys.add(y);
|
||||
}
|
||||
int[] out = new int[ys.size()];
|
||||
for (int i = 0; i < ys.size(); i++) out[i] = ys.get(i);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public class VoxyServerInstance extends VoxyInstance {
|
||||
public VoxyServerInstance(MinecraftServer server) {
|
||||
super();
|
||||
// Ensure config is loaded/created
|
||||
// VoxyServerConfig.CONFIG.save(); // Don't force save, just accessing it will load it
|
||||
VoxyServerConfig.CONFIG.saveIfMissing();
|
||||
|
||||
this.server = server;
|
||||
this.basePath = server.getWorldPath(LevelResource.ROOT).resolve("voxy");
|
||||
|
||||
Reference in New Issue
Block a user