Compare commits

..
28 Commits
Author SHA1 Message Date
Jim C K Flaten 0004539b41 Merge branch 'release/0.14.0' 2026-07-09 17:57:27 +02:00
Jim C K Flaten 9ef0f09dd2 Release 0.14.0. 2026-07-09 17:57:22 +02:00
Jim C K Flaten 79cb5aa302 Per-world material cache. 2026-07-03 17:05:48 +02:00
Jim C K Flaten 5e9760c3d1 Clean up discard logic. 2026-07-03 17:00:16 +02:00
Jim C K Flaten 637636b5e8 Pre-size ArrayLists. 2026-07-02 23:07:21 +02:00
Jim C K Flaten 0ffe620f92 More buffer defaults. 2026-07-02 22:24:35 +02:00
Jim C K Flaten 2bcf9c7ca9 Reduce buffer copies during message transmit. 2026-07-02 21:56:52 +02:00
Jim C K Flaten 222221f9dc Add missing copyright notice. 2026-07-02 16:54:50 +02:00
Jim C K Flaten d086458546 Ignore blocks outside section boundary. Assume okay. 2026-07-02 16:41:57 +02:00
Jim C K Flaten 12b1580535 Allocation reductions. 2026-07-02 16:17:35 +02:00
Matt Lavallee 81326b415d Optimize LOD compression by caching generation step and world compression type
Caches the compressed generation-step and world-compression-type blocks (which are identical constant bytes for every LOD) in static volatile fields so the LZMA2 encoder runs once for them instead of on every LOD, and it sizes the LZMA2 dictionary to the actual payload (clamped between DICT_SIZE_MIN and 1 MiB) rather than using the multi-MiB preset default, cutting per-call match-finder allocation by orders of magnitude with identical compressed results.
2026-06-27 01:16:54 +02:00
Jim C K Flaten 328684cf1d Make sure block state properties are ordered. 2026-06-24 22:56:47 +02:00
Jim C K Flaten d89cfbfec5 Reject with empty LOD. 2026-06-23 21:58:48 +02:00
Jim C K Flaten 413e2a6e70 Use the correct variables to identify chunks for unloading. 2026-06-22 12:18:43 +02:00
Jim C K Flaten ee385ea5af Iterate worlds to find by key. Fixes exception on level init requests under Spigot. 2026-06-22 00:50:18 +02:00
Jim C K Flaten 37db471c4f Improve game version detection. 2026-06-22 00:17:04 +02:00
Jim C K Flaten 3e9165b7a3 Allow real-time updates even if generation is disabled. 2026-06-21 19:40:55 +02:00
Jim C K Flaten da893ec1f0 Protocol v15 2026-06-07 00:23:06 +02:00
Jim C K Flaten 320c04aeb7 Use the requested world instead of current world. 2026-06-05 00:01:11 +02:00
Jim C K Flaten fb8f98c52f Support for DH's (currently broken) protocol v14. 2026-06-02 19:48:16 +02:00
Jim C K Flaten 99cee46ae5 Bump version numer 2026-05-10 13:51:14 +02:00
Jim C K Flaten a05ee01b8b Merge tag '0.13.1' into develop
0.13.1
2026-05-10 13:50:11 +02:00
Jim C K Flaten b6878aaaaf Merge branch 'release/0.13.1' 2026-05-10 13:50:03 +02:00
Jim C K Flaten 0fbc612eef Release 0.13.1 2026-05-10 13:49:19 +02:00
Jim C K Flaten da2dba88b1 Disable async chunk access for all platforms. 2026-05-10 13:46:08 +02:00
Jim C K Flaten 35822cb8a7 Bukkit has getVersion(), while getMinecraftVersion() is Paper only. 2026-05-10 13:16:09 +02:00
Jim C K Flaten 9625035edd Bump version number. 2026-04-19 01:31:19 +02:00
Jim C K Flaten ecda68771c Merge tag '0.13.0' into develop
0.13.0
2026-04-19 01:29:42 +02:00
26 changed files with 531 additions and 153 deletions

No files matched your search

+2 -2
View File
@@ -4,7 +4,7 @@ plugins {
}
group = 'no.jckf.dhs'
version = '0.13.0'
version = '0.14.0'
description = 'Distant Horizons Support'
java {
@@ -104,4 +104,4 @@ paperVersions.each { mcVersion ->
}
build.dependsOn(taskName)
}
}
@@ -71,7 +71,7 @@ public class BukkitScheduler implements Scheduler
@Override
public boolean canReadWorldAsync()
{
return !this.foliaLib.isFolia();
return false;
}
@Override
@@ -24,9 +24,9 @@ import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.configuration.WorldConfiguration;
import no.jckf.dhsupport.core.scheduling.Scheduler;
import no.jckf.dhsupport.core.world.MaterialRecord;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.*;
import org.bukkit.block.Beacon;
import javax.annotation.Nullable;
import java.io.File;
@@ -58,7 +58,14 @@ public class BukkitWorldInterface implements WorldInterface
protected boolean trustHeightMap = true;
protected Map<String, ChunkSnapshot> chunks = new HashMap<>();
private Integer sectionX;
private Integer sectionZ;
private final ChunkSnapshot[] chunks = new ChunkSnapshot[16];
protected EnumMap<Material, MaterialRecord> materialCache = new EnumMap<>(Material.class);
protected Map<String, Map<String, String>> propertyCache = new HashMap<>();
protected UnsafeValues unsafeValues;
@@ -147,13 +154,26 @@ public class BukkitWorldInterface implements WorldInterface
int chunkX = Coordinates.blockToChunk(x);
int chunkZ = Coordinates.blockToChunk(z);
String key = chunkX + "x" + chunkZ;
int sectionX = Coordinates.chunkToSection(chunkX);
int sectionZ = Coordinates.chunkToSection(chunkZ);
if (this.chunks.containsKey(key)) {
return this.chunks.get(key);
if (this.sectionX == null) {
this.sectionX = sectionX;
this.sectionZ = sectionZ;
} else if (this.sectionX != sectionX || this.sectionZ != sectionZ) {
throw new IllegalStateException("We appear to be leaking world interface instances across LOD sections");
}
ChunkSnapshot chunk;
int baseChunkX = Coordinates.sectionToChunk(sectionX);
int baseChunkZ = Coordinates.sectionToChunk(sectionZ);
int index = (chunkX - baseChunkX) + ((chunkZ - baseChunkZ) << 2);
ChunkSnapshot chunk = this.chunks[index];
if (chunk != null) {
return chunk;
}
Scheduler scheduler = this.plugin.getDhSupport().getScheduler();
@@ -165,11 +185,16 @@ public class BukkitWorldInterface implements WorldInterface
).join();
}
this.chunks.put(key, chunk);
this.chunks[index] = chunk;
return chunk;
}
public boolean isInsideSection(int x, int z)
{
return Coordinates.blockToSection(x) == this.sectionX && Coordinates.blockToSection(z) == this.sectionZ;
}
@Override
public UUID getId()
{
@@ -503,7 +528,7 @@ public class BukkitWorldInterface implements WorldInterface
}
if (this.isDummyMode()) {
return Collections.max(this.getDummyColumn().keySet());
return this.getDummyColumn().isEmpty() ? this.getMinY() : Collections.max(this.getDummyColumn().keySet());
}
return this.getChunk(x, z).getHighestBlockYAt(Coordinates.blockToChunkRelative(x), Coordinates.blockToChunkRelative(z));
@@ -540,45 +565,98 @@ public class BukkitWorldInterface implements WorldInterface
public String getMaterialAt(int x, int y, int z)
{
if (this.isDummyMode()) {
return this.getDummyMaterialAt(y);
return Utils.namespaceKey(this.getDummyMaterialAt(y));
}
String material = this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getKey().toString();
return this.getMaterialRecordAt(x, y, z).toString();
}
return Utils.namespaceKey(this.config.getString(DhsConfig.MATERIAL_MAP + "." + Utils.unNamespaceKey(material), material));
public MaterialRecord getMaterialRecordAt(int x, int y, int z)
{
return this.materialCache.computeIfAbsent(
this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)),
(mat) -> {
String key = Utils.unNamespaceKey(mat.getKey().toString());
String mapped = this.config.getString(DhsConfig.MATERIAL_MAP + '.' + key, key);
return new MaterialRecord(
Utils.namespaceKey(mapped == null ? key : mapped),
!key.equals(mapped)
);
}
);
}
@Override
public String getBlockStateAsStringAt(int x, int y, int z)
{
if (this.isDummyMode()) {
return "";
MaterialRecord materialRecord = this.getMaterialRecordAt(x, y, z);
if (this.isDummyMode() || materialRecord.isMapped()) {
return materialRecord.toString();
}
return this.getChunk(x, z).getBlockData(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getAsString();
Map<String, String> properties = this.getBlockPropertiesAt(x, y, z);
if (properties.isEmpty()) {
return materialRecord.toString();
}
StringBuilder builder = new StringBuilder();
builder.append(materialRecord.toString())
.append('[');
for (Map.Entry<String, String> property : properties.entrySet()) {
builder.append(property.getKey())
.append('=')
.append(property.getValue())
.append(',');
}
builder.setLength(builder.length() - 1);
builder.append(']');
return builder.toString();
}
@Override
public Map<String, String> getBlockPropertiesAt(int x, int y, int z)
{
Map<String, String> properties = new HashMap<>();
if (this.isDummyMode()) {
return Collections.emptyMap();
}
String dataString = this.getBlockStateAsStringAt(x, y, z);
String dataString = this.getChunk(x, z).getBlockData(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).getAsString();
int kvStart = dataString.indexOf("[");
Map<String, String> cached = this.propertyCache.get(dataString);
if (cached != null) {
return cached;
}
int kvStart = dataString.indexOf('[');
if (kvStart == -1) {
return properties;
return Collections.emptyMap();
}
String[] kvStrings = dataString.substring(kvStart + 1, dataString.length() - 1).split(",");
Arrays.sort(kvStrings);
Map<String, String> properties = new LinkedHashMap<>(kvStrings.length);
for (String kvString : kvStrings) {
String[] kv = kvString.split("=", 2);
properties.put(kv[0], kv[1]);
}
properties = Collections.unmodifiableMap(properties);
this.propertyCache.put(dataString, properties);
return properties;
}
@@ -615,15 +693,63 @@ public class BukkitWorldInterface implements WorldInterface
return false;
}
if (!this.getMaterialAt(x, y, z).equals("minecraft:beacon")) {
if (!this.getChunk(x, z).getBlockType(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z)).equals(Material.BEACON)) {
return false;
}
return this.plugin.getDhSupport().getScheduler()
.runOnRegionThread(this.world.getUID(), x, z, () ->
((Beacon) this.world.getBlockAt(x, y, z).getState()).getTier() > 0
)
.join();
return this.hasTierOneBeaconBase(x, y, z) && this.hasClearBeaconBeamPath(x, y, z);
}
public boolean hasTierOneBeaconBase(int x, int y, int z)
{
int baseY = y - 1;
if (baseY < this.getMinY()) {
return false;
}
for (int checkX = x - 1; checkX <= x + 1; checkX++) {
for (int checkZ = z - 1; checkZ <= z + 1; checkZ++) {
if (this.isInsideSection(checkX, checkZ) && !this.isBeaconBaseBlock(this.getMaterialAt(checkX, baseY, checkZ))) {
return false;
}
}
}
return true;
}
private boolean isBeaconBaseBlock(String material)
{
switch (material) {
case "minecraft:iron_block":
case "minecraft:gold_block":
case "minecraft:diamond_block":
case "minecraft:emerald_block":
case "minecraft:netherite_block":
return true;
default:
return false;
}
}
public boolean hasClearBeaconBeamPath(int x, int y, int z)
{
for (int checkY = y + 1; checkY < this.getMaxY(); checkY++) {
String material = this.getMaterialAt(x, checkY, z);
switch (material) {
case "minecraft:air":
case "minecraft:cave_air":
case "minecraft:void_air":
case "minecraft:glass":
continue;
default:
return material.endsWith("_stained_glass");
}
}
return true;
}
@Override
@@ -30,6 +30,7 @@ import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.MultiLineChart;
import org.bstats.charts.SingleLineChart;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
@@ -65,9 +66,11 @@ public class DhSupportBukkitPlugin extends JavaPlugin
}
this.getLogger().info("I am DHS " + pluginVersion + ".");
String gameVersion = this.getGameVersion();
this.dhSupport = new DhSupport(pluginVersion, this.getServer().getMinecraftVersion());
this.getLogger().info("I am DHS " + pluginVersion + " running on " + this.getServer().getName() + " for Minecraft " + gameVersion + ".");
this.dhSupport = new DhSupport(pluginVersion, gameVersion);
this.dhSupport.setLogger(this.getLogger());
this.dhSupport.setDataDirectory(this.getDataFolder().getAbsolutePath());
@@ -161,6 +164,21 @@ public class DhSupportBukkitPlugin extends JavaPlugin
return null;
}
protected String getGameVersion()
{
try {
Class<? extends Server> serverClass = this.getServer().getClass();
serverClass.getMethod("getMinecraftVersion");
return this.getServer().getMinecraftVersion();
} catch (NoSuchMethodException exception) {
// If we're here, that probably means we're not running Paper or any Paper fork.
}
return this.getServer().getBukkitVersion().split("-", 2)[0];
}
@Nullable
public DhSupport getDhSupport()
{
@@ -28,6 +28,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class PlayerHandler implements Listener
@@ -51,7 +52,7 @@ public class PlayerHandler implements Listener
return null;
}
for (String message : dhSupport.getConfig().getStringList(DhsConfig.AD_MESSAGE, new ArrayList<>())) {
for (String message : dhSupport.getConfig().getStringList(DhsConfig.AD_MESSAGE, Collections.emptyList())) {
player.sendMessage(message);
}
@@ -18,6 +18,8 @@
package no.jckf.dhsupport.core;
import java.util.function.BiConsumer;
// Block -> chunk -> section -> region
public class Coordinates
{
@@ -101,4 +103,39 @@ public class Coordinates
{
return value & 0x0F;
}
// PACK/UNPACK
public static long pack(int x, int z)
{
return ((long) x & 0xffffffffL)
| (((long) z & 0xffffffffL) << 32);
}
public static int unpackX(long packed)
{
return (int) packed;
}
public static int unpackZ(long packed)
{
return (int) (packed >> 32);
}
// ???
public static void foreachChunkInSection(int sectionX, int sectionZ, BiConsumer<Integer, Integer> consumer)
{
int worldX = sectionToBlock(sectionX);
int worldZ = sectionToBlock(sectionZ);
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
consumer.accept(
worldX + 16 * xMultiplier,
worldZ + 16 * zMultiplier
);
}
}
}
}
@@ -400,24 +400,19 @@ public class DhSupport implements Configurable
boolean generateNewChunks = world.getConfig().getBool(DhsConfig.GENERATE_NEW_CHUNKS, true);
Map<String, CompletableFuture<Boolean>> loads = new HashMap<>();
Map<Long, CompletableFuture<Boolean>> loads = new HashMap<>();
if (manageChunks) {
// Load all the chunks we need for this request.
for (int xMultiplier = 0; xMultiplier < 4; xMultiplier++) {
for (int zMultiplier = 0; zMultiplier < 4; zMultiplier++) {
int chunkX = worldX + 16 * xMultiplier;
int chunkZ = worldZ + 16 * zMultiplier;
if (!world.isChunkLoaded(chunkX, chunkZ)) {
if (generateNewChunks) {
loads.put(worldX + "x" + worldZ, world.loadOrGenerateChunkAsync(chunkX, chunkZ));
} else {
loads.put(worldX + "x" + worldZ, world.loadChunkAsync(chunkX, chunkZ));
}
Coordinates.foreachChunkInSection(position.getX(), position.getZ(), (chunkX, chunkZ) -> {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
if (generateNewChunks) {
loads.put(Coordinates.pack(chunkX, chunkZ), world.loadOrGenerateChunkAsync(chunkX, chunkZ));
} else {
loads.put(Coordinates.pack(chunkX, chunkZ), world.loadChunkAsync(chunkX, chunkZ));
}
}
}
});
}
// Wait for chunk loads, then...
@@ -436,52 +431,36 @@ public class DhSupport implements Configurable
.anyMatch(Predicate.isEqual(false));
if (loadRejected) {
// Discard the chunks we loaded.
this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
for (String key : loads.keySet()) {
String[] xz = key.split("x", 2);
world.discardChunk(
Integer.parseInt(xz[0]),
Integer.parseInt(xz[1])
);
}
return null;
});
// If no dummy chunk is configured, reject the build request.
if (world.getConfig().getStringList(DhsConfig.DUMMY_CHUNK).isEmpty()) {
return CompletableFuture.completedFuture(null);
}
// Use dummy chunks instead of real world data.
world.setDummyMode(true);
} else {
// Prime chunk data in world interface.
Coordinates.foreachChunkInSection(position.getX(), position.getZ(), (chunkX, chunkZ) -> {
world.getMaterialAt(chunkX, world.getSeaLevel(), chunkZ);
});
}
// Discard the chunks we loaded.
this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
for (long packed : loads.keySet()) {
world.discardChunk(
Coordinates.unpackX(packed),
Coordinates.unpackZ(packed)
);
}
return null;
});
// No LOD was found. Start building a new one.
CompletableFuture<Lod> lodFuture = this.queueBuilder(worldId, position, this.getBuilder(world, position));
// Combine the LOD and beacons and save the result in the database.
return lodFuture.thenApply((lod) -> {
// Discard the chunks we loaded.
this.getScheduler().runOnRegionThread(worldId, worldX, worldZ, () -> {
for (String key : loads.keySet()) {
String[] xz = key.split("x", 2);
world.discardChunk(
Integer.parseInt(xz[0]),
Integer.parseInt(xz[1])
);
}
return null;
});
Encoder lodEncoder = new Encoder();
Encoder lodEncoder = new Encoder(8192);
lod.encode(lodEncoder);
Encoder beaconEncoder = new Encoder();
Encoder beaconEncoder = new Encoder(16 + lod.getBeacons().size() * 16);
beaconEncoder.writeCollection(lod.getBeacons());
this.generationTracker.ping();
@@ -592,7 +571,7 @@ public class DhSupport implements Configurable
continue;
}
if (!playerConfig.getBool(DhsConfig.DISTANT_GENERATION_ENABLED) || !playerConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED)) {
if (!playerConfig.getBool(DhsConfig.REAL_TIME_UPDATES_ENABLED)) {
continue;
}
@@ -627,11 +606,11 @@ public class DhSupport implements Configurable
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(Arrays.copyOfRange(
chunkResponse.setData(
data,
LodHandler.CHUNK_SIZE * chunkNo,
Math.min(LodHandler.CHUNK_SIZE * chunkNo + LodHandler.CHUNK_SIZE, data.length)
));
Math.min(LodHandler.CHUNK_SIZE, data.length - LodHandler.CHUNK_SIZE * chunkNo)
);
this.pluginMessageHandler.sendPluginMessage(player.getUniqueId(), chunkResponse);
}
@@ -697,7 +676,7 @@ public class DhSupport implements Configurable
{
WorldInterface world = this.getWorldInterface(worldId);
List<SectionPosition> positions = new ArrayList<>();
List<SectionPosition> positions = new ArrayList<>(concurrency);
for (int i = 0; i < concurrency; i++) {
SectionPosition position = new SectionPosition();
@@ -713,7 +692,7 @@ public class DhSupport implements Configurable
.toArray(CompletableFuture[]::new)
)
.thenCompose((foo) -> {
List<CompletableFuture<?>> chains = new ArrayList<>();
List<CompletableFuture<?>> chains = new ArrayList<>(positions.size());
long ts = System.currentTimeMillis();
@@ -76,7 +76,7 @@ public class PreGenerator implements Runnable
int[][] directions = {{ 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 }};
int dirIndex = 0;
List<CompletableFuture<LodModel>> requests = new ArrayList<>();
List<CompletableFuture<LodModel>> requests = new ArrayList<>(rateLimit);
this.startTime = System.currentTimeMillis();
@@ -21,11 +21,16 @@ package no.jckf.dhsupport.core;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Utils
{
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private static final Map<String, String> NAMESPACE_CACHE = new ConcurrentHashMap<>();
private static final Map<String, String> UNNAMESPACE_CACHE = new ConcurrentHashMap<>();
public static String bytesToHex(byte[] bytes)
{
byte[] hexChars = new byte[bytes.length * 2];
@@ -115,9 +120,12 @@ public class Utils
return builder.substring(0, builder.length() - 2);
}
/**
* @deprecated For awareness: This method caches results by key only.
*/
public static String namespaceKey(String key, String defaultNamespace)
{
return key.contains(":") ? key : defaultNamespace + ":" + key;
return NAMESPACE_CACHE.computeIfAbsent(key, (k) -> k.contains(":") ? k : defaultNamespace + ":" + k);
}
public static String namespaceKey(String key)
@@ -125,9 +133,12 @@ public class Utils
return namespaceKey(key, "minecraft");
}
/**
* @deprecated For awareness: This method caches results by key only.
*/
public static String unNamespaceKey(String key, String namespace)
{
return key.startsWith(namespace + ":") ? key.substring(namespace.length() + 1) : key;
return UNNAMESPACE_CACHE.computeIfAbsent(key, (k) -> k.startsWith(namespace + ":") ? k.substring(namespace.length() + 1) : k);
}
public static String unNamespaceKey(String key)
@@ -110,7 +110,7 @@ public class Decoder
{
int size = this.readInt();
List<T> items = new ArrayList<>();
List<T> items = new ArrayList<>(Math.min(size, 128));
for (int i = 0; i < size; i++) {
T item = this.readObject(className);
@@ -33,11 +33,21 @@ public class Encoder
this.output = ByteStreams.newDataOutput();
}
public Encoder(int size)
{
this.output = ByteStreams.newDataOutput(size);
}
public void write(byte[] data)
{
this.output.write(data);
}
public void write(byte[] data, int offset, int length)
{
this.output.write(data, offset, length);
}
public void writeByte(byte value)
{
this.output.writeByte(value);
@@ -22,6 +22,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -35,6 +36,11 @@ public class Configuration
this.variables.clear();
}
public boolean has(String key)
{
return this.variables.containsKey(key);
}
public void set(String key, @Nullable Object value)
{
if (value instanceof String stringValue) {
@@ -106,7 +112,7 @@ public class Configuration
public List<String> getStringList(String key)
{
return this.getStringList(key, new ArrayList<>());
return this.getStringList(key, Collections.emptyList());
}
public int increment(String key)
@@ -42,6 +42,10 @@ public class Lod extends DataObject
public static int width = 64;
protected static volatile byte[] compressedGenerationStep;
protected static volatile byte[] compressedWorldCompressionType;
protected WorldInterface worldInterface;
protected SectionPosition position;
@@ -63,7 +67,7 @@ public class Lod extends DataObject
protected void encodeData(Encoder encoder)
{
Encoder toCompress = new Encoder();
Encoder toCompress = new Encoder(8192);
for (List<DataPoint> column : this.columns) {
toCompress.writeShort(column.size());
@@ -89,10 +93,15 @@ public class Lod extends DataObject
protected void encodeColumnGenerationStep(Encoder encoder)
{
byte[] bytesToCompress = new byte[Lod.width * Lod.width];
Arrays.fill(bytesToCompress, (byte) GenerationStep.LIGHT.value);
byte[] compressed = Lod.compressedGenerationStep;
byte[] compressed = this.compress(bytesToCompress);
if (compressed == null) {
byte[] bytesToCompress = new byte[Lod.width * Lod.width];
Arrays.fill(bytesToCompress, (byte) GenerationStep.LIGHT.value);
compressed = this.compress(bytesToCompress);
Lod.compressedGenerationStep = compressed;
}
encoder.writeInt(compressed.length);
encoder.write(compressed);
@@ -100,9 +109,14 @@ public class Lod extends DataObject
protected void encodeWorldCompressionType(Encoder encoder)
{
byte[] bytesToCompress = new byte[Lod.width * Lod.width]; // All zeroes means WorldCompressionType.STRICT
byte[] compressed = Lod.compressedWorldCompressionType;
byte[] compressed = this.compress(bytesToCompress);
if (compressed == null) {
byte[] bytesToCompress = new byte[Lod.width * Lod.width]; // All zeroes means WorldCompressionType.STRICT
compressed = this.compress(bytesToCompress);
Lod.compressedWorldCompressionType = compressed;
}
encoder.writeInt(compressed.length);
encoder.write(compressed);
@@ -110,7 +124,7 @@ public class Lod extends DataObject
protected void encodeMappings(Encoder encoder)
{
Encoder toCompress = new Encoder();
Encoder toCompress = new Encoder(1024);
toCompress.writeInt(this.idMappings.size());
@@ -155,7 +169,11 @@ public class Lod extends DataObject
ByteArrayOutputStream compressedStream = new ByteArrayOutputStream();
try {
OutputStream compressorStream = new XZOutputStream(compressedStream, new LZMA2Options(3), XZ.CHECK_CRC64);
LZMA2Options options = new LZMA2Options(3);
options.setDictSize(Math.max(LZMA2Options.DICT_SIZE_MIN, Math.min(uncompressedData.length, 1 << 20)));
OutputStream compressorStream = new XZOutputStream(compressedStream, options, XZ.CHECK_CRC64);
compressorStream.write(uncompressedData);
compressorStream.flush();
@@ -30,7 +30,6 @@ import no.jckf.dhsupport.core.message.plugin.FullDataSourceResponseMessage;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.Bukkit;
import java.util.Arrays;
import java.util.UUID;
public class LodHandler
@@ -174,11 +173,11 @@ public class LodHandler
FullDataChunkMessage chunkResponse = new FullDataChunkMessage();
chunkResponse.setBufferId(myBufferId);
chunkResponse.setIsFirst(chunkNo == 0);
chunkResponse.setData(Arrays.copyOfRange(
chunkResponse.setData(
data,
CHUNK_SIZE * chunkNo,
Math.min(CHUNK_SIZE * chunkNo + CHUNK_SIZE, data.length)
));
Math.min(CHUNK_SIZE, data.length - CHUNK_SIZE * chunkNo)
);
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), chunkResponse);
}
@@ -22,12 +22,17 @@ import no.jckf.dhsupport.core.DhSupport;
import no.jckf.dhsupport.core.configuration.Configuration;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import no.jckf.dhsupport.core.message.plugin.LevelInitMessage;
import no.jckf.dhsupport.core.message.plugin.LevelInitRequestMessage;
import no.jckf.dhsupport.core.message.plugin.RemotePlayerConfigMessage;
import no.jckf.dhsupport.core.world.WorldInterface;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.UUID;
public class PlayerConfigHandler
{
public final static String USAGE_PERMISSION = "distant_horizons.worlds.%s";
@@ -44,14 +49,28 @@ public class PlayerConfigHandler
public void register()
{
this.pluginMessageHandler.getEventBus().registerHandler(RemotePlayerConfigMessage.class, (configMessage) -> {
this.pluginMessageHandler.getEventBus().registerHandler(LevelInitRequestMessage.class, (initRequestMessage) -> {
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
Player player = Bukkit.getPlayer(configMessage.getSender());
Player player = Bukkit.getPlayer(initRequestMessage.getSender());
WorldInterface world = this.dhSupport.getWorldInterface(player.getWorld().getUID());
NamespacedKey worldKey = NamespacedKey.fromString(initRequestMessage.getWorldKey());
String permission = USAGE_PERMISSION.formatted(world.getName());
boolean dhUseIsAllowed = !player.isPermissionSet(permission) || player.hasPermission(permission);
// TODO: This too 👎
UUID worldUuid = null;
for (World world : Bukkit.getServer().getWorlds()) {
if (world.getKey().equals(worldKey)) {
worldUuid = world.getUID();
break;
}
}
if (worldUuid == null) {
this.dhSupport.warning("Player " + player.getName() + " requested init for unknown world " + worldKey + ".");
return;
}
WorldInterface world = this.dhSupport.getWorldInterface(worldUuid);
Configuration dhsConfig = world.getConfig();
@@ -62,13 +81,24 @@ public class PlayerConfigHandler
levelKey = levelKeyPrefix + levelKey;
}
//this.dhSupport.info("Received DH config for " + player.getName() + " in " + levelKey);
LevelInitMessage levelInitResponse = new LevelInitMessage();
levelInitResponse.setNamespacedKey(worldKey.toString());
levelInitResponse.setServerKey(dhsConfig.getString(DhsConfig.SERVER_KEY, ""));
levelInitResponse.setLevelKey(levelKey);
levelInitResponse.setTime(System.currentTimeMillis());
this.pluginMessageHandler.sendPluginMessage(configMessage.getSender(), levelInitResponse);
this.pluginMessageHandler.sendPluginMessage(initRequestMessage.getSender(), levelInitResponse);
});
this.pluginMessageHandler.getEventBus().registerHandler(RemotePlayerConfigMessage.class, (configMessage) -> {
// TODO: Some sort of Player wrapper or interface object. Bukkit classes should not be imported here.
Player player = Bukkit.getPlayer(configMessage.getSender());
WorldInterface world = this.dhSupport.getWorldInterface(player.getWorld().getUID());
String permission = USAGE_PERMISSION.formatted(world.getName());
boolean dhUseIsAllowed = !player.isPermissionSet(permission) || player.hasPermission(permission);
Configuration dhsConfig = world.getConfig();
Configuration clientConfig = configMessage.toConfiguration();
@@ -92,6 +122,10 @@ public class PlayerConfigHandler
}
}
if (key.equals(DhsConfig.REAL_TIME_UPDATES_ENABLED) && !dhUseIsAllowed) {
keepValue = false;
}
// TODO: This is ugly. Move to comparator closures like in DH.
if (key.equals(DhsConfig.BORDER_CENTER_X)) {
keepValue = world.getWorldBorderX();
@@ -38,7 +38,7 @@ public class PluginMessageHandler
public final String pluginChannel = "distant_horizons:msg";
public final short protocolVersion = 13;
public final short protocolVersion = 15;
private EventBus<PluginMessage> eventBus;
@@ -53,13 +53,14 @@ public class PluginMessageHandler
this.messageTypeRegistry.registerMessageType(0, null);
this.messageTypeRegistry.registerMessageType(1, CloseReasonMessage.class);
this.messageTypeRegistry.registerMessageType(2, LevelInitMessage.class);
this.messageTypeRegistry.registerMessageType(3, RemotePlayerConfigMessage.class);
this.messageTypeRegistry.registerMessageType(4, CancelMessage.class);
this.messageTypeRegistry.registerMessageType(5, ExceptionMessage.class);
this.messageTypeRegistry.registerMessageType(6, FullDataSourceRequestMessage.class);
this.messageTypeRegistry.registerMessageType(7, FullDataSourceResponseMessage.class);
this.messageTypeRegistry.registerMessageType(8, FullDataPartialUpdateMessage.class);
this.messageTypeRegistry.registerMessageType(9, FullDataChunkMessage.class);
this.messageTypeRegistry.registerMessageType(3, LevelInitRequestMessage.class);
this.messageTypeRegistry.registerMessageType(4, RemotePlayerConfigMessage.class);
this.messageTypeRegistry.registerMessageType(5, CancelMessage.class);
this.messageTypeRegistry.registerMessageType(6, ExceptionMessage.class);
this.messageTypeRegistry.registerMessageType(7, FullDataSourceRequestMessage.class);
this.messageTypeRegistry.registerMessageType(8, FullDataSourceResponseMessage.class);
this.messageTypeRegistry.registerMessageType(9, FullDataPartialUpdateMessage.class);
this.messageTypeRegistry.registerMessageType(10, FullDataChunkMessage.class);
}
public void onEnable()
@@ -153,19 +154,7 @@ public class PluginMessageHandler
return;
}
byte[] data;
try {
Encoder encoder = new Encoder();
message.encode(encoder);
data = encoder.toByteArray();
} catch (Exception exception) {
this.dhSupport.warning("Failed to encode " + message.getClass().getSimpleName());
exception.printStackTrace();
return;
}
Encoder encoder = new Encoder();
Encoder encoder = new Encoder(message.bufferSize());
encoder.writeShort(this.protocolVersion);
encoder.writeShort(messageTypeId);
@@ -174,7 +163,13 @@ public class PluginMessageHandler
encoder.writeInt(((TrackablePluginMessage) message).getTracker());
}
encoder.write(data);
try {
message.encode(encoder);
} catch (Exception exception) {
this.dhSupport.warning("Failed to encode " + message.getClass().getSimpleName());
exception.printStackTrace();
return;
}
byte[] fullMessage = encoder.toByteArray();
@@ -56,12 +56,12 @@ public class FastOverworldBuilder extends LodBuilder
boolean includeNonCollidingTopLayer = this.worldInterface.getConfig().getBool(DhsConfig.INCLUDE_NON_COLLIDING_TOP_LAYER, true);
boolean sampleBiomes3d = this.worldInterface.getConfig().getBool(DhsConfig.SAMPLE_BIOMES_3D, false);
List<IdMapping> idMappings = new ArrayList<>();
Map<String, Integer> mapMap = new HashMap<>();
List<IdMapping> idMappings = new ArrayList<>(64);
Map<String, Map<String, Integer>> mapMap = new HashMap<>();
List<List<DataPoint>> columns = new ArrayList<>();
List<List<DataPoint>> columns = new ArrayList<>(Lod.width * Lod.width);
List<Beacon> beacons = new ArrayList<>();
List<Beacon> beacons = new ArrayList<>(8);
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
@@ -100,7 +100,7 @@ public class FastOverworldBuilder extends LodBuilder
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
}
List<DataPoint> column = new ArrayList<>();
List<DataPoint> column = new ArrayList<>(8);
@Nullable
DataPoint previous = null;
@@ -158,15 +158,18 @@ public class FastOverworldBuilder extends LodBuilder
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
}
String compositeKey = biome + "|" + material;
Map<String, Integer> biomeMap = mapMap.get(biome);
if (biomeMap == null) {
biomeMap = new HashMap<>();
mapMap.put(biome, biomeMap);
}
@Nullable
Integer id = mapMap.get(compositeKey);
Integer id = biomeMap.get(material);
if (id == null) {
idMappings.add(new IdMapping(biome, material, null));
id = idMappings.size() - 1;
mapMap.put(compositeKey, id);
biomeMap.put(material, id);
}
DataPoint point;
@@ -52,12 +52,12 @@ public class FullBuilder extends LodBuilder
boolean performUnderglowHack = this.worldInterface.getConfig().getBool(DhsConfig.PERFORM_UNDERGLOW_HACK, false);
boolean sampleBiomes3d = this.worldInterface.getConfig().getBool(DhsConfig.SAMPLE_BIOMES_3D, false);
List<IdMapping> idMappings = new ArrayList<>();
Map<String, Integer> mapMap = new HashMap<>();
List<IdMapping> idMappings = new ArrayList<>(64);
Map<String, Map<String, Map<String, Integer>>> mapMap = new HashMap<>();
List<List<DataPoint>> columns = new ArrayList<>();
List<List<DataPoint>> columns = new ArrayList<>(Lod.width * Lod.width);
List<Beacon> beacons = new ArrayList<>();
List<Beacon> beacons = new ArrayList<>(8);
for (int relativeX = 0; relativeX < Lod.width; relativeX++) {
for (int relativeZ = 0; relativeZ < Lod.width; relativeZ++) {
@@ -96,7 +96,7 @@ public class FullBuilder extends LodBuilder
biome = this.worldInterface.getBiomeAt(worldX, worldZ);
}
List<DataPoint> column = new ArrayList<>();
List<DataPoint> column = new ArrayList<>(32);
@Nullable
DataPoint previous = null;
@@ -138,15 +138,26 @@ public class FullBuilder extends LodBuilder
biome = this.worldInterface.getBiomeAt(worldX, highWorldY, worldZ);
}
String compositeKey = biome + "|" + material + "|" + this.worldInterface.getBlockStateAsStringAt(worldX, highWorldY, worldZ);
Map<String, Map<String, Integer>> biomeMap = mapMap.get(biome);
if (biomeMap == null) {
biomeMap = new HashMap<>();
mapMap.put(biome, biomeMap);
}
@Nullable
Integer id = mapMap.get(compositeKey);
Map<String, Integer> materialMap = biomeMap.get(material);
if (materialMap == null) {
materialMap = new HashMap<>();
biomeMap.put(material, materialMap);
}
String blockState = this.worldInterface.getBlockStateAsStringAt(worldX, highWorldY, worldZ);
Integer id = materialMap.get(blockState);
if (id == null) {
idMappings.add(new IdMapping(biome, material, this.worldInterface.getBlockPropertiesAt(worldX, highWorldY, worldZ)));
id = idMappings.size() - 1;
mapMap.put(compositeKey, id);
materialMap.put(blockState, id);
}
DataPoint point;
@@ -25,6 +25,11 @@ import no.jckf.dhsupport.core.bytestream.Encoder;
public abstract class Message implements Encodable, Decodable
{
public int bufferSize()
{
return 64;
}
public void encode(Encoder encoder)
{
throw new UnsupportedOperationException();
@@ -26,6 +26,10 @@ public class FullDataChunkMessage extends PluginMessage
protected byte[] data;
protected int offset;
protected int length;
protected boolean isFirst = true;
public void setBufferId(int bufferId)
@@ -38,9 +42,11 @@ public class FullDataChunkMessage extends PluginMessage
return this.bufferId;
}
public void setData(byte[] data)
public void setData(byte[] data, int offset, int length)
{
this.data = data;
this.offset = offset;
this.length = length;
}
public byte[] getData()
@@ -58,12 +64,18 @@ public class FullDataChunkMessage extends PluginMessage
return this.isFirst;
}
@Override
public int bufferSize()
{
return 32 + this.length;
}
@Override
public void encode(Encoder encoder)
{
encoder.writeInt(this.bufferId);
encoder.writeInt(this.data.length);
encoder.write(this.data);
encoder.writeInt(this.length);
encoder.write(this.data, this.offset, this.length);
encoder.writeBoolean(this.isFirst);
}
}
@@ -58,6 +58,12 @@ public class FullDataPartialUpdateMessage extends PluginMessage
return this.beacons;
}
@Override
public int bufferSize()
{
return 32 + this.beacons.length;
}
@Override
public void encode(Encoder encoder)
{
@@ -48,6 +48,12 @@ public class FullDataSourceResponseMessage extends TrackablePluginMessage
return this.beacons;
}
@Override
public int bufferSize()
{
return this.bufferId == null ? super.bufferSize() : 16 + this.beacons.length;
}
@Override
public void encode(Encoder encoder)
{
@@ -21,14 +21,26 @@ package no.jckf.dhsupport.core.message.plugin;
import no.jckf.dhsupport.core.bytestream.Decoder;
import no.jckf.dhsupport.core.bytestream.Encoder;
public class LevelInitMessage extends PluginMessage
public class LevelInitMessage extends PluginMessage // TODO: Should probably eventually be a TrackablePluginMessage
{
protected String namespacedKey;
protected String serverKey;
protected String levelKey;
protected long time;
public void setNamespacedKey(String namespacedKey)
{
this.namespacedKey = namespacedKey;
}
public String getNamespacedKey()
{
return namespacedKey;
}
public void setServerKey(String serverKey)
{
this.serverKey = serverKey;
@@ -62,6 +74,7 @@ public class LevelInitMessage extends PluginMessage
@Override
public void encode(Encoder encoder)
{
encoder.writeShortString(this.namespacedKey);
encoder.writeShortString(this.serverKey);
encoder.writeShortString(this.levelKey);
encoder.writeLong(this.time);
@@ -70,6 +83,7 @@ public class LevelInitMessage extends PluginMessage
@Override
public void decode(Decoder decoder)
{
this.namespacedKey = decoder.readShortString();
this.serverKey = decoder.readShortString();
this.levelKey = decoder.readShortString();
this.time = decoder.readLong();
@@ -0,0 +1,37 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.message.plugin;
import no.jckf.dhsupport.core.bytestream.Decoder;
public class LevelInitRequestMessage extends PluginMessage
{
protected String worldKey;
public String getWorldKey()
{
return worldKey;
}
@Override
public void decode(Decoder decoder)
{
this.worldKey = decoder.readShortString();
}
}
@@ -0,0 +1,42 @@
/*
* DH Support, server-side support for Distant Horizons.
* Copyright (C) 2024 Jim C K Flaten
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package no.jckf.dhsupport.core.world;
public class MaterialRecord
{
protected final String key;
protected final boolean isMapped;
public MaterialRecord(String key, boolean mapped)
{
this.key = key;
this.isMapped = mapped;
}
public boolean isMapped()
{
return this.isMapped;
}
public String toString()
{
return this.key;
}
}
@@ -85,8 +85,16 @@ public interface WorldInterface extends Configurable
String getMaterialAt(int x, int y, int z);
/**
* Get the string representation of the block state at the given coordinates.
* Block state properties are ordered in ascending order.
*/
String getBlockStateAsStringAt(int x, int y, int z);
/**
* Get block properties as key/value pairs.
* Ordered in ascending order.
*/
Map<String, String> getBlockPropertiesAt(int x, int y, int z);
byte getBlockLightAt(int x, int y, int z);