diff --git a/build.gradle b/build.gradle index 083b0a188..adf4ede09 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ allprojects { apply plugin: 'java' group = 'us.dynmap' - version = '3.9-SNAPSHOT' + version = '26.1.2-beta.10' } @@ -47,7 +47,7 @@ subprojects { apply plugin: 'maven-publish' // Set Java version - paperweight modules define their own Java version - if (project.name != 'bukkit-helper-121-11') { + if (project.name != 'bukkit-helper-121-11' && project.name != 'bukkit-helper-26-1-2') { java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/bukkit-helper-26-1-2/build.gradle b/bukkit-helper-26-1-2/build.gradle new file mode 100644 index 000000000..341f34f77 --- /dev/null +++ b/bukkit-helper-26-1-2/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'io.papermc.paperweight.userdev' +} + +eclipse { + project { + name = "Dynmap(Spigot-26.1.2)" + } +} + +description = 'bukkit-helper-26.1.2' + +// Minecraft 26.1+ Paper requires Java 25 at runtime, so the userdev setup +// (paperclip patching) needs to launch under Java 25 as well. The Shadow +// plugin in this build runs on JDK 21 and cannot read Java 25 (major 69) +// bytecode, so emit Java 21 bytecode here while keeping the Java 25 toolchain +// for compilation against Paper's API. `options.release` (rather than +// source/targetCompatibility) keeps the org.gradle.jvm.version attribute at +// 25 so paper-api (which requires JVM 25) still resolves. +// +// FIXME: drop the toolchain-25 + release-21 + JVM-25-attribute dance once the +// Shadow plugin can run on JDK 25 (Gradle would then run on 25 too and read +// major-69 class files natively). +java { + toolchain.languageVersion = JavaLanguageVersion.of(25) +} +tasks.named('compileJava').configure { + options.release = 21 +} + +// `options.release = 21` would normally narrow the resolution attribute to +// JVM 21, but paper-api 26.1.2 declares JVM 25 in its module metadata. Force +// the consumer attribute back to 25 so dependency resolution succeeds; the +// emitted bytecode is still Java 21 thanks to `--release`. +[configurations.compileClasspath, configurations.runtimeClasspath].each { cfg -> + cfg.attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25) + } +} + +dependencies { + implementation project(':bukkit-helper') + implementation project(':dynmap-api') + implementation project(path: ':DynmapCore', configuration: 'shadow') + // Paper publishes 26.1.2 dev bundles as "26.1.2.build.-stable" (no -R0.1-SNAPSHOT for the new version scheme) + paperweight.paperDevBundle("26.1.2.build.57-stable") +} + +// Paper 26.1+ ships Mojang-mapped at runtime; the dev bundle no longer +// provides reobf-to-Spigot mappings, so the reobfJar task can't run. Disable +// it. Consumers (spigot) must then pull from the `runtimeElements` +// configuration explicitly because paperweight registers `reobf` as the +// preferred runtime variant. +tasks.named('reobfJar').configure { enabled = false } diff --git a/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/BukkitVersionHelperSpigot26_1_2.java b/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/BukkitVersionHelperSpigot26_1_2.java new file mode 100644 index 000000000..cb5f56a95 --- /dev/null +++ b/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/BukkitVersionHelperSpigot26_1_2.java @@ -0,0 +1,546 @@ +package org.dynmap.bukkit.helper.v26_1_2; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.dynmap.DynmapChunk; +import org.dynmap.Log; +import org.dynmap.bukkit.helper.BukkitMaterial; +import org.dynmap.bukkit.helper.BukkitVersionHelper; +import org.dynmap.bukkit.helper.BukkitWorld; +import org.dynmap.bukkit.helper.BukkitVersionHelperGeneric.TexturesPayload; +import org.dynmap.renderer.DynmapBlockState; +import org.dynmap.utils.MapChunkCache; +import org.dynmap.utils.Polygon; + +import com.google.common.collect.Iterables; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.resources.Identifier; +import net.minecraft.nbt.Tag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * Helper for isolation of bukkit version specific issues + */ +public class BukkitVersionHelperSpigot26_1_2 extends BukkitVersionHelper { + + // CraftBukkit class references (loaded via reflection for Paper/Spigot compatibility) + private static Class craftWorldClass; + private static Class craftChunkClass; + private static Class craftPlayerClass; + private static Method craftWorldGetHandle; + private static Method craftWorldGetMinHeight; + private static Method craftChunkGetHandle; + private static Method craftPlayerGetProfile; + private static boolean initialized = false; + + private static synchronized void initCraftBukkitClasses() { + if (initialized) return; + initialized = true; + + // Paper 1.20.5+ uses unversioned org.bukkit.craftbukkit. We deliberately + // don't carry a Spigot-versioned fallback here: the real package name on + // Spigot 26.1.2 hasn't been verified, and a wrong fallback would mask + // the genuine ClassNotFoundException with a misleading "fell through to + // Spigot" path. If a Spigot-versioned helper is ever needed, add it + // after confirming the package against an actual Spigot 26.1.2 build. + String prefix = "org.bukkit.craftbukkit"; + try { + craftWorldClass = Class.forName(prefix + ".CraftWorld"); + craftChunkClass = Class.forName(prefix + ".CraftChunk"); + craftPlayerClass = Class.forName(prefix + ".entity.CraftPlayer"); + + craftWorldGetHandle = craftWorldClass.getMethod("getHandle"); + craftWorldGetMinHeight = craftWorldClass.getMethod("getMinHeight"); + craftChunkGetHandle = craftChunkClass.getMethod("getHandle", ChunkStatus.class); + craftPlayerGetProfile = craftPlayerClass.getMethod("getProfile"); + + Log.info("[Dynmap] Using CraftBukkit package: " + prefix); + } catch (ClassNotFoundException | NoSuchMethodException e) { + Log.severe("[Dynmap] Failed to find CraftBukkit classes!"); + } + } + + @Override + public boolean isUnsafeAsync() { + return false; + } + + /** + * Get block short name list + */ + @Override + public String[] getBlockNames() { + IdMapper bsids = Block.BLOCK_STATE_REGISTRY; + Block baseb = null; + Iterator iter = bsids.iterator(); + ArrayList names = new ArrayList(); + while (iter.hasNext()) { + BlockState bs = iter.next(); + Block b = bs.getBlock(); + // If this is new block vs last, it's the base block state + if (b != baseb) { + baseb = b; + continue; + } + Identifier id = BuiltInRegistries.BLOCK.getKey(b); + String bn = id.toString(); + if (bn != null) { + names.add(bn); + Log.info("block=" + bn); + } + } + return names.toArray(new String[0]); + } + + private static Object biomeRegistry = null; + private static java.lang.reflect.Method getKeyMethod = null; + private static java.lang.reflect.Method getIdMethod = null; + + private static synchronized Object getBiomeReg() { + if (biomeRegistry == null) { + biomeRegistry = MinecraftServer.getServer().registryAccess().lookup(Registries.BIOME).orElseThrow(); + // Cache reflection methods + try { + getKeyMethod = biomeRegistry.getClass().getMethod("getKey", Object.class); + getIdMethod = biomeRegistry.getClass().getMethod("getId", Object.class); + } catch (Exception e) { + Log.severe("Failed to get biome registry methods: " + e.getMessage()); + } + } + return biomeRegistry; + } + + @SuppressWarnings("unchecked") + private static Iterator getBiomeIterator() { + return ((Iterable) getBiomeReg()).iterator(); + } + + private static int getBiomeId(Biome biome) { + try { + getBiomeReg(); // ensure methods are cached + return (Integer) getIdMethod.invoke(biomeRegistry, biome); + } catch (Exception e) { + return -1; + } + } + + private static Identifier getBiomeKey(Biome biome) { + try { + getBiomeReg(); // ensure methods are cached + return (Identifier) getKeyMethod.invoke(biomeRegistry, biome); + } catch (Exception e) { + Log.warning("Failed to get biome key: " + e.getMessage()); + return null; + } + } + + private Object[] biomelist; + /** + * Get list of defined biomebase objects + */ + @Override + public Object[] getBiomeBaseList() { + if (biomelist == null) { + biomelist = new Biome[256]; + Iterator iter = getBiomeIterator(); + while (iter.hasNext()) { + Biome b = iter.next(); + int bidx = getBiomeId(b); + if (bidx >= biomelist.length) { + biomelist = Arrays.copyOf(biomelist, bidx + biomelist.length); + } + biomelist[bidx] = b; + } + } + return biomelist; + } + + /** Get ID from biomebase */ + @Override + public int getBiomeBaseID(Object bb) { + return getBiomeId((Biome)bb); + } + + public static IdentityHashMap dataToState; + + /** + * Initialize block states (org.dynmap.blockstate.DynmapBlockState) + */ + @Override + public void initializeBlockStates() { + dataToState = new IdentityHashMap(); + HashMap lastBlockState = new HashMap(); + IdMapper bsids = Block.BLOCK_STATE_REGISTRY; + Block baseb = null; + Iterator iter = bsids.iterator(); + ArrayList names = new ArrayList(); + + // Loop through block data states + DynmapBlockState.Builder bld = new DynmapBlockState.Builder(); + while (iter.hasNext()) { + BlockState bd = iter.next(); + Block b = bd.getBlock(); + Identifier id = BuiltInRegistries.BLOCK.getKey(b); + String bname = id.toString(); + DynmapBlockState lastbs = lastBlockState.get(bname); // See if we have seen this one + int idx = 0; + if (lastbs != null) { // Yes + idx = lastbs.getStateCount(); // Get number of states so far, since this is next + } + // Build state name + String sb = ""; + String fname = bd.toString(); + int off1 = fname.indexOf('['); + if (off1 >= 0) { + int off2 = fname.indexOf(']'); + sb = fname.substring(off1+1, off2); + } + int lightAtten = bd.getLightDampening(); // MC 26.1: renamed from getLightBlock() + // Fill in base attributes + bld.setBaseState(lastbs).setStateIndex(idx).setBlockName(bname).setStateName(sb).setAttenuatesLight(lightAtten); + if (bd.isSolid()) { bld.setSolid(); } + if (bd.isAir()) { bld.setAir(); } + if (bd.is(BlockTags.OVERWORLD_NATURAL_LOGS)) { bld.setLog(); } + if (bd.is(BlockTags.LEAVES)) { bld.setLeaves(); } + if (!bd.getFluidState().isEmpty() && !(bd.getBlock() instanceof LiquidBlock)) { + bld.setWaterlogged(); + } + DynmapBlockState dbs = bld.build(); // Build state + + dataToState.put(bd, dbs); + lastBlockState.put(bname, (lastbs == null) ? dbs : lastbs); + Log.verboseinfo("blk=" + bname + ", idx=" + idx + ", state=" + sb + ", waterlogged=" + dbs.isWaterlogged()); + } + } + /** + * Create chunk cache for given chunks of given world + * @param dw - world + * @param chunks - chunk list + * @return cache + */ + @Override + public MapChunkCache getChunkCache(BukkitWorld dw, List chunks) { + MapChunkCache26_1_2 c = new MapChunkCache26_1_2(gencache); + c.setChunks(dw, chunks); + return c; + } + + /** + * Get biome base water multiplier + */ + @Override + public int getBiomeBaseWaterMult(Object bb) { + Biome biome = (Biome) bb; + return biome.getWaterColor(); + } + + /** Get temperature from biomebase */ + @Override + public float getBiomeBaseTemperature(Object bb) { + return ((Biome)bb).getBaseTemperature(); + } + + /** Get humidity from biomebase */ + @Override + public float getBiomeBaseHumidity(Object bb) { + String vals = ((Biome)bb).climateSettings.toString(); + float humidity = 0.5F; + int idx = vals.indexOf("downfall="); + if (idx >= 0) { + humidity = Float.parseFloat(vals.substring(idx+9, vals.indexOf(']', idx))); + } + return humidity; + } + + @Override + public Polygon getWorldBorder(World world) { + Polygon p = null; + WorldBorder wb = world.getWorldBorder(); + if (wb != null) { + Location c = wb.getCenter(); + double size = wb.getSize(); + if ((size > 1) && (size < 1E7)) { + size = size / 2; + p = new Polygon(); + p.addVertex(c.getX()-size, c.getZ()-size); + p.addVertex(c.getX()+size, c.getZ()-size); + p.addVertex(c.getX()+size, c.getZ()+size); + p.addVertex(c.getX()-size, c.getZ()+size); + } + } + return p; + } + // Send title/subtitle to user + public void sendTitleText(Player p, String title, String subtitle, int fadeInTicks, int stayTicks, int fadeOutTIcks) { + if (p != null) { + p.sendTitle(title, subtitle, fadeInTicks, stayTicks, fadeOutTIcks); + } + } + + /** + * Get material map by block ID + */ + @Override + public BukkitMaterial[] getMaterialList() { + return new BukkitMaterial[4096]; // Not used + } + + @Override + public void unloadChunkNoSave(World w, Chunk c, int cx, int cz) { + Log.severe("unloadChunkNoSave not implemented"); + } + + private String[] biomenames; + @Override + public String[] getBiomeNames() { + if (biomenames == null) { + biomenames = new String[256]; + Iterator iter = getBiomeIterator(); + while (iter.hasNext()) { + Biome b = iter.next(); + int bidx = getBiomeId(b); + if (bidx >= biomenames.length) { + biomenames = Arrays.copyOf(biomenames, bidx + biomenames.length); + } + biomenames[bidx] = b.toString(); + } + } + return biomenames; + } + + @Override + public String getStateStringByCombinedId(int blkid, int meta) { + Log.severe("getStateStringByCombinedId not implemented"); + return null; + } + @Override + /** Get ID string from biomebase */ + public String getBiomeBaseIDString(Object bb) { + Identifier key = getBiomeKey((Biome)bb); + return key != null ? key.getPath() : ""; + } + @Override + public String getBiomeBaseResourceLocsation(Object bb) { + Identifier key = getBiomeKey((Biome)bb); + return key != null ? key.toString() : ""; + } + + @Override + public Object getUnloadQueue(World world) { + Log.warning("getUnloadQueue not implemented yet"); + return null; + } + + @Override + public boolean isInUnloadQueue(Object unloadqueue, int x, int z) { + Log.warning("isInUnloadQueue not implemented yet"); + return false; + } + + @Override + public Object[] getBiomeBaseFromSnapshot(ChunkSnapshot css) { + Log.warning("getBiomeBaseFromSnapshot not implemented yet"); + return new Object[256]; + } + + @Override + public long getInhabitedTicks(Chunk c) { + initCraftBukkitClasses(); + try { + Object handle = craftChunkGetHandle.invoke(c, ChunkStatus.FULL); + return ((LevelChunk)handle).getInhabitedTime(); + } catch (Exception e) { + Log.warning("getInhabitedTicks failed: " + e.getMessage()); + return 0; + } + } + + @Override + public Map getTileEntitiesForChunk(Chunk c) { + initCraftBukkitClasses(); + try { + Object handle = craftChunkGetHandle.invoke(c, ChunkStatus.FULL); + return ((LevelChunk)handle).getBlockEntities(); + } catch (Exception e) { + Log.warning("getTileEntitiesForChunk failed: " + e.getMessage()); + return new HashMap<>(); + } + } + + @Override + public int getTileEntityX(Object te) { + BlockEntity blockent = (BlockEntity) te; + return blockent.getBlockPos().getX(); + } + + @Override + public int getTileEntityY(Object te) { + BlockEntity blockent = (BlockEntity) te; + return blockent.getBlockPos().getY(); + } + + @Override + public int getTileEntityZ(Object te) { + BlockEntity blockent = (BlockEntity) te; + return blockent.getBlockPos().getZ(); + } + + @Override + public Object readTileEntityNBT(Object te, World w) { + initCraftBukkitClasses(); + try { + BlockEntity blockent = (BlockEntity) te; + Object handle = craftWorldGetHandle.invoke(w); + Method registryAccess = handle.getClass().getMethod("registryAccess"); + HolderLookup.Provider registry = (HolderLookup.Provider) registryAccess.invoke(handle); + return blockent.saveCustomOnly(registry); + } catch (Exception e) { + Log.warning("readTileEntityNBT failed: " + e.getMessage()); + return null; + } + } + + @Override + public Object getFieldValue(Object nbt, String field) { + CompoundTag rec = (CompoundTag) nbt; + Tag val = rec.get(field); + if(val == null) return null; + if(val instanceof ByteTag) { + return ((ByteTag)val).byteValue(); + } + else if(val instanceof ShortTag) { + return ((ShortTag)val).shortValue(); + } + else if(val instanceof IntTag) { + return ((IntTag)val).intValue(); + } + else if(val instanceof LongTag) { + return ((LongTag)val).longValue(); + } + else if(val instanceof FloatTag) { + return ((FloatTag)val).floatValue(); + } + else if(val instanceof DoubleTag) { + return ((DoubleTag)val).doubleValue(); + } + else if(val instanceof ByteArrayTag) { + return ((ByteArrayTag)val).getAsByteArray(); + } + else if(val instanceof StringTag) { + return val.asString().orElse(""); + } + else if(val instanceof IntArrayTag) { + return ((IntArrayTag)val).getAsIntArray(); + } + return null; + } + + @Override + public Player[] getOnlinePlayers() { + Collection p = Bukkit.getServer().getOnlinePlayers(); + return p.toArray(new Player[0]); + } + + @Override + public double getHealth(Player p) { + return p.getHealth(); + } + + private static final Gson gson = new GsonBuilder().create(); + + /** + * Get skin URL for player + * @param player + */ + @Override + public String getSkinURL(Player player) { + initCraftBukkitClasses(); + String url = null; + try { + GameProfile profile = (GameProfile) craftPlayerGetProfile.invoke(player); + if (profile != null) { + PropertyMap pm = profile.properties(); + if (pm != null) { + Collection txt = pm.get("textures"); + Property textureProperty = Iterables.getFirst(pm.get("textures"), null); + if (textureProperty != null) { + String val = textureProperty.value(); + if (val != null) { + TexturesPayload result = null; + try { + String json = new String(Base64.getDecoder().decode(val), StandardCharsets.UTF_8); + result = gson.fromJson(json, TexturesPayload.class); + } catch (JsonParseException e) { + } catch (IllegalArgumentException x) { + Log.warning("Malformed response from skin URL check: " + val); + } + if ((result != null) && (result.textures != null) && (result.textures.containsKey("SKIN"))) { + url = result.textures.get("SKIN").url; + } + } + } + } + } + } catch (Exception e) { + Log.warning("getSkinURL failed: " + e.getMessage()); + } + return url; + } + // Get minY for world + @Override + public int getWorldMinY(World w) { + initCraftBukkitClasses(); + try { + return (Integer) craftWorldGetMinHeight.invoke(w); + } catch (Exception e) { + Log.warning("getWorldMinY failed: " + e.getMessage()); + return 0; + } + } + @Override + public boolean useGenericCache() { + return true; + } + +} diff --git a/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/MapChunkCache26_1_2.java b/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/MapChunkCache26_1_2.java new file mode 100644 index 000000000..08b7e3293 --- /dev/null +++ b/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/MapChunkCache26_1_2.java @@ -0,0 +1,181 @@ +package org.dynmap.bukkit.helper.v26_1_2; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSpecialEffects; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.dynmap.DynmapChunk; +import org.dynmap.Log; +import org.dynmap.bukkit.helper.BukkitWorld; +import org.dynmap.common.BiomeMap; +import org.dynmap.common.chunk.GenericChunk; +import org.dynmap.common.chunk.GenericChunkCache; +import org.dynmap.common.chunk.GenericMapChunkCache; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * Container for managing chunks - dependent upon using chunk snapshots, since rendering is off server thread + */ +public class MapChunkCache26_1_2 extends GenericMapChunkCache { + private World w; + + // CraftBukkit reflection support for Paper/Spigot compatibility + private static Class craftWorldClass; + private static Class craftServerClass; + private static Method craftWorldGetHandle; + private static Method craftWorldIsChunkLoaded; + private static Method craftServerGetServer; + private static boolean initialized = false; + + private static synchronized void initReflection() { + if (initialized) return; + initialized = true; + + // Paper 1.20.5+ uses unversioned org.bukkit.craftbukkit. See the matching + // note in BukkitVersionHelperSpigot26_1_2#initCraftBukkitClasses about + // why we don't carry a Spigot-versioned fallback here. + String prefix = "org.bukkit.craftbukkit"; + try { + craftWorldClass = Class.forName(prefix + ".CraftWorld"); + craftServerClass = Class.forName(prefix + ".CraftServer"); + + craftWorldGetHandle = craftWorldClass.getMethod("getHandle"); + craftWorldIsChunkLoaded = craftWorldClass.getMethod("isChunkLoaded", int.class, int.class); + craftServerGetServer = craftServerClass.getMethod("getServer"); + + Log.info("[Dynmap] MapChunkCache using CraftBukkit package: " + prefix); + } catch (ClassNotFoundException | NoSuchMethodException e) { + Log.severe("[Dynmap] MapChunkCache failed to find CraftBukkit classes!"); + } + } + + private ServerLevel getServerLevel(World world) { + initReflection(); + try { + return (ServerLevel) craftWorldGetHandle.invoke(world); + } catch (Exception e) { + Log.severe("Failed to get ServerLevel: " + e.getMessage()); + return null; + } + } + + private boolean isChunkLoaded(World world, int x, int z) { + initReflection(); + try { + return (Boolean) craftWorldIsChunkLoaded.invoke(world, x, z); + } catch (Exception e) { + Log.warning("Failed to check chunk loaded status: " + e.getMessage()); + return false; + } + } + + private MinecraftServer getMinecraftServer() { + initReflection(); + try { + return (MinecraftServer) craftServerGetServer.invoke(Bukkit.getServer()); + } catch (Exception e) { + Log.severe("Failed to get MinecraftServer: " + e.getMessage()); + return null; + } + } + + /** + * Construct empty cache + */ + public MapChunkCache26_1_2(GenericChunkCache cc) { + super(cc); + } + + @Override + protected Supplier getLoadedChunkAsync(DynmapChunk chunk) { + ServerLevel serverLevel = getServerLevel(w); + MinecraftServer server = getMinecraftServer(); + if (serverLevel == null || server == null) { + return () -> null; + } + + CompletableFuture> chunkData = CompletableFuture.supplyAsync(() -> { + LevelChunk c = serverLevel.getChunkIfLoaded(chunk.x, chunk.z); + if (c == null || !c.loaded) { + return Optional.empty(); + } + return Optional.of(SerializableChunkData.copyOf(serverLevel, c)); + }, server); + return () -> chunkData.join().map(SerializableChunkData::write).map(NBT.NBTCompound::new).map(this::parseChunkFromNBT).orElse(null); + } + + protected GenericChunk getLoadedChunk(DynmapChunk chunk) { + ServerLevel serverLevel = getServerLevel(w); + if (serverLevel == null) return null; + + if (!isChunkLoaded(w, chunk.x, chunk.z)) return null; + LevelChunk c = serverLevel.getChunkIfLoaded(chunk.x, chunk.z); + if (c == null || !c.loaded) return null; + SerializableChunkData chunkData = SerializableChunkData.copyOf(serverLevel, c); + CompoundTag nbt = chunkData.write(); + return nbt != null ? parseChunkFromNBT(new NBT.NBTCompound(nbt)) : null; + } + + @Override + protected Supplier loadChunkAsync(DynmapChunk chunk) { + ServerLevel serverLevel = getServerLevel(w); + if (serverLevel == null) { + return () -> null; + } + + CompletableFuture> genericChunk = serverLevel.getChunkSource().chunkMap.read(new ChunkPos(chunk.x, chunk.z)); + return () -> genericChunk.join().map(NBT.NBTCompound::new).map(this::parseChunkFromNBT).orElse(null); + } + + protected GenericChunk loadChunk(DynmapChunk chunk) { + ServerLevel serverLevel = getServerLevel(w); + if (serverLevel == null) return null; + + CompoundTag nbt = null; + ChunkPos cc = new ChunkPos(chunk.x, chunk.z); + GenericChunk gc = null; + try { + nbt = serverLevel + .getChunkSource() + .chunkMap + .read(cc) + .join().get(); + } catch (CancellationException cx) { + } catch (NoSuchElementException snex) { + } + if (nbt != null) { + gc = parseChunkFromNBT(new NBT.NBTCompound(nbt)); + } + return gc; + } + + public void setChunks(BukkitWorld dw, List chunks) { + this.w = dw.getWorld(); + super.setChunks(dw, chunks); + } + + @Override + public int getFoliageColor(BiomeMap bm, int[] colormap, int x, int z) { + return bm.getBiomeObject().map(Biome::getSpecialEffects).flatMap(BiomeSpecialEffects::foliageColorOverride).orElse(colormap[bm.biomeLookup()]); + } + + @Override + public int getGrassColor(BiomeMap bm, int[] colormap, int x, int z) { + BiomeSpecialEffects effects = bm.getBiomeObject().map(Biome::getSpecialEffects).orElse(null); + if (effects == null) return colormap[bm.biomeLookup()]; + return effects.grassColorModifier().modifyColor(x, z, effects.grassColorOverride().orElse(colormap[bm.biomeLookup()])); + } +} diff --git a/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/NBT.java b/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/NBT.java new file mode 100644 index 000000000..fe2b7ea22 --- /dev/null +++ b/bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/NBT.java @@ -0,0 +1,143 @@ +package org.dynmap.bukkit.helper.v26_1_2; + +import org.dynmap.common.chunk.GenericBitStorage; +import org.dynmap.common.chunk.GenericNBTCompound; +import org.dynmap.common.chunk.GenericNBTList; + +import java.util.Optional; +import java.util.Set; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.util.SimpleBitStorage; + +public class NBT { + + public static class NBTCompound implements GenericNBTCompound { + private final CompoundTag obj; + public NBTCompound(CompoundTag t) { + this.obj = t; + } + @Override + public Set getAllKeys() { + return obj.keySet(); + } + @Override + public boolean contains(String s) { + return obj.contains(s); + } + @Override + public boolean contains(String s, int i) { + Tag base = obj.get(s); + if (base == null) + return false; + byte type = base.getId(); + if (type == i) + return true; + else if (i != TAG_ANY_NUMERIC) + return false; + return type == TAG_BYTE || type == TAG_SHORT || type == TAG_INT || type == TAG_LONG || type == TAG_FLOAT + || type == TAG_DOUBLE; + } + @Override + public byte getByte(String s) { + return obj.getByteOr(s, (byte)0); + } + @Override + public short getShort(String s) { + return obj.getShortOr(s, (short)0); + } + @Override + public int getInt(String s) { + return obj.getIntOr(s, 0); + } + @Override + public long getLong(String s) { + return obj.getLongOr(s, 0L); + } + @Override + public float getFloat(String s) { + return obj.getFloatOr(s, 0.0f); + } + @Override + public double getDouble(String s) { + return obj.getDoubleOr(s, 0.0); + } + @Override + public String getString(String s) { + return obj.getStringOr(s, ""); + } + @Override + public byte[] getByteArray(String s) { + Optional byteArr = obj.getByteArray(s); + return byteArr.orElseGet(() -> new byte[0]); + } + @Override + public int[] getIntArray(String s) { + Optional intArr = obj.getIntArray(s); + return intArr.orElseGet(() -> new int[0]); + } + @Override + public long[] getLongArray(String s) { + Optional longArr = obj.getLongArray(s); + return longArr.orElseGet(() -> new long[0]); + } + @Override + public GenericNBTCompound getCompound(String s) { + return new NBTCompound(obj.getCompoundOrEmpty(s)); + } + @Override + public GenericNBTList getList(String s, int i) { + return new NBTList(obj.getListOrEmpty(s)); + } + @Override + public boolean getBoolean(String s) { + return getByte(s) != 0; + } + @Override + public String getAsString(String s) { + Tag tag = obj.get(s); + return tag != null ? tag.asString().orElse("") : ""; + } + @Override + public GenericBitStorage makeBitStorage(int bits, int count, long[] data) { + return new OurBitStorage(bits, count, data); + } + public String toString() { + return obj.toString(); + } + } + + public static class NBTList implements GenericNBTList { + private final ListTag obj; + public NBTList(ListTag t) { + obj = t; + } + @Override + public int size() { + return obj.size(); + } + @Override + public String getString(int idx) { + return obj.getStringOr(idx, ""); + } + @Override + public GenericNBTCompound getCompound(int idx) { + return new NBTCompound(obj.getCompoundOrEmpty(idx)); + } + public String toString() { + return obj.toString(); + } + } + + public static class OurBitStorage implements GenericBitStorage { + private final SimpleBitStorage bs; + public OurBitStorage(int bits, int count, long[] data) { + bs = new SimpleBitStorage(bits, count, data); + } + @Override + public int get(int idx) { + return bs.get(idx); + } + } +} diff --git a/settings.gradle b/settings.gradle index 449a8f85b..f73758cca 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,7 +9,10 @@ pluginManagement { plugins { // Apply paperweight.userdev to root project with 'apply false' for multi-project builds // See: https://docs.papermc.io/paper/dev/userdev/ - id 'io.papermc.paperweight.userdev' version '2.0.0-beta.19' apply false + id 'io.papermc.paperweight.userdev' version '2.0.0-beta.21' apply false + // Auto-download JDK toolchains; bukkit-helper-26-1-2 requires JDK 25 + // (Paper 26.1+ runtime), and not all CI runners have it preinstalled. + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' } rootProject.name = 'dynmap-common' @@ -42,6 +45,7 @@ include ':bukkit-helper-121-6' // helper is rewritten to use reflection like bukkit-helper-121-11. // include ':bukkit-helper-121-10' include ':bukkit-helper-121-11' +include ':bukkit-helper-26-1-2' include ':bukkit-helper' include ':dynmap-api' include ':DynmapCore' @@ -95,6 +99,7 @@ project(':bukkit-helper-121-5').projectDir = "$rootDir/bukkit-helper-121-5" as F project(':bukkit-helper-121-6').projectDir = "$rootDir/bukkit-helper-121-6" as File // project(':bukkit-helper-121-10').projectDir = "$rootDir/bukkit-helper-121-10" as File project(':bukkit-helper-121-11').projectDir = "$rootDir/bukkit-helper-121-11" as File +project(':bukkit-helper-26-1-2').projectDir = "$rootDir/bukkit-helper-26-1-2" as File project(':bukkit-helper').projectDir = "$rootDir/bukkit-helper" as File project(':dynmap-api').projectDir = "$rootDir/dynmap-api" as File project(':DynmapCore').projectDir = "$rootDir/DynmapCore" as File diff --git a/spigot/build.gradle b/spigot/build.gradle index 5d14071aa..3f8aff984 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -112,6 +112,18 @@ dependencies { implementation(project(':bukkit-helper-121-11')) { transitive = false } + // WORKAROUND: paperweight registers its `reobf` configuration as the + // preferred runtime variant, but Paper 26.1+ ships Mojang-mapped only and + // the dev bundle no longer provides reobf mappings, so reobfJar can't run + // and the `reobf` outgoing artifact is empty. Pinning `configuration: + // 'default'` here bypasses Gradle variant matching and pulls the plain + // jar instead, so org.dynmap.bukkit.helper.v26_1_2.* actually lands in + // the shaded spigot jar. Do NOT "align" this back to the sibling + // bukkit-helper modules' style without first verifying paperweight + // exposes a working reobf jar for this Minecraft version. + implementation(project(path: ':bukkit-helper-26-1-2', configuration: 'default')) { + transitive = false + } } processResources { @@ -160,6 +172,7 @@ shadowJar { include(dependency(':bukkit-helper-121-6')) // include(dependency(':bukkit-helper-121-10')) include(dependency(':bukkit-helper-121-11')) + include(dependency(':bukkit-helper-26-1-2:.*')) } relocate('org.bstats', 'org.dynmap.bstats') destinationDirectory = file '../target' diff --git a/spigot/src/main/java/org/dynmap/bukkit/Helper.java b/spigot/src/main/java/org/dynmap/bukkit/Helper.java index 26cc72e7f..6a17ff3cd 100644 --- a/spigot/src/main/java/org/dynmap/bukkit/Helper.java +++ b/spigot/src/main/java/org/dynmap/bukkit/Helper.java @@ -61,6 +61,14 @@ else if (v.contains("(MC: 1.21.9)") || v.contains("(MC: 1.21.10)")) { else if (v.contains("(MC: 1.21.")) { // Set up in case 1.21.12 works 'as is' BukkitVersionHelper.helper = loadVersionHelper("org.dynmap.bukkit.helper.v121_11.BukkitVersionHelperSpigot121_11"); } + // Minecraft adopted year.drop.hotfix versioning starting with MC 26.1 (March 2026). + // Match all 26.x releases here so future hotfixes load the same helper until specialised. + // NOTE: this is a prefix match. When a future MC 26.x release needs its own helper, + // add the more specific check (e.g. "(MC: 26.2.")) ABOVE this branch -- otherwise + // the new version will be swallowed by the fallback and load the wrong helper. + else if (v.contains("(MC: 26.")) { + BukkitVersionHelper.helper = loadVersionHelper("org.dynmap.bukkit.helper.v26_1_2.BukkitVersionHelperSpigot26_1_2"); + } else if (v.contains("(MC: 1.20)") || v.contains("(MC: 1.20.1)")) { BukkitVersionHelper.helper = loadVersionHelper("org.dynmap.bukkit.helper.v120.BukkitVersionHelperSpigot120"); }