From 0d701d53ddafe53591e00d7558f00da398f6f0cb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 15:44:46 +0000 Subject: [PATCH] feat: add Minecraft 26.1.2 support for Spigot/Paper - Add bukkit-helper-26-1-2 module modeled on bukkit-helper-121-11. - Uses paperweight.userdev (paperDevBundle 26.1.2-R0.1-SNAPSHOT), matching the pattern adopted by the rest of the modern helpers. - Compiled with Java 25 (required by MC 26.1+). - getLightDampening() instead of getLightBlock() (post-26.1 API). - Reflective CraftBukkit lookup tries Paper unversioned, plus candidate Spigot prefixes (v1_21_R8, v26_1_R1). - Spigot Helper.java routes (MC: 26.*) servers to the new helper. - Wire bukkit-helper-26-1-2 into settings.gradle and spigot/build.gradle (impl + shadowJar include). - ci(release): install both JDK 21 (Gradle/Shadow runtime) and JDK 25 (used via toolchain by the new module), pass both to Gradle. - ci(auto-beta-release): on each push to the upgrade branch, - open a draft PR back to the default branch on first push, - compute next 26.1.2-beta. tag (auto-incrementing), - build the spigot jar, - create a GitHub Release with the jar attached. --- .github/workflows/auto-beta-release.yml | 112 ++++ .github/workflows/release.yml | 12 +- bukkit-helper-26-1-2/build.gradle | 24 + .../BukkitVersionHelperSpigot26_1_2.java | 557 ++++++++++++++++++ .../helper/v26_1_2/MapChunkCache26_1_2.java | 190 ++++++ .../org/dynmap/bukkit/helper/v26_1_2/NBT.java | 143 +++++ settings.gradle | 2 + spigot/build.gradle | 4 + .../main/java/org/dynmap/bukkit/Helper.java | 3 + 9 files changed, 1046 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/auto-beta-release.yml create mode 100644 bukkit-helper-26-1-2/build.gradle create mode 100644 bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/BukkitVersionHelperSpigot26_1_2.java create mode 100644 bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/MapChunkCache26_1_2.java create mode 100644 bukkit-helper-26-1-2/src/main/java/org/dynmap/bukkit/helper/v26_1_2/NBT.java diff --git a/.github/workflows/auto-beta-release.yml b/.github/workflows/auto-beta-release.yml new file mode 100644 index 000000000..69a0bcc0e --- /dev/null +++ b/.github/workflows/auto-beta-release.yml @@ -0,0 +1,112 @@ +name: Auto Beta Release + +# Each push to the upgrade branch produces a new release tagged +# 26.1.2-beta., where N auto-increments from the highest existing tag. +# Also opens a PR back to the default branch on first push if one +# doesn't exist yet. + +on: + push: + branches: + - claude/minecraft-26.1.2-upgrade-6YV9K + +concurrency: + group: auto-beta-release + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +jobs: + build-and-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Ensure pull request exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_BRANCH: ${{ github.ref_name }} + BASE_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + existing=$(gh pr list --head "${HEAD_BRANCH}" --base "${BASE_BRANCH}" --state open --json number --jq 'length') + if [ "${existing}" = "0" ]; then + gh pr create \ + --base "${BASE_BRANCH}" \ + --head "${HEAD_BRANCH}" \ + --title "feat: add Minecraft 26.1.2 support for Spigot/Paper" \ + --body "$(printf '## Summary\n- Adds bukkit-helper-26-1-2 module modeled on 1.21.11, using paperweight.userdev with the 26.1.2-R0.1-SNAPSHOT Paper dev bundle.\n- Compiles the new module with Java 25 (required by MC 26.1+).\n- Routes (MC: 26.*) servers to the new helper in spigot/Helper.java.\n- Wires the new module into settings.gradle and spigot/build.gradle (impl + shadowJar include).\n- ci(release): adds JDK 25 alongside JDK 21 so paperweight can resolve the 26.1.2 toolchain.\n- ci(auto-beta-release): on each push, computes next 26.1.2-beta. tag, builds, and publishes a GitHub Release with the spigot JAR.\n\n## Test plan\n- [ ] Gradle :spigot:build succeeds (JDK 21 host with JDK 25 toolchain).\n- [ ] Paper dev bundle 26.1.2-R0.1-SNAPSHOT resolves from PaperMC repo.\n- [ ] Auto-beta-release workflow tags 26.1.2-beta. and uploads the spigot jar.\n- [ ] Manual smoke test on a Paper/Spigot 26.1.2 server.')" \ + --draft || true + else + echo "PR already exists for ${HEAD_BRANCH} -> ${BASE_BRANCH}; skipping." + fi + + - name: Compute next beta version + id: version + run: | + set -euo pipefail + BASE="26.1.2-beta" + git fetch --tags --force + LAST=$(git tag --list "${BASE}.*" \ + | sed -E "s/^${BASE}\.([0-9]+)$/\1/" \ + | grep -E '^[0-9]+$' \ + | sort -n \ + | tail -n1 || true) + if [ -z "${LAST}" ]; then + NEXT=1 + else + NEXT=$((LAST + 1)) + fi + TAG="${BASE}.${NEXT}" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "Next tag: ${TAG}" + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'temurin' + cache: gradle + + - name: Build with Gradle + env: + USERNAME: ${{ secrets.USERNAME }} + TOKEN: ${{ secrets.TOKEN }} + # Run Gradle on JDK 21 (compatible with Shadow plugin); JDK 25 used via toolchain. + JAVA_HOME: ${{ env.JAVA_HOME_21_X64 }} + run: | + ./gradlew :spigot:build \ + -Porg.gradle.java.installations.paths="${JAVA_HOME_25_X64},${JAVA_HOME_21_X64}" + + - name: Find JAR file + id: find_jar + run: | + set -euo pipefail + JAR_FILE=$(ls target/*.jar | grep 'spigot' | tail -n 1) + echo "jar_file=${JAR_FILE}" >> "$GITHUB_OUTPUT" + echo "jar_name=$(basename "${JAR_FILE}")" >> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + JAR_FILE: ${{ steps.find_jar.outputs.jar_file }} + run: | + set -euo pipefail + gh release create "${TAG}" "${JAR_FILE}" \ + --target "${GITHUB_SHA}" \ + --title "${TAG}" \ + --notes "Automated beta release for Minecraft 26.1.2 (commit ${GITHUB_SHA})." \ + --prerelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5eed1b93..cda939b58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,13 +17,23 @@ jobs: with: java-version: '21' distribution: 'temurin' + + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'temurin' cache: gradle - name: Build with Gradle env: USERNAME: ${{ secrets.USERNAME }} TOKEN: ${{ secrets.TOKEN }} - run: ./gradlew :spigot:build + # Run Gradle on JDK 21 (compatible with Shadow plugin); JDK 25 used via toolchain. + JAVA_HOME: ${{ env.JAVA_HOME_21_X64 }} + run: | + ./gradlew :spigot:build \ + -Porg.gradle.java.installations.paths="${JAVA_HOME_25_X64},${JAVA_HOME_21_X64}" - name: Find JAR file id: find_jar diff --git a/bukkit-helper-26-1-2/build.gradle b/bukkit-helper-26-1-2/build.gradle new file mode 100644 index 000000000..def61f2dd --- /dev/null +++ b/bukkit-helper-26-1-2/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'io.papermc.paperweight.userdev' +} + +eclipse { + project { + name = "Dynmap(Spigot-26.1.2)" + } +} + +description = 'bukkit-helper-26.1.2' + +// MC 26.1+ requires Java 25. +java { + toolchain.languageVersion = JavaLanguageVersion.of(25) +} + +dependencies { + implementation project(':bukkit-helper') + implementation project(':dynmap-api') + implementation project(path: ':DynmapCore', configuration: 'shadow') + // Use paperweight dev bundle (matches the pattern used by bukkit-helper-121-11). + paperweight.paperDevBundle("26.1.2-R0.1-SNAPSHOT") +} 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..b953af59a --- /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,557 @@ +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 Class craftServerClass; + private static Method craftWorldGetHandle; + private static Method craftWorldGetMinHeight; + private static Method craftChunkGetHandle; + private static Method craftPlayerGetProfile; + private static Method craftServerGetServer; + private static boolean initialized = false; + + private static void initCraftBukkitClasses() { + if (initialized) return; + initialized = true; + + // Try Paper's unversioned packages first, then fall back to Spigot's versioned packages. + // MC 26.1.x is fully unobfuscated: Paper drops versioned packages and Spigot's R-series + // continues from 1.21.11 (R7). The exact suffix is unconfirmed, so we try several. + String[] packagePrefixes = { + "org.bukkit.craftbukkit", // Paper 1.20.5+ / Spigot 26.1+ (unversioned) + "org.bukkit.craftbukkit.v1_21_R8", // Spigot 26.1.x (next R after 1.21.11) + "org.bukkit.craftbukkit.v26_1_R1" // Possible new year-based naming + }; + + for (String prefix : packagePrefixes) { + try { + craftWorldClass = Class.forName(prefix + ".CraftWorld"); + craftChunkClass = Class.forName(prefix + ".CraftChunk"); + craftPlayerClass = Class.forName(prefix + ".entity.CraftPlayer"); + craftServerClass = Class.forName(prefix + ".CraftServer"); + + // Get methods + craftWorldGetHandle = craftWorldClass.getMethod("getHandle"); + craftWorldGetMinHeight = craftWorldClass.getMethod("getMinHeight"); + craftChunkGetHandle = craftChunkClass.getMethod("getHandle", ChunkStatus.class); + craftPlayerGetProfile = craftPlayerClass.getMethod("getProfile"); + craftServerGetServer = craftServerClass.getMethod("getServer"); + + Log.info("[Dynmap] Using CraftBukkit package: " + prefix); + return; + } catch (ClassNotFoundException | NoSuchMethodException e) { + // Try next prefix + } + } + 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 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(); + // 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..b63d5b6f8 --- /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,190 @@ +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 void initReflection() { + if (initialized) return; + initialized = true; + + // Try Paper's unversioned packages first, then fall back to Spigot's versioned packages. + // MC 26.1.x is fully unobfuscated; we try several prefix patterns. + String[] packagePrefixes = { + "org.bukkit.craftbukkit", // Paper 1.20.5+ / Spigot 26.1+ (unversioned) + "org.bukkit.craftbukkit.v1_21_R8", // Spigot 26.1.x (next R after 1.21.11) + "org.bukkit.craftbukkit.v26_1_R1" // Possible new year-based naming + }; + + for (String prefix : packagePrefixes) { + try { + craftWorldClass = Class.forName(prefix + ".CraftWorld"); + craftServerClass = Class.forName(prefix + ".CraftServer"); + + // Get methods + craftWorldGetHandle = craftWorldClass.getMethod("getHandle"); + craftWorldIsChunkLoaded = craftWorldClass.getMethod("isChunkLoaded", int.class, int.class); + craftServerGetServer = craftServerClass.getMethod("getServer"); + + Log.info("[Dynmap] MapChunkCache using CraftBukkit package: " + prefix); + return; + } catch (ClassNotFoundException | NoSuchMethodException e) { + // Try next prefix + } + } + 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 f4ccd2de9..4aa6e4a2a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,7 @@ include ':bukkit-helper-121-5' include ':bukkit-helper-121-6' // include ':bukkit-helper-121-10' // TODO: needs reflection refactor for paperweight include ':bukkit-helper-121-11' +include ':bukkit-helper-26-1-2' include ':bukkit-helper' include ':dynmap-api' include ':DynmapCore' @@ -94,6 +95,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 // TODO: needs reflection refactor 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 f7330152e..b7c62d14e 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -112,6 +112,9 @@ dependencies { implementation(project(':bukkit-helper-121-11')) { transitive = false } + implementation(project(':bukkit-helper-26-1-2')) { + transitive = false + } } processResources { @@ -160,6 +163,7 @@ shadowJar { include(dependency(':bukkit-helper-121-6')) // include(dependency(':bukkit-helper-121-10')) // TODO: needs reflection refactor 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 89441f999..dac61a80c 100644 --- a/spigot/src/main/java/org/dynmap/bukkit/Helper.java +++ b/spigot/src/main/java/org/dynmap/bukkit/Helper.java @@ -61,6 +61,9 @@ else if (v.contains("(MC: 1.21.10)")) { else if (v.contains("(MC: 1.21.")) { BukkitVersionHelper.helper = loadVersionHelper("org.dynmap.bukkit.helper.v121_11.BukkitVersionHelperSpigot121_11"); } + 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"); }