Compare commits

...
49 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
Jim C K Flaten f7822bc204 Merge branch 'release/0.13.0' 2026-04-19 01:29:06 +02:00
Jim C K Flaten ca4a3a6bcc Release 0.13.0. 2026-04-19 01:28:33 +02:00
Jim C K Flaten a189487623 Shorter channel name, for compatibility with MC < 1.13. 2026-04-19 01:23:29 +02:00
Jim C K Flaten 721d4c2027 Add config option to ignore world height map. 2026-02-05 01:13:11 +01:00
Jim C K Flaten 577a45be5c Correct DataPoint.getStartY() unpacking. 2026-02-05 01:12:52 +01:00
Jim C K Flaten 8bc102a26f Add unit to config comments. 2026-01-22 20:02:06 +01:00
Jim C K Flaten 8aa9fa9c31 Be better behaved around ResultSets. 2026-01-05 21:25:49 +01:00
Jckf 3644051706 Don't warn if permission is denied. 2026-01-05 16:16:47 +01:00
Jckf 2786f4b8c7 Warn if a client has disabled Distant Generation. 2026-01-05 16:07:06 +01:00
Jim C K Flaten 930483ee0c Fix light not being sampled for top-most data point in a column. 2026-01-02 19:02:07 +01:00
Jim C K Flaten 64b24aba31 Use minY as default for 1.20.1 hack. 2026-01-02 15:43:21 +01:00
Jim C K Flaten 22447f5bd5 Fix a couple comment mistakes in the config file. 2026-01-02 03:16:44 +01:00
Jim C K Flaten 697ca18299 Workaround for 1.20.1 lighting. 2026-01-02 00:14:25 +01:00
Jim C K Flaten bef45fe732 Puase inside builder thread, and reject generation requests while paused. 2026-01-01 21:05:24 +01:00
Jim C K Flaten f60736f21e Call BStats shutdown on disable. 2026-01-01 20:51:32 +01:00
Jim C K Flaten a36102c16e Trigger updates for new chunks.
Updates are ignored if there isn't an LOD for that area already, so this will only affect server that use the "fake chunk" feature.
2025-12-21 22:59:57 +01:00
Jim C K Flaten ed98a7e0d4 Also track DH RDs. 2025-12-21 00:16:18 +01:00
Jim C K Flaten 1b4138f537 Report number of players with DH. 2025-12-18 20:40:22 +01:00
Jim C K Flaten a387c7730d Don't try to scan for beacon color above the build height limit. 2025-12-18 15:38:41 +01:00
Jim C K Flaten dbb9d8a2cf Bump version number. 2025-12-14 17:51:14 +01:00
Jim C K Flaten 3c76e8f229 Merge tag '0.12.0' into develop
0.12.0
2025-12-14 17:50:29 +01:00
32 changed files with 657 additions and 198 deletions

No files matched your search

+2 -2
View File
@@ -4,7 +4,7 @@ plugins {
}
group = 'no.jckf.dhs'
version = '0.12.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;
@@ -56,7 +56,16 @@ public class BukkitWorldInterface implements WorldInterface
protected Map<Integer, String> dummyColumn;
protected Map<String, ChunkSnapshot> chunks = new HashMap<>();
protected boolean trustHeightMap = true;
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;
@@ -69,12 +78,17 @@ public class BukkitWorldInterface implements WorldInterface
@Nullable
protected Method getChunkAtAsync;
protected int lightOffset = 0;
public BukkitWorldInterface(DhSupportBukkitPlugin plugin, World world, Configuration config)
{
this.plugin = plugin;
this.world = world;
this.config = config;
this.worldConfig = new WorldConfiguration(this, config);
this.lightOffset = this.getConfig().getInt(DhsConfig.LIGHT_OFFSET_HACK, this.plugin.getDhSupport().getGameVersion().equals("1.20.1") ? this.world.getMinHeight() : 0);
this.trustHeightMap = this.getConfig().getBool(DhsConfig.TRUST_HEIGHT_MAP, this.trustHeightMap);
}
public void setLogger(Logger logger)
@@ -140,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();
@@ -158,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()
{
@@ -491,8 +523,12 @@ public class BukkitWorldInterface implements WorldInterface
@Override
public int getHighestYAt(int x, int z)
{
if (!this.trustHeightMap) {
return this.getMaxY() - 1;
}
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));
@@ -529,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;
}
@@ -578,7 +667,7 @@ public class BukkitWorldInterface implements WorldInterface
return 0;
}
return (byte) this.getChunk(x, z).getBlockEmittedLight(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
return (byte) this.getChunk(x, z).getBlockEmittedLight(Coordinates.blockToChunkRelative(x), Math.clamp(y + this.lightOffset, this.getMinY(), this.getMaxY() - 1), Coordinates.blockToChunkRelative(z));
}
@Override
@@ -588,7 +677,7 @@ public class BukkitWorldInterface implements WorldInterface
return (byte) (this.getDummyMaterialAt(y + 1).equals("minecraft:air") ? 15 : 0);
}
return (byte) this.getChunk(x, z).getBlockSkyLight(Coordinates.blockToChunkRelative(x), y, Coordinates.blockToChunkRelative(z));
return (byte) this.getChunk(x, z).getBlockSkyLight(Coordinates.blockToChunkRelative(x), Math.clamp(y + this.lightOffset, this.getMinY(), this.getMaxY() - 1), Coordinates.blockToChunkRelative(z));
}
@Override
@@ -604,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
@@ -626,7 +763,7 @@ public class BukkitWorldInterface implements WorldInterface
int maxY = this.getMaxY();
while (y < maxY) {
while (y + 1 < maxY) {
y++;
String material = Utils.unNamespaceKey(this.getMaterialAt(x, y, z)).toUpperCase();
@@ -25,14 +25,20 @@ import no.jckf.dhsupport.bukkit.handler.PlayerHandler;
import no.jckf.dhsupport.bukkit.handler.PluginMessageProxy;
import no.jckf.dhsupport.bukkit.handler.WorldHandler;
import no.jckf.dhsupport.core.DhSupport;
import no.jckf.dhsupport.core.configuration.Configuration;
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;
import javax.annotation.Nullable;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class DhSupportBukkitPlugin extends JavaPlugin
{
@@ -60,13 +66,27 @@ public class DhSupportBukkitPlugin extends JavaPlugin
}
this.getLogger().info("I am DHS " + pluginVersion + ".");
String gameVersion = this.getGameVersion();
this.dhSupport = new DhSupport(pluginVersion);
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());
this.metrics = new Metrics(this, 21843);
this.metrics.addCustomChart(new SingleLineChart("dh_users", () -> this.dhSupport.getPlayerConfigurations().size()));
this.metrics.addCustomChart(new MultiLineChart("dh_render_distance", () -> {
Map<String, Integer> renderDistances = new HashMap<>();
for (Configuration playerConfig : this.dhSupport.getPlayerConfigurations().values()) {
String rd = String.valueOf(playerConfig.getInt(DhsConfig.RENDER_DISTANCE));
renderDistances.put(rd, renderDistances.getOrDefault(rd, 0) + 1);
}
return renderDistances;
}));
this.loadDhsConfig();
@@ -97,6 +117,10 @@ public class DhSupportBukkitPlugin extends JavaPlugin
@Override
public void onDisable()
{
if (this.metrics != null) {
this.metrics.shutdown();
}
if (this.scheduler != null) {
this.scheduler.cancelTasks();
}
@@ -140,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);
}
@@ -21,6 +21,7 @@ package no.jckf.dhsupport.bukkit.handler;
import no.jckf.dhsupport.bukkit.BukkitWorldInterface;
import no.jckf.dhsupport.bukkit.DhSupportBukkitPlugin;
import no.jckf.dhsupport.core.configuration.DhsConfig;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
@@ -40,7 +41,7 @@ import java.util.Map;
public class WorldHandler implements Listener
{
protected static final String[] EVENT_METHOD_NAMES = { "blockList", "getBlocks", "getBlock" };
protected static final String[] EVENT_METHOD_NAMES = { "blockList", "getBlocks", "getBlock", "getLocation", "getChunk" };
protected DhSupportBukkitPlugin plugin;
@@ -137,7 +138,11 @@ public class WorldHandler implements Listener
try {
Object result = method.invoke(event);
if (result instanceof List) {
if (result instanceof Location location) {
list = List.of(location.getBlock());
} else if (result instanceof Chunk chunk) {
list = List.of(chunk.getBlock(0, 0, 0));
} else if (result instanceof List) {
list = (List<?>) result;
} else {
list = List.of(result);
@@ -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
);
}
}
}
}
@@ -56,6 +56,8 @@ public class DhSupport implements Configurable
{
protected String pluginVersion;
protected String gameVersion;
protected String dataDirectory;
protected Database database;
@@ -89,9 +91,10 @@ public class DhSupport implements Configurable
protected Map<UUID, PreGenerator> preGenerators = new HashMap<>();
public DhSupport(String pluginVersion)
public DhSupport(String pluginVersion, String gameVersion)
{
this.pluginVersion = pluginVersion;
this.gameVersion = gameVersion;
this.configuration = new Configuration();
@@ -244,6 +247,16 @@ public class DhSupport implements Configurable
return this.lodRepository;
}
public String getPluginVersion()
{
return this.pluginVersion;
}
public String getGameVersion()
{
return this.gameVersion;
}
public Configuration getConfig()
{
return this.configuration;
@@ -340,7 +353,11 @@ public class DhSupport implements Configurable
return this.queuedBuilders.get(key);
}
CompletableFuture<Lod> queued = this.getScheduler().runOnSeparateThread(builder::generate)
CompletableFuture<Lod> queued = this.getScheduler().runOnSeparateThread(() -> {
this.joinPauseState();
return builder.generate();
})
.thenApply((lod) -> {
this.queuedBuilders.remove(key);
@@ -376,8 +393,6 @@ public class DhSupport implements Configurable
protected CompletableFuture<LodModel> generateLod(UUID worldId, SectionPosition position, boolean manageChunks)
{
this.joinPauseState();
int worldX = Coordinates.sectionToBlock(position.getX());
int worldZ = Coordinates.sectionToBlock(position.getZ());
@@ -385,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...
@@ -421,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();
@@ -577,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;
}
@@ -612,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);
}
@@ -682,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();
@@ -698,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)
@@ -70,6 +70,8 @@ public abstract class DhsConfig
public static String BUILDER_TYPE = "builder_type";
public static String TRUST_HEIGHT_MAP = "trust_height_map";
public static String BUILDER_RESOLUTION = "builder_resolution";
public static String SCAN_TO_SEA_LEVEL= "scan_to_sea_level";
@@ -91,4 +93,6 @@ public abstract class DhsConfig
public static String MATERIAL_MAP = "material_map";
public static String AD_MESSAGE = "ad_message";
public static String LIGHT_OFFSET_HACK = "light_offset_hack";
}
@@ -102,6 +102,8 @@ public class LodRepository
statement.setInt(3, sectionZ);
try (ResultSet result = statement.executeQuery()) {
result.next();
byte[] data = result.getBytes("data");
if (data == null) {
@@ -135,6 +137,8 @@ public class LodRepository
statement.setInt(3, sectionZ);
try (ResultSet result = statement.executeQuery()) {
result.next();
return result.getInt(1) == 1;
}
} catch (SQLException exception) {
@@ -76,16 +76,9 @@ public class DataPoint extends DataObject
this.data |= ((long) startY & START_Y_MASK) << START_Y_OFFSET;
}
public int getStartY()
{
int raw = (int) ((this.data >> START_Y_OFFSET) & START_Y_MASK);
if ((raw & 0x0800) != 0) {
raw |= (int) ~START_Y_MASK;
}
return raw;
return (int) ((this.data >> START_Y_OFFSET) & START_Y_MASK);
}
public void setSkyLight(byte skyLight)
@@ -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
@@ -91,6 +90,15 @@ public class LodHandler
return;
}
if (this.dhSupport.isPaused() && !this.dhSupport.getLodRepository().lodExists(worldUuid, position.getX(), position.getZ())) {
ExceptionMessage exceptionMessage = new ExceptionMessage();
exceptionMessage.isResponseTo(requestMessage);
exceptionMessage.setTypeId(ExceptionMessage.TYPE_REQUEST_REJECTED);
exceptionMessage.setMessage("Server has paused Distant Horizons");
this.pluginMessageHandler.sendPluginMessage(requestMessage.getSender(), exceptionMessage);
return;
}
int worldX = Coordinates.sectionToBlock(position.getX());
int worldZ = Coordinates.sectionToBlock(position.getZ());
@@ -165,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,11 +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";
@@ -43,6 +49,46 @@ public class PlayerConfigHandler
public void register()
{
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(initRequestMessage.getSender());
NamespacedKey worldKey = NamespacedKey.fromString(initRequestMessage.getWorldKey());
// 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();
String levelKeyPrefix = dhsConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
String levelKey = world.getKey();
if (levelKeyPrefix != null) {
levelKey = levelKeyPrefix + 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(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());
@@ -54,21 +100,6 @@ public class PlayerConfigHandler
Configuration dhsConfig = world.getConfig();
String levelKeyPrefix = dhsConfig.getString(DhsConfig.LEVEL_KEY_PREFIX);
String levelKey = world.getKey();
if (levelKeyPrefix != null) {
levelKey = levelKeyPrefix + levelKey;
}
//this.dhSupport.info("Received DH config for " + player.getName() + " in " + levelKey);
LevelInitMessage levelInitResponse = new LevelInitMessage();
levelInitResponse.setServerKey(dhsConfig.getString(DhsConfig.SERVER_KEY, ""));
levelInitResponse.setLevelKey(levelKey);
levelInitResponse.setTime(System.currentTimeMillis());
this.pluginMessageHandler.sendPluginMessage(configMessage.getSender(), levelInitResponse);
Configuration clientConfig = configMessage.toConfiguration();
// This is not very flexible, but will do for now.
@@ -79,9 +110,19 @@ public class PlayerConfigHandler
Object clientValue = clientConfig.get(key);
Object keepValue = null;
if (key.equals(DhsConfig.DISTANT_GENERATION_ENABLED) && !dhUseIsAllowed) {
this.dhSupport.info("Player " + player.getName() + " has been denied the use of Distant Horizons through permissions.");
if (key.equals(DhsConfig.DISTANT_GENERATION_ENABLED)) {
if (dhUseIsAllowed) {
if (!(boolean) clientValue) {
player.sendMessage(ChatColor.GOLD + "This server supports Distant Horizons, but you have disabled Distant Generation in your DH settings.");
}
} else {
this.dhSupport.info("Player " + player.getName() + " has been denied the use of Distant Horizons through permissions.");
keepValue = false;
}
}
if (key.equals(DhsConfig.REAL_TIME_UPDATES_ENABLED) && !dhUseIsAllowed) {
keepValue = false;
}
@@ -36,9 +36,9 @@ public class PluginMessageHandler
private MessageTypeRegistry messageTypeRegistry;
public final String pluginChannel = "distant_horizons:message";
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;
@@ -184,10 +187,8 @@ public class FastOverworldBuilder extends LodBuilder
point.setHeight(thisStep);
point.setMappingId(id);
if (highWorldY + 1 < maxY) {
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
}
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
if (material.equals("minecraft:air") || material.equals("minecraft:void_air")) {
// Start by filling the top of the column with air, then jump down to the top layer.
@@ -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;
@@ -164,10 +175,8 @@ public class FullBuilder extends LodBuilder
point.setHeight(thisStep);
point.setMappingId(id);
if (highWorldY + 1 < maxY) {
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
}
point.setSkyLight(this.worldInterface.getSkyLightAt(worldX, highWorldY + 1, worldZ));
point.setBlockLight(this.worldInterface.getBlockLightAt(worldX, highWorldY + 1, worldZ));
if (material.equals("minecraft:air") || material.equals("minecraft:void_air")) {
// Start by filling the top of the column with air, then jump down to the top layer.
@@ -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);
+22 -6
View File
@@ -3,7 +3,7 @@
# Default: "{datadir}/data.sqlite"
database_path: "{datadir}/data.sqlite"
# Maximum supported render distance. Clients will be capped to this value.
# Maximum supported render distance, given as a radius in number of chunks. Clients will be capped to this value.
# Required.
# Default: 1024
render_distance: 1024
@@ -24,7 +24,7 @@ full_data_request_concurrency_limit: 20
# Default: true
real_time_updates_enabled: true
# How close to an update players have to be in order to receive it.
# How close to an update players have to be in order to receive it. Given as a radius in number of chunks.
# Required.
# Default: 128
real_time_update_radius: 128
@@ -34,7 +34,7 @@ real_time_update_radius: 128
# Default: true
login_data_sync_enabled: true
# How far away can a player request updates for during login sync?
# How far away can a player request updates for during login sync? Given as a radius in number of chunks.
# Required.
# Default: 128
login_data_sync_radius: 128
@@ -70,7 +70,7 @@ max_data_transfer_speed: 0
# How frequently (in seconds) should LODs be updated if world changes are detected?
# Required.
# Default: 5
# Default: 15
lod_refresh_interval: 15
# Select which LOD builder to use.
@@ -79,6 +79,13 @@ lod_refresh_interval: 15
# Default: FullBuilder
builder_type: FullBuilder
# Trust world height map.
# Disable if you're using a world that was generated with an external tool and your LODs come out broken.
# WARNING: This will make LOD building MUCH slower!
# Optional.
# Default: true
#trust_height_map: false
# Number of blocks to advance between each sample on the Y axis when generating LODs.
# A setting of 1 will sample every block, while 2 will sample every other block, and so on.
# CURRENTLY BROKEN. THIS VALUE IS IGNORED.
@@ -92,7 +99,7 @@ builder_resolution: 2
# Default: true
#include_non-colliding_top_layer: false
# If your server is accessible via multiple different addresses, consider setting a level key to let clients know
# If your server is accessible via multiple different addresses, consider setting a server key to let clients know
# that even though the address is different; it's the same server.
# This value must be GLOBALLY UNIQUE! No other server on planet Earth should have the same server key as you.
# Optional.
@@ -129,6 +136,14 @@ material_map:
iron_ore: stone
diamond_ore: stone
# Vertical distance to offset light data.
# This is a hack to work around SPIGOT-7498. If you're somehow on a 1.20.1 server _with_ this patch, set the value to 0.
# Optional.
# Default:
# - On 1.20.1: Minimum build height (-64 for vanilla worlds)
# - Any other version: 0
#light_offset_hack: 0
# World specific settings.
# You can override any of the above settings on a per-world basis.
worlds:
@@ -154,7 +169,7 @@ worlds:
# Default: true
#fast_underfill: false
# Limit LOD generation to within this area.
# Limit LOD generation to within this area. Alle three values are in blocks.
# Optional.
#border_center_x: 0
#border_center_z: 0
@@ -190,6 +205,7 @@ update_events:
# - org.bukkit.event.block.SpongeAbsorbEvent
# - org.bukkit.event.inventory.FurnaceBurnEvent
- org.bukkit.event.world.StructureGrowEvent
- org.bukkit.event.world.ChunkPopulateEvent
# Number of worker threads used to generate LODs.
# Required.
+4 -4
View File
@@ -54,7 +54,7 @@ public class DataPointTest extends TestCase
this.testHeight();
}
public void testPositiveStartY()
public void testStartY()
{
int randomNumber = (int) Math.floor(Math.random() * 100);
@@ -63,9 +63,9 @@ public class DataPointTest extends TestCase
assertEquals(randomNumber, this.dataPoint.getStartY());
}
public void testNegativeStartY()
public void testHighStartY()
{
int randomNumber = (int) Math.floor(0 - Math.random() * 100);
int randomNumber = 4000; // Chosen by fair dice roll. Guaranteed to be random.
this.dataPoint.setStartY(randomNumber);
@@ -74,7 +74,7 @@ public class DataPointTest extends TestCase
public void testStartYAgain()
{
this.testPositiveStartY();
this.testStartY();
}
public void testSkyLight()