From 0b6409397bbe8fa01be4e1d4892f411d6ba7dad1 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 7 Jun 2026 23:02:01 +0200 Subject: [PATCH] feat(generator): improve jump generation and block placement logic Refactor the parkour generator to improve jump accuracy, block placement, and lifecycle management. - Improve jump calculation by normalizing heading vectors and adjusting height/distance constraints. - Implement height overrides for tall materials (fences, trapdoors, glass panes) during block placement. - Add difficulty-based special block generation (packed/blue ice). - Fix block identity issues by using coordinate-based comparison instead of reference equality for scoring and cleanup. - Optimize generation lead to reduce unnecessary block generation. - Update leaderboard logic to only replace scores if the new score is strictly better (higher score or faster time). - Add error handling for generator ticks to prevent session crashes. build: update build configuration and dependencies - Add `run-paper` plugin for easier local server testing. - Update Gradle wrapper to version 8.14.3. - Change WorldEdit dependencies to `compileOnly`. - Configure `runServer` task with Minecraft 1.21.4 and PlaceholderAPI. fix(ui): improve scoreboard placeholder handling - Add check for PlaceholderAPI plugin enablement before attempting to use it. --- .gitignore | 2 +- build.gradle.kts | 20 ++++++-- gradle/wrapper/gradle-wrapper.properties | 2 +- .../coordinator/GeneratorProfileManager.java | 13 +++++ .../jump/calculation/JumpCalculator.java | 24 +++++---- .../jump/calculation/JumpDirector.java | 3 +- .../generator/jump/placement/BlockPlacer.java | 51 ++++++++++++++----- .../jump/placement/BlockSelector.java | 6 ++- .../lifecycle/loop/LifecycleTickManager.java | 10 ++-- .../lifecycle/player/GeneratorCleanup.java | 13 ++++- .../player/PlayerInteractionHandler.java | 27 ++++++++-- .../leaderboard/core/Leaderboard.java | 20 ++++++-- .../loparkour/player/core/ParkourPlayer.java | 12 +++-- .../player/service/ScoreboardManager.java | 13 +++-- .../session/manager/SessionStateManager.java | 7 ++- 15 files changed, 168 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index 45e85db..f62046d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ build/ gradle-app.setting !**/src/main/**/build/ !**/src/test/**/build/ - +run/ # OS files .DS_Store Thumbs.db diff --git a/build.gradle.kts b/build.gradle.kts index 1944fe6..d659488 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("java") id("io.github.goooler.shadow") version("8.1.8") + id("xyz.jpenilla.run-paper") version("3.0.2") } group = "dev.loki" @@ -90,8 +91,8 @@ dependencies { implementation("org.bstats:bstats-bukkit:3.2.1") implementation("com.zaxxer:HikariCP:5.1.0") implementation("com.mysql:mysql-connector-j:8.3.0") - implementation("com.sk89q.worldedit:worldedit-core:7.3.6") - implementation("com.sk89q.worldedit:worldedit-bukkit:7.3.6") + compileOnly("com.sk89q.worldedit:worldedit-core:7.3.6") + compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.6") // Provided dependencies (plugins) compileOnly("me.clip:placeholderapi:2.11.6") @@ -119,8 +120,6 @@ tasks.shadowJar { relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib") relocate("com.google.gson", "dev.loki.loparkour.lib.gson") relocate("org.bstats", "dev.loki.loparkour.lib.bstats") - relocate("com.sk89q.worldedit", "dev.loki.loparkour.lib.worldedit") - relocate("org.enginehub", "dev.loki.loparkour.lib.enginehub") // Don't minimize - causes issues with Caffeine's dynamically generated classes // minimize { @@ -156,5 +155,18 @@ tasks.withType { options.encoding = "UTF-8" } +// Run paper server via gradlew runServer +tasks { + runServer { + minecraftVersion("1.21.4") + jvmArgs("-Xms2G", "-Xmx2G") + runDirectory.set(layout.projectDirectory.dir("run")) + + downloadPlugins { + modrinth("placeholderapi", "2.11.6") + } + } +} + // Default task defaultTasks("clean", "build") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b41..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java index 79e6125..ec189fa 100644 --- a/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java +++ b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java @@ -121,6 +121,8 @@ public class GeneratorProfileManager { generator.state.specialChances.clear(); try { + double difficulty = getDifficultyScore(); + for (String specialType : Config.GENERATION.getChildren("advanced.special")) { String materialPath = "advanced.special." + specialType + ".material"; String chancePath = "advanced.special." + specialType + ".chance"; @@ -132,6 +134,17 @@ public class GeneratorProfileManager { generator.state.specialChances.put(blockData, (double) chance); } + // Add ice variants on higher difficulties + // difficulty >= ~1.0 = hard: packed ice, >= ~2.0 = expert: blue ice + if (difficulty >= 1.0) { + generator.state.specialChances.put( + org.bukkit.Bukkit.createBlockData("minecraft:packed_ice"), 15.0); + } + if (difficulty >= 2.0) { + generator.state.specialChances.put( + org.bukkit.Bukkit.createBlockData("minecraft:blue_ice"), 10.0); + } + normalizeMap(generator.state.specialChances); } catch (Exception ignored) { // special blocks config optional diff --git a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java index 72ca180..3d65722 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java @@ -48,9 +48,9 @@ public class JumpCalculator { distance = Math.min(distance, constraints.maxDistance); // Clamp values to valid ranges - height = Math.max(-2, Math.min(1, height)); + height = Math.max(-2, Math.min(2, height)); distance = Math.max(1, Math.min(4, distance)); - + if (height > 0) distance = Math.max(distance - height, 1); // Calculate offset @@ -102,7 +102,7 @@ public class JumpCalculator { double sd = java.util.Arrays.asList(generator.generatorOptions).contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1; int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd); - Vector offset = generator.state.heading.clone().multiply(distance + 1).setY(height); + Vector offset = generator.state.heading.clone().multiply(distance).setY(height); if (offset.getX() == 0) { offset.setX(randomOffset); } else { @@ -133,17 +133,21 @@ public class JumpCalculator { private int getSpecialMaterialMaxHeight(@NotNull Material material) { return switch (material) { - case PACKED_ICE -> 1; // Ice allows normal height - case LADDER -> 1; // Ladder allows some height - default -> 0; // Most special materials restrict height + case PACKED_ICE -> 1; // Ice allows normal height + case BLUE_ICE -> 1; // Blue ice same + case ICE -> 0; // Regular ice melts, slippery + case LADDER -> 1; // Ladder allows some height + default -> 0; // Most special materials restrict height }; } - + private int getSpecialMaterialMaxDistance(@NotNull Material material) { return switch (material) { - case PACKED_ICE -> 3; // Ice is slippery - case LADDER -> 2; // Ladder is very restrictive - default -> 3; // Default restriction + case PACKED_ICE -> 4; // Ice — full distance, slippery + case BLUE_ICE -> 4; // Blue ice — full distance + case ICE -> 3; // Regular ice — reduced + case LADDER -> 2; // Ladder is very restrictive + default -> 3; // Default restriction }; } diff --git a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java index d79d551..93f7d76 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java @@ -101,7 +101,8 @@ public class JumpDirector { if (recommendedHeading.lengthSquared() == 0) { return current; } else { - return recommendedHeading; + // Normalize so diagonal vectors don't multiply distance by √2 + return recommendedHeading.normalize(); } } diff --git a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java index 44aa578..81ce9ff 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java @@ -9,6 +9,7 @@ import dev.loki.loparkour.schematic.core.ParkourSchematic; import dev.loki.loparkour.schematic.core.SchematicManager; import dev.loki.loparkour.util.misc.Probs; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.Sound; import org.bukkit.block.Block; @@ -40,7 +41,7 @@ public class BlockPlacer { public void generate() { generate(1); } - + /** * Generate specified amount of blocks. */ @@ -49,6 +50,27 @@ public class BlockPlacer { generateSingleBlock(); } } + + private boolean isTallMaterial(@NotNull Material mat) { + return blockSelector.isFenceMaterial(mat) || blockSelector.isTrapdoorMaterial(mat) || mat == Material.GLASS_PANE; + } + + /** + * Select blocks with a forced height override for tall materials (fences, trapdoors). + */ + @NotNull + private List selectBlocks(int forcedHeight) { + List blocks = new ArrayList<>(); + if (generator.state.history.isEmpty()) { + return blocks; + } + Block current = generator.getLatest(); + int distance = Probs.random(generator.state.distanceChances); + int height = forcedHeight != Integer.MIN_VALUE ? forcedHeight : Probs.random(generator.state.heightChances); + Block nextBlock = jumpCalculator.calculateNextBlock(current, distance, height); + blocks.add(nextBlock); + return blocks; + } /** * Generate the first block at specified locations. @@ -92,30 +114,33 @@ public class BlockPlacer { } private void placeNormalBlock() { - List blocks = selectBlocks(); + BlockData blockData = blockSelector.selectBlockData(); + if (blockData == null) return; + + Material mat = blockData.getMaterial(); + boolean tall = isTallMaterial(mat); + + List blocks = tall ? selectBlocks(-1) : selectBlocks(); if (blocks.isEmpty()) return; - Block selectedBlock = blocks.get(0); // Use first block for simplicity - BlockData blockData = blockSelector.selectBlockData(); - if (blockData == null) { - return; // Skip if no valid block data available - } - + Block selectedBlock = blocks.get(0); placeBlockData(selectedBlock, blockData); generator.state.history.add(selectedBlock); } - + private void placeSpecialBlock() { - // Select special block type based on probabilities BlockData specialBlockData = Probs.random(generator.state.specialChances); if (specialBlockData == null) { placeNormalBlock(); return; } - - List blocks = selectBlocks(); + + Material mat = specialBlockData.getMaterial(); + boolean tall = isTallMaterial(mat); + + List blocks = tall ? selectBlocks(-1) : selectBlocks(); if (blocks.isEmpty()) return; - + Block selectedBlock = blocks.get(0); placeBlockData(selectedBlock, specialBlockData); generator.state.history.add(selectedBlock); diff --git a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java index d8a6746..af09e0c 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java @@ -107,12 +107,14 @@ public class BlockSelector { return isSlabMaterial(material) || material == Material.GLASS_PANE || material == Material.PACKED_ICE || + material == Material.BLUE_ICE || + material == Material.ICE || isFenceMaterial(material) || isTrapdoorMaterial(material) || material == Material.LADDER; } - private boolean isFenceMaterial(@NotNull Material material) { + public boolean isFenceMaterial(@NotNull Material material) { return material == Material.OAK_FENCE || material == Material.BIRCH_FENCE || material == Material.SPRUCE_FENCE || @@ -126,7 +128,7 @@ public class BlockSelector { material == Material.NETHER_BRICK_FENCE; } - private boolean isTrapdoorMaterial(@NotNull Material material) { + public boolean isTrapdoorMaterial(@NotNull Material material) { return material == Material.OAK_TRAPDOOR || material == Material.BIRCH_TRAPDOOR || material == Material.SPRUCE_TRAPDOOR || diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java index ec45978..563dc96 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java @@ -11,7 +11,7 @@ import org.jetbrains.annotations.NotNull; public class LifecycleTickManager { private static final int TIME_UI_INTERVAL = 20; // 1 second — time display tick - private static final int GENERATION_LEAD = 5; + private static final int GENERATION_LEAD = 2; // keep only 2 blocks ahead, generate more when player approaches end private final ParkourGenerator generator; private final GeneratorCleanup cleanup; @@ -32,12 +32,10 @@ public class LifecycleTickManager { * Start the tick cycle. */ public void startTick() { - if (generator.state.start == null) { - generator.state.start = java.time.Instant.now(); - } + // Timer starts on first block jump, not here — see PlayerInteractionHandler.handleScore() - // Generate initial blocks (reduced from GENERATION_LEAD to avoid too far generation) - generator.generate(3); + // Generate initial blocks ahead of the player + generator.generate(2); } /** diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java index 18d537c..17a3548 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java @@ -34,7 +34,7 @@ public class GeneratorCleanup { for (ParkourPlayer pp : generator.getPlayers()) { Block below = blockBelowPlayer(pp); if (below == null) continue; - int idx = history.indexOf(below); + int idx = indexOfBlock(history, below); if (idx >= 0) { minStandingIndex = Math.min(minStandingIndex, idx); } @@ -56,6 +56,17 @@ public class GeneratorCleanup { history.subList(0, removeCount).clear(); } + /** Find index of a block in history by coordinates, not reference. */ + private static int indexOfBlock(@NotNull List list, @NotNull Block target) { + for (int i = 0; i < list.size(); i++) { + Block b = list.get(i); + if (b != null && b.getX() == target.getX() && b.getY() == target.getY() && b.getZ() == target.getZ()) { + return i; + } + } + return -1; + } + private static Block blockBelowPlayer(@NotNull ParkourPlayer pp) { Location loc = pp.player.getLocation().clone(); Block below = loc.subtract(0, 1, 0).getBlock(); diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java index 530e09f..e0611ba 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java @@ -11,6 +11,9 @@ import org.bukkit.block.Block; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.time.Instant; +import java.util.List; + /** * Handles player interactions with parkour blocks. */ @@ -42,11 +45,29 @@ public class PlayerInteractionHandler { } } } + + /** Compare two blocks by their world coordinates (Bukkit Block uses reference equality). */ + private static boolean isSameBlock(@NotNull Block a, @NotNull Block b) { + return a.getX() == b.getX() && a.getY() == b.getY() && a.getZ() == b.getZ(); + } + + /** Check if history contains a block by coordinates, not reference. */ + private static boolean historyContains(@NotNull List history, @NotNull Block block) { + for (Block b : history) { + if (isSameBlock(b, block)) return true; + } + return false; + } /** * Handle player scoring on a block. */ public void handleScore() { + // Start the timer on the very first scored block + if (generator.state.start == null) { + generator.state.start = Instant.now(); + } + generator.state.score++; eventManager.handleScore(); @@ -91,8 +112,8 @@ public class PlayerInteractionHandler { handleSchematicEndBlock(block); } - // Check for scoring - if (generator.state.history.contains(block) && !player.hasScored(block)) { + // Check for scoring (compare by coordinates, not reference — Bukkit Block uses identity) + if (historyContains(generator.state.history, block) && !player.hasScored(block)) { player.markScored(block); handleScore(); } @@ -111,7 +132,7 @@ public class PlayerInteractionHandler { private boolean isSchematicEndBlock(@NotNull Block block) { return generator.state.schematicBlocks != null - && generator.state.schematicBlocks.contains(block); + && historyContains(generator.state.schematicBlocks, block); } /** Block directly under feet; {@code null} in air — scoring runs when landed. */ diff --git a/src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java b/src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java index 0266e3f..b94d9f9 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java @@ -67,17 +67,22 @@ public class Leaderboard { // ── CRUD operations ─────────────────────────────────────────────────────── /** - * Registers a new score, overriding the old one + * Registers a new score — only replaces the old one if it's strictly better + * (higher score, or same score with faster time). * * @param uuid The player's uuid * @param score The {@link Score} instance associated with a player's run - * @return the previous score, if there was one + * @return the previous best score, if there was one */ @Nullable public Score put(@NotNull UUID uuid, @NotNull Score score) { Score previous; synchronized (scores) { - previous = scores.put(uuid, score); + previous = scores.get(uuid); + if (previous != null && !isBetterThan(score, previous)) { + return previous; // keep the better score + } + scores.put(uuid, score); } sorter.sortInPlace(scores); @@ -85,6 +90,15 @@ public class Leaderboard { return previous; } + /** Returns true if {@code candidate} is strictly better than {@code existing}. */ + private static boolean isBetterThan(@NotNull Score candidate, @NotNull Score existing) { + if (candidate.score() != existing.score()) { + return candidate.score() > existing.score(); + } + // same score — faster time wins + return candidate.getTimeMillis() < existing.getTimeMillis(); + } + /** * Resets the score of a player by deleting it from the internal map * diff --git a/src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java b/src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java index f1071d3..77e056e 100644 --- a/src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java +++ b/src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java @@ -36,8 +36,8 @@ public class ParkourPlayer extends ParkourUser { public static final Map PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings(); - // Track scored blocks to prevent duplicate scoring - private final Set scoredBlocks = new HashSet<>(); + // Track scored blocks to prevent duplicate scoring (String key = "x,y,z" to avoid Bukkit Block reference equality) + private final Set scoredBlocks = new HashSet<>(); public @Expose Double schematicDifficulty; public @Expose Integer blockLead; @@ -115,18 +115,22 @@ public class ParkourPlayer extends ParkourUser { save(LoParkour.getPlugin().isEnabled()); } + private static String blockKey(Block block) { + return block.getX() + "," + block.getY() + "," + block.getZ(); + } + /** * Check if player has already scored on this block. */ public boolean hasScored(Block block) { - return scoredBlocks.contains(block); + return scoredBlocks.contains(blockKey(block)); } /** * Mark block as scored to prevent duplicate scoring. */ public void markScored(Block block) { - scoredBlocks.add(block); + scoredBlocks.add(blockKey(block)); } /** diff --git a/src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java b/src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java index 219e730..8904e9c 100644 --- a/src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java +++ b/src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java @@ -5,7 +5,7 @@ import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; import dev.loki.loparkour.leaderboard.core.Leaderboard; import dev.loki.loparkour.leaderboard.model.Score; import dev.loki.loparkour.util.text.ColorUtil; -import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.scoreboard.DisplaySlot; import org.bukkit.scoreboard.Objective; @@ -124,10 +124,13 @@ public class ScoreboardManager { } private String applyPlaceholderAPI(String text) { - try { - return PlaceholderAPI.setPlaceholders(player, text); - } catch (Exception e) { - return text; + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + try { + return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, text); + } catch (Exception e) { + return text; + } } + return text; } } diff --git a/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java index 1cae06f..9e53bb7 100644 --- a/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java +++ b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java @@ -75,7 +75,12 @@ public class SessionStateManager { tickTask = Scheduler.get(LoParkour.getPlugin()).runTimer(() -> { if (session.generator != null) { - session.generator.tick(); + try { + session.generator.tick(); + } catch (Throwable t) { + session.generator.session.getPlayers().forEach(p -> + p.player.sendMessage("§c[LoParkour] Tick error: " + t.getMessage())); + } } }, 0, 1); // Run every tick (1 = 50ms) }