This commit is contained in:
@@ -3,6 +3,8 @@ package me.cortex.voxy.common.config;
|
|||||||
import com.google.gson.FieldNamingPolicy;
|
import com.google.gson.FieldNamingPolicy;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
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.common.Logger;
|
||||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
@@ -22,61 +24,76 @@ public class VoxyServerConfig {
|
|||||||
|
|
||||||
public static VoxyServerConfig CONFIG = loadOrCreate();
|
public static VoxyServerConfig CONFIG = loadOrCreate();
|
||||||
|
|
||||||
|
@SerializedName(value = "view_distance", alternate = {"viewDistance"})
|
||||||
public int viewDistance = 32;
|
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;
|
public boolean ingestEnabled = true;
|
||||||
|
|
||||||
private static VoxyServerConfig loadOrCreate() {
|
private static VoxyServerConfig loadOrCreate() {
|
||||||
if (VoxyCommon.isAvailable()) {
|
Path path = getConfigPath();
|
||||||
var path = getConfigPath();
|
try {
|
||||||
if (Files.exists(path)) {
|
if (Files.exists(path)) {
|
||||||
try (FileReader reader = new FileReader(path.toFile())) {
|
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) {
|
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;
|
return conf;
|
||||||
} else {
|
} else {
|
||||||
Logger.error("Failed to load voxy server config (null), resetting");
|
Logger.error("Failed to load voxy server config (null), using defaults");
|
||||||
}
|
return new VoxyServerConfig();
|
||||||
} catch (Exception e) { // Catch all exceptions including syntax errors
|
|
||||||
Logger.error("Could not parse config, resetting to default", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.info("Server config doesnt exist or failed to load, creating new");
|
|
||||||
var config = new VoxyServerConfig();
|
|
||||||
config.save();
|
|
||||||
return config;
|
|
||||||
} else {
|
} 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();
|
return new VoxyServerConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
public void save() {
|
||||||
if (!VoxyCommon.isAvailable()) {
|
Path path = getConfigPath();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Only save if file doesn't exist to prevent overwriting user changes?
|
Files.createDirectories(path.getParent());
|
||||||
// Actually, usually save() is called to persist changes.
|
Files.writeString(path, GSON.toJson(this));
|
||||||
// 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));
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logger.error("Failed to write config file", 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() {
|
private static Path getConfigPath() {
|
||||||
return FabricLoader.getInstance()
|
return FabricLoader.getInstance()
|
||||||
.getConfigDir()
|
.getConfigDir()
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ public class DirtyUpdateService {
|
|||||||
int voxyDistance = VoxyServerConfig.CONFIG.viewDistance * 16;
|
int voxyDistance = VoxyServerConfig.CONFIG.viewDistance * 16;
|
||||||
int standardViewDist = server.getPlayerList().getViewDistance() * 16;
|
int standardViewDist = server.getPlayerList().getViewDistance() * 16;
|
||||||
int m = (1 << (lvl + 1)) - 1;
|
int m = (1 << (lvl + 1)) - 1;
|
||||||
|
int perPlayerBudget = 25;
|
||||||
for (int dx = 0; dx <= m; dx++) {
|
for (int dx = 0; dx <= m; dx++) {
|
||||||
for (int dz = 0; dz <= m; dz++) {
|
for (int dz = 0; dz <= m; dz++) {
|
||||||
double centerX = (((sx << (lvl + 1)) | dx) * sectionSize) + (sectionSize / 2.0);
|
double centerX = (((sx << (lvl + 1)) | dx) * sectionSize) + (sectionSize / 2.0);
|
||||||
@@ -106,14 +107,19 @@ public class DirtyUpdateService {
|
|||||||
double dzp = player.getZ() - centerZ;
|
double dzp = player.getZ() - centerZ;
|
||||||
double distSq = dxp * dxp + dzp * dzp;
|
double distSq = dxp * dxp + dzp * dzp;
|
||||||
if (distSq > standardViewDist * standardViewDist && distSq < voxyDistance * voxyDistance) {
|
if (distSq > standardViewDist * standardViewDist && distSq < voxyDistance * voxyDistance) {
|
||||||
for (int dy = 0; dy <= m; dy++) {
|
|
||||||
int absX = (sx << (lvl + 1)) | dx;
|
int absX = (sx << (lvl + 1)) | dx;
|
||||||
int absY = (sy << (lvl + 1)) | dy;
|
|
||||||
int absZ = (sz << (lvl + 1)) | dz;
|
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);
|
var voxelized = WorldSectionToVoxelizedConverter.convert(dirty.section, lvl, absX, absY, absZ);
|
||||||
if (voxelized.lvl0NonAirCount > 0) {
|
if (voxelized.lvl0NonAirCount > 0) {
|
||||||
var payload = VoxyNetwork.LodUpdatePayload.create(voxelized, dirty.world.getMapper());
|
var payload = VoxyNetwork.LodUpdatePayload.create(voxelized, dirty.world.getMapper());
|
||||||
ServerPlayNetworking.send(player, payload);
|
ServerPlayNetworking.send(player, payload);
|
||||||
|
perPlayerBudget--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class PlayerLodTracker {
|
|||||||
final RingTracker tracker;
|
final RingTracker tracker;
|
||||||
int lastX = Integer.MIN_VALUE;
|
int lastX = Integer.MIN_VALUE;
|
||||||
int lastZ = 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) {
|
PlayerState(int radius, int x, int z) {
|
||||||
this.tracker = new RingTracker(radius, x, z, true);
|
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.
|
// RingTracker is initialized with maxVoxyDist, so it won't generate events beyond that.
|
||||||
// But we should double check here?
|
// 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)
|
// On Add (Load)
|
||||||
double dx = x - state.lastX;
|
double dx = x - state.lastX;
|
||||||
double dz = z - state.lastZ;
|
double dz = z - state.lastZ;
|
||||||
@@ -104,20 +106,36 @@ public class PlayerLodTracker {
|
|||||||
// 2. Check if inside Voxy Max Distance
|
// 2. Check if inside Voxy Max Distance
|
||||||
// This is implicitly handled by RingTracker radius, but good to be explicit.
|
// This is implicitly handled by RingTracker radius, but good to be explicit.
|
||||||
if (distSq > vanillaBoundary * vanillaBoundary && distSq <= maxVoxyDist * maxVoxyDist) {
|
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) -> {
|
}, (x, z) -> {
|
||||||
// On Remove (Unload)
|
// On Remove (Unload)
|
||||||
// We don't explicit send unload packets for LODs usually, client manages its own cache/LRU?
|
long key = (((long)x) << 32) ^ (z & 0xffffffffL);
|
||||||
// Or maybe we should? Voxy client has its own tracker.
|
state.sentColumns.remove(key);
|
||||||
// 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.
|
|
||||||
});
|
});
|
||||||
|
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?
|
// Decide LOD level based on distance?
|
||||||
// For now, let's just send LOD 0 if it exists.
|
// For now, let's just send LOD 0 if it exists.
|
||||||
// Optimization: Distant things can be higher LOD.
|
// Optimization: Distant things can be higher LOD.
|
||||||
@@ -155,10 +173,6 @@ public class PlayerLodTracker {
|
|||||||
|
|
||||||
this.instance.getThreadPool().execute(() -> {
|
this.instance.getThreadPool().execute(() -> {
|
||||||
try {
|
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));
|
var sec = engine.acquire(lvl, x >> (lvl + 1), y >> (lvl + 1), z >> (lvl + 1));
|
||||||
if (sec != null) {
|
if (sec != null) {
|
||||||
try {
|
try {
|
||||||
@@ -173,10 +187,30 @@ public class PlayerLodTracker {
|
|||||||
} else {
|
} else {
|
||||||
//Logger.warn("Failed to acquire section " + x + ", " + y + ", " + z);
|
//Logger.warn("Failed to acquire section " + x + ", " + y + ", " + z);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.error("Error sending LOD to player", 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) {
|
public VoxyServerInstance(MinecraftServer server) {
|
||||||
super();
|
super();
|
||||||
// Ensure config is loaded/created
|
// 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.server = server;
|
||||||
this.basePath = server.getWorldPath(LevelResource.ROOT).resolve("voxy");
|
this.basePath = server.getWorldPath(LevelResource.ROOT).resolve("voxy");
|
||||||
|
|||||||
Reference in New Issue
Block a user