|
|
|
@ -23,17 +23,16 @@ import java.lang.reflect.Field; |
|
|
|
import java.lang.reflect.Method; |
|
|
|
import java.lang.reflect.Method; |
|
|
|
import java.util.BitSet; |
|
|
|
import java.util.BitSet; |
|
|
|
import java.util.Arrays; |
|
|
|
import java.util.Arrays; |
|
|
|
|
|
|
|
import java.util.HashMap; |
|
|
|
|
|
|
|
|
|
|
|
public class FakeWater implements PacketListener { |
|
|
|
public class ChunkModifier implements PacketListener { |
|
|
|
private static final int FALLBACK_WATER_STATE_ID_1_21_1 = 12015; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final JavaPlugin plugin; |
|
|
|
private final JavaPlugin plugin; |
|
|
|
private final NamespacedKey keyX1, keyX2, keyZ1, keyZ2, keyType; |
|
|
|
private final NamespacedKey keyX1, keyX2, keyZ1, keyZ2, keyType; |
|
|
|
|
|
|
|
|
|
|
|
// Resolved once (reflection), cached afterwards
|
|
|
|
HashMap<String, Integer> blockStates = new HashMap<>(); |
|
|
|
private volatile Integer cachedWaterStateId; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public FakeWater(JavaPlugin plugin) { |
|
|
|
public ChunkModifier(JavaPlugin plugin) { |
|
|
|
this.plugin = plugin; |
|
|
|
this.plugin = plugin; |
|
|
|
this.keyX1 = new NamespacedKey(plugin, "borderx1"); |
|
|
|
this.keyX1 = new NamespacedKey(plugin, "borderx1"); |
|
|
|
this.keyX2 = new NamespacedKey(plugin, "borderx2"); |
|
|
|
this.keyX2 = new NamespacedKey(plugin, "borderx2"); |
|
|
|
@ -53,7 +52,21 @@ public class FakeWater implements PacketListener { |
|
|
|
PersistentDataContainer pdc = world.getPersistentDataContainer(); |
|
|
|
PersistentDataContainer pdc = world.getPersistentDataContainer(); |
|
|
|
|
|
|
|
|
|
|
|
String type = pdc.get(keyType, PersistentDataType.STRING); |
|
|
|
String type = pdc.get(keyType, PersistentDataType.STRING); |
|
|
|
if (!"flat_grass".equals(type)) return; |
|
|
|
|
|
|
|
|
|
|
|
String block; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch (type){ |
|
|
|
|
|
|
|
case "flat_grass": |
|
|
|
|
|
|
|
case "flat_sand": |
|
|
|
|
|
|
|
block = "WATER"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case "flat_hell": |
|
|
|
|
|
|
|
block = "LAVA"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Integer x1 = pdc.get(keyX1, PersistentDataType.INTEGER); |
|
|
|
Integer x1 = pdc.get(keyX1, PersistentDataType.INTEGER); |
|
|
|
Integer x2 = pdc.get(keyX2, PersistentDataType.INTEGER); |
|
|
|
Integer x2 = pdc.get(keyX2, PersistentDataType.INTEGER); |
|
|
|
@ -75,12 +88,11 @@ public class FakeWater implements PacketListener { |
|
|
|
int chunkZ = original.getZ(); |
|
|
|
int chunkZ = original.getZ(); |
|
|
|
if (chunkX >= minCX && chunkX <= maxCX && chunkZ >= minCZ && chunkZ <= maxCZ) return; |
|
|
|
if (chunkX >= minCX && chunkX <= maxCX && chunkZ >= minCZ && chunkZ <= maxCZ) return; |
|
|
|
|
|
|
|
|
|
|
|
Column fake = createFakeWaterColumn(world, original, resolveWaterStateId()); |
|
|
|
Column ghostColumn = createGhostColumn(world, original, resolveBlockStateID(block)); |
|
|
|
if (fake == null) return; |
|
|
|
if (ghostColumn == null) return; |
|
|
|
|
|
|
|
|
|
|
|
wrapper.setColumn(fake); |
|
|
|
wrapper.setColumn(ghostColumn); |
|
|
|
|
|
|
|
|
|
|
|
// Force full-bright lighting for fake chunks to avoid "goes dark after radius"
|
|
|
|
|
|
|
|
wrapper.setLightData(buildFullBrightLightData(world)); |
|
|
|
wrapper.setLightData(buildFullBrightLightData(world)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -88,16 +100,14 @@ public class FakeWater implements PacketListener { |
|
|
|
int sections = (world.getMaxHeight() - world.getMinHeight()) >> 4; |
|
|
|
int sections = (world.getMaxHeight() - world.getMinHeight()) >> 4; |
|
|
|
if (sections <= 0) sections = 24; |
|
|
|
if (sections <= 0) sections = 24; |
|
|
|
|
|
|
|
|
|
|
|
// 1.17+ light data uses sectionCount + 2 (one below min, one above max)
|
|
|
|
|
|
|
|
int lightCount = sections + 2; |
|
|
|
int lightCount = sections + 2; |
|
|
|
|
|
|
|
|
|
|
|
// Each light array entry is 16*16*16 nibbles = 4096 nibbles = 2048 bytes
|
|
|
|
|
|
|
|
byte[] fullSky = new byte[2048]; |
|
|
|
byte[] fullSky = new byte[2048]; |
|
|
|
Arrays.fill(fullSky, (byte) 0xFF); // 0xF in every nibble => light level 15
|
|
|
|
Arrays.fill(fullSky, (byte) 0xFF); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
byte[] noBlock = new byte[2048]; |
|
|
|
|
|
|
|
|
|
|
|
byte[] noBlock = new byte[2048]; // default 0 => block light 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If your dimension can ever be no-skylight, handle it:
|
|
|
|
|
|
|
|
boolean hasSkyLight = world.getEnvironment() != World.Environment.NETHER |
|
|
|
boolean hasSkyLight = world.getEnvironment() != World.Environment.NETHER |
|
|
|
&& world.getEnvironment() != World.Environment.THE_END; // adjust if you have custom dims
|
|
|
|
&& world.getEnvironment() != World.Environment.THE_END; // adjust if you have custom dims
|
|
|
|
|
|
|
|
|
|
|
|
@ -142,7 +152,7 @@ public class FakeWater implements PacketListener { |
|
|
|
return ld; |
|
|
|
return ld; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Column createFakeWaterColumn(World world, Column original, int waterStateId) { |
|
|
|
private Column createGhostColumn(World world, Column original, int blockID) { |
|
|
|
int sections = (world.getMaxHeight() - world.getMinHeight()) >> 4; |
|
|
|
int sections = (world.getMaxHeight() - world.getMinHeight()) >> 4; |
|
|
|
if (sections <= 0) sections = 24; |
|
|
|
if (sections <= 0) sections = 24; |
|
|
|
|
|
|
|
|
|
|
|
@ -178,12 +188,11 @@ public class FakeWater implements PacketListener { |
|
|
|
chunks[sectionIndex] = section; |
|
|
|
chunks[sectionIndex] = section; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Section 0 == world Y -64..-49. Set only local y=0 and y=1 to water.
|
|
|
|
|
|
|
|
if (chunks[0] instanceof Chunk_v1_18 section0) { |
|
|
|
if (chunks[0] instanceof Chunk_v1_18 section0) { |
|
|
|
for (int localY = 0; localY <= 1; localY++) { |
|
|
|
for (int localY = 0; localY <= 1; localY++) { |
|
|
|
for (int x = 0; x < 16; x++) { |
|
|
|
for (int x = 0; x < 16; x++) { |
|
|
|
for (int z = 0; z < 16; z++) { |
|
|
|
for (int z = 0; z < 16; z++) { |
|
|
|
section0.set(x, localY, z, waterStateId); |
|
|
|
section0.set(x, localY, z, blockID); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -199,35 +208,31 @@ public class FakeWater implements PacketListener { |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private int resolveWaterStateId() { |
|
|
|
private int resolveBlockStateID(String name) { |
|
|
|
Integer cached = cachedWaterStateId; |
|
|
|
Integer stateID = blockStates.get(name); |
|
|
|
if (cached != null) return cached; |
|
|
|
if (stateID != null) return stateID; |
|
|
|
|
|
|
|
|
|
|
|
int resolved = FALLBACK_WATER_STATE_ID_1_21_1; |
|
|
|
int resolved = 0; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
Object waterState = resolveNmsWaterBlockState(); |
|
|
|
Object blockState = resolveNmsBlockState(name); |
|
|
|
if (waterState != null) { |
|
|
|
if (blockState != null) { |
|
|
|
Integer id = tryGetBlockStateIdViaBlockGetId(waterState); |
|
|
|
Integer id = tryGetBlockStateIdViaBlockGetId(blockState); |
|
|
|
if (id == null) { |
|
|
|
|
|
|
|
id = tryGetBlockStateIdViaBuiltInRegistry(waterState); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (id != null && id > 0) { |
|
|
|
if (id != null && id > 0) { |
|
|
|
resolved = id; |
|
|
|
resolved = id; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (ReflectiveOperationException ignored) { |
|
|
|
} catch (ReflectiveOperationException ignored) { |
|
|
|
// fallback
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
cachedWaterStateId = resolved; |
|
|
|
blockStates.put(name, resolved); |
|
|
|
plugin.getLogger().info("[FakeWater] Using water block-state id: " + resolved); |
|
|
|
|
|
|
|
return resolved; |
|
|
|
return resolved; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static Object resolveNmsWaterBlockState() throws ReflectiveOperationException { |
|
|
|
private static Object resolveNmsBlockState(String name) throws ReflectiveOperationException { |
|
|
|
Class<?> blocksClass = Class.forName("net.minecraft.world.level.block.Blocks"); |
|
|
|
Class<?> blocksClass = Class.forName("net.minecraft.world.level.block.Blocks"); |
|
|
|
Field waterField = blocksClass.getField("WATER"); |
|
|
|
Field waterField = blocksClass.getField(name); |
|
|
|
Object waterBlock = waterField.get(null); |
|
|
|
Object waterBlock = waterField.get(null); |
|
|
|
|
|
|
|
|
|
|
|
Method defaultBlockState = waterBlock.getClass().getMethod("defaultBlockState"); |
|
|
|
Method defaultBlockState = waterBlock.getClass().getMethod("defaultBlockState"); |
|
|
|
@ -248,22 +253,4 @@ public class FakeWater implements PacketListener { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static Integer tryGetBlockStateIdViaBuiltInRegistry(Object blockState) throws ReflectiveOperationException { |
|
|
|
|
|
|
|
Class<?> blockStateClass = Class.forName("net.minecraft.world.level.block.state.BlockState"); |
|
|
|
|
|
|
|
Class<?> builtInRegistriesClass = Class.forName("net.minecraft.core.registries.BuiltInRegistries"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Field blockStateRegistryField = builtInRegistriesClass.getField("BLOCK_STATE"); |
|
|
|
|
|
|
|
Object blockStateRegistry = blockStateRegistryField.get(null); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Method getId = blockStateRegistry.getClass().getMethod("getId", Object.class); |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
// Prefer exact signature if present: getId(BlockState)
|
|
|
|
|
|
|
|
Method exact = blockStateRegistry.getClass().getMethod("getId", blockStateClass); |
|
|
|
|
|
|
|
Object idObj = exact.invoke(blockStateRegistry, blockState); |
|
|
|
|
|
|
|
return (idObj instanceof Integer i) ? i : null; |
|
|
|
|
|
|
|
} catch (NoSuchMethodException ignored) { |
|
|
|
|
|
|
|
Object idObj = getId.invoke(blockStateRegistry, blockState); |
|
|
|
|
|
|
|
return (idObj instanceof Integer i) ? i : null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |