From 9809d131716dbdf66a4c0c13c6fada2fc45eccf6 Mon Sep 17 00:00:00 2001 From: Ofek B Date: Thu, 14 Aug 2025 01:30:25 +0300 Subject: [PATCH 1/3] feat: basic structure with base modules code --- .../fasterpathways/FasterPathways.java | 120 ++++---------- .../fasterpathways/core/BoostService.java | 82 ++++++++++ .../fasterpathways/core/ConfigManager.java | 150 ++++++++++++++++++ .../fasterpathways/core/MovementListener.java | 120 ++++++++++++++ src/main/resources/config.yml | 43 ++--- 5 files changed, 412 insertions(+), 103 deletions(-) create mode 100644 src/main/java/gemesil/fasterpathways/core/BoostService.java create mode 100644 src/main/java/gemesil/fasterpathways/core/ConfigManager.java create mode 100644 src/main/java/gemesil/fasterpathways/core/MovementListener.java diff --git a/src/main/java/gemesil/fasterpathways/FasterPathways.java b/src/main/java/gemesil/fasterpathways/FasterPathways.java index 8092f21..86df047 100644 --- a/src/main/java/gemesil/fasterpathways/FasterPathways.java +++ b/src/main/java/gemesil/fasterpathways/FasterPathways.java @@ -1,104 +1,54 @@ package gemesil.fasterpathways; -import net.md_5.bungee.api.ChatMessageType; -import net.md_5.bungee.api.chat.TextComponent; +import gemesil.fasterpathways.core.BoostService; +import gemesil.fasterpathways.core.ConfigManager; +import gemesil.fasterpathways.core.MovementListener; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.attribute.Attribute; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import java.util.HashSet; -import java.util.Set; -import java.util.logging.Level; +/** + * Entry point for the FasterPathways plugin. + * Keeps lifecycle minimal and delegates to the core package. + */ +public final class FasterPathways extends JavaPlugin { -public final class FasterPathways extends JavaPlugin implements Listener { - - private PotionEffect speedEffect; - private Set speedBlocks; - private Set disabledWorlds; - private String actionBarMessage; - private boolean showActionBar; + private ConfigManager configManager; + private BoostService boostService; @Override public void onEnable() { saveDefaultConfig(); - loadConfiguration(); - getServer().getPluginManager().registerEvents(this, this); - getLogger().info("FasterPathways | Plugin has been enabled!"); - } - - private void loadConfiguration() { - reloadConfig(); - FileConfiguration config = getConfig(); - - // Load speed effect settings - int speedLevel = config.getInt("speed.level", 1); - int speedDuration = config.getInt("speed.duration", 2); - speedEffect = new PotionEffect(PotionEffectType.SPEED, speedDuration * 20, speedLevel - 1); + reloadAll(); - // Load action bar settings - actionBarMessage = ChatColor.translateAlternateColorCodes('&', - config.getString("messages.actionBar", "&eYou're moving faster!")); - showActionBar = config.getBoolean("messages.showActionBar", true); + // Register movement listener + Bukkit.getPluginManager().registerEvents( + new MovementListener(configManager, boostService), + this + ); - // Load disabled worlds - disabledWorlds = new HashSet<>(config.getStringList("disabledWorlds")); - for (String worldName : disabledWorlds) { - if (Bukkit.getWorld(worldName) == null) { - getLogger().warning("FasterPathways | World not found: " + worldName); - } - } + getLogger().info("FasterPathways | Enabled (attribute-only)"); + } - // Load speed blocks - speedBlocks = new HashSet<>(); - for (String blockName : config.getStringList("speedBlocks")) { - try { - Material material = Material.valueOf(blockName.toUpperCase()); - speedBlocks.add(material); - } catch (IllegalArgumentException e) { - getLogger().log(Level.WARNING, "FasterPathways | Invalid block material in config: " + blockName); + @Override + public void onDisable() { + // Ensure no one remains boosted after disable/reload + for (Player onlinePlayer : getServer().getOnlinePlayers()) { + if (onlinePlayer.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED) != null) { + boostService.clearAttributeModifier(onlinePlayer); } } - - if (speedBlocks.isEmpty()) { - getLogger().warning("FasterPathways | No valid speed blocks configured! Adding DIRT_PATH as default."); - speedBlocks.add(Material.DIRT_PATH); - } + boostService.clearAll(); } - @EventHandler - public void onPlayerMove(PlayerMoveEvent event) { - if (event.getTo() != null && event.getTo().getBlock().equals(event.getFrom().getBlock())) { - return; - } - - final Player player = event.getPlayer(); - - // Check if world is disabled - if (disabledWorlds.contains(player.getWorld().getName())) { - return; - } - - final Block block = player.getLocation().getBlock(); - final Block relativeBlock = block.getRelative(BlockFace.DOWN); - - if (speedBlocks.contains(block.getType()) || speedBlocks.contains(relativeBlock.getType())) { - player.addPotionEffect(speedEffect); - - if (showActionBar) { - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, - TextComponent.fromLegacyText(actionBarMessage)); - } - } + /** + * Reloads configuration and re-initializes services. + * Call this if you later add a /reload command. + */ + public void reloadAll() { + reloadConfig(); + this.configManager = new ConfigManager(getConfig(), getLogger(), getServer()); + this.boostService = new BoostService(); } -} \ No newline at end of file +} diff --git a/src/main/java/gemesil/fasterpathways/core/BoostService.java b/src/main/java/gemesil/fasterpathways/core/BoostService.java new file mode 100644 index 0000000..3f1a68e --- /dev/null +++ b/src/main/java/gemesil/fasterpathways/core/BoostService.java @@ -0,0 +1,82 @@ +package gemesil.fasterpathways.core; + +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.entity.Player; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * Applies and clears the movement speed attribute modifier in a stable, idempotent way. + * Uses a deterministic UUID per player so re-application does not stack. + */ +public final class BoostService { + + private static final String MODIFIER_NAME = "FasterPathways"; + private final Map appliedMultiplierByPlayer = new HashMap<>(); + + /** + * Ensures the player has the specified multiplier applied. + * If a different value is already present, it will be replaced. + * + * @param player target player + * @param multiplier +0.20 for +20% speed + */ + public void ensureAttribute(final Player player, final double multiplier) { + final double clamped = Math.max(0.0, multiplier); + final Double current = appliedMultiplierByPlayer.get(player.getUniqueId()); + if (current != null && Math.abs(current - clamped) < 1e-9) { + return; // already applied with the same value + } + + clearAttributeModifier(player); + addAttributeModifier(player, clamped); + } + + /** + * Removes the plugin's attribute modifier from the player if present. + * + * @param player target player + */ + public void clearAttributeModifier(final Player player) { + final AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); + if (instance == null) return; + + final UUID modifierId = stableUuidFor(player.getUniqueId()); + instance.getModifiers().stream() + .filter(mod -> mod.getUniqueId().equals(modifierId) || MODIFIER_NAME.equals(mod.getName())) + .toList() + .forEach(instance::removeModifier); + + appliedMultiplierByPlayer.remove(player.getUniqueId()); + } + + /** + * Clears internal bookkeeping. Should be called on plugin disable. + */ + public void clearAll() { + appliedMultiplierByPlayer.clear(); + } + + private void addAttributeModifier(final Player player, final double multiplier) { + final AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); + if (instance == null) return; + + final UUID modifierId = stableUuidFor(player.getUniqueId()); + final AttributeModifier modifier = new AttributeModifier( + modifierId, + MODIFIER_NAME, + multiplier, + AttributeModifier.Operation.ADD_SCALAR + ); + instance.addModifier(modifier); + appliedMultiplierByPlayer.put(player.getUniqueId(), multiplier); + } + + private static UUID stableUuidFor(final UUID playerUniqueId) { + final String seed = "fasterpathways-" + playerUniqueId; + return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/gemesil/fasterpathways/core/ConfigManager.java b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java new file mode 100644 index 0000000..35a2bcc --- /dev/null +++ b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java @@ -0,0 +1,150 @@ +package gemesil.fasterpathways.core; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Loads and exposes plugin configuration with readable accessors. + * Supports both the preferred per-block format and the legacy format. + */ +public final class ConfigManager { + + /** + * Immutable per-block configuration entry. + * multiplier: 0.20 means +20% movement speed. + */ + public static final class BlockBoost { + public final Material material; + public final double multiplier; + + public BlockBoost(final Material material, final double multiplier) { + this.material = material; + this.multiplier = Math.max(0.0, multiplier); + } + } + + private final Logger logger; + private final Set disabledWorldNames = new HashSet<>(); + private final Map boostsByMaterial = new EnumMap<>(Material.class); + private final boolean showActionBar; + private final String actionBarMessage; + private final double defaultMultiplier; + + /** + * Constructs and loads configuration. + * + * @param cfg Bukkit configuration + * @param logger Plugin logger + * @param server Bukkit server (used to warn on missing worlds) + */ + public ConfigManager(final FileConfiguration cfg, final Logger logger, final Server server) { + this.logger = logger; + + // Messages + this.showActionBar = cfg.getBoolean("messages.showActionBar", true); + this.actionBarMessage = ChatColor.translateAlternateColorCodes( + '&', cfg.getString("messages.actionBar", "&eYou're moving faster!") + ); + + // Default multiplier (legacy/global fallback) + this.defaultMultiplier = Math.max(0.0, cfg.getDouble("speed.multiplier", 0.20)); + + // Disabled worlds + this.disabledWorldNames.addAll(cfg.getStringList("disabledWorlds")); + for (String worldName : disabledWorldNames) { + World world = server.getWorld(worldName); + if (world == null) { + logger.warning("FasterPathways | World not found: " + worldName); + } + } + + // Preferred per-block format + if (cfg.isList("blocks")) { + for (Map rawEntry : cfg.getMapList("blocks")) { + if (!(rawEntry instanceof Map entry)) continue; + String materialName = String.valueOf(entry.getOrDefault("material", "")).toUpperCase(Locale.ROOT); + if (materialName.isBlank()) continue; + + try { + Material material = Material.valueOf(materialName); + double multiplier = toDouble(entry.get("multiplier"), defaultMultiplier); + boostsByMaterial.put(material, new BlockBoost(material, multiplier)); + } catch (IllegalArgumentException ex) { + logger.log(Level.WARNING, "FasterPathways | Invalid material in blocks: " + materialName); + } + } + } + + // Legacy fallback if no per-block entries present + if (boostsByMaterial.isEmpty()) { + List legacyList = cfg.getStringList("speedBlocks"); + if (legacyList == null || legacyList.isEmpty()) { + legacyList = Collections.singletonList("DIRT_PATH"); + logger.warning("FasterPathways | No blocks configured; adding DIRT_PATH as default"); + } + for (String name : legacyList) { + try { + Material material = Material.valueOf(name.toUpperCase(Locale.ROOT)); + boostsByMaterial.put(material, new BlockBoost(material, defaultMultiplier)); + } catch (IllegalArgumentException ex) { + logger.log(Level.WARNING, "FasterPathways | Invalid material in speedBlocks: " + name); + } + } + } + } + + /** + * Determines whether the given world is configured as disabled. + * + * @param world Bukkit world + * @return true if boosts are disabled in this world + */ + public boolean isWorldDisabled(final World world) { + return disabledWorldNames.contains(world.getName()); + } + + /** + * Returns the configured BlockBoost for the given material if any. + * + * @param material block type to check + * @return Optional containing a BlockBoost if configured + */ + public Optional findBoost(final Material material) { + return Optional.ofNullable(boostsByMaterial.get(material)); + } + + /** + * Indicates if the action bar should be displayed during boost. + * + * @return true when showing the action bar message is enabled + */ + public boolean isShowActionBar() { + return showActionBar; + } + + /** + * Returns the translated action bar message. + * + * @return message string + */ + public String getActionBarMessage() { + return actionBarMessage; + } + + private static double toDouble(final Object value, final double def) { + if (value instanceof Number number) return number.doubleValue(); + if (value == null) return def; + try { + return Double.parseDouble(String.valueOf(value)); + } catch (Exception ignored) { + return def; + } + } +} diff --git a/src/main/java/gemesil/fasterpathways/core/MovementListener.java b/src/main/java/gemesil/fasterpathways/core/MovementListener.java new file mode 100644 index 0000000..1346895 --- /dev/null +++ b/src/main/java/gemesil/fasterpathways/core/MovementListener.java @@ -0,0 +1,120 @@ +package gemesil.fasterpathways.core; + +import gemesil.fasterpathways.core.ConfigManager.BlockBoost; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerTeleportEvent; + +import java.util.Optional; + +/** + * Listens for player movement and applies/removes attribute boosts + * based on the block under the player's feet or directly below. + */ +public final class MovementListener implements Listener { + + private final ConfigManager config; + private final BoostService boostService; + + /** + * Creates the movement listener. + * + * @param config configuration accessor + * @param boostService attribute application service + */ + public MovementListener(final ConfigManager config, final BoostService boostService) { + this.config = config; + this.boostService = boostService; + } + + /** + * Handles player movement. Evaluates blocks at feet and below every move. + * Does not early-return on "same block" to avoid Bedrock effect flicker. + * + * @param event PlayerMoveEvent + */ + @EventHandler + public void onPlayerMove(final PlayerMoveEvent event) { + if (event.getTo() == null) return; + + final Player player = event.getPlayer(); + if (config.isWorldDisabled(player.getWorld())) { + boostService.clearAttributeModifier(player); + return; + } + + final Optional boost = resolveBoostFor(player); + if (boost.isPresent()) { + final double multiplier = boost.get().multiplier; + boostService.ensureAttribute(player, multiplier); + + if (config.isShowActionBar()) { + player.spigot().sendMessage( + ChatMessageType.ACTION_BAR, + TextComponent.fromLegacyText(config.getActionBarMessage()) + ); + } + } else { + boostService.clearAttributeModifier(player); + } + } + + /** + * Clears attribute state on player teleport to avoid sticky modifiers. + * + * @param event PlayerTeleportEvent + */ + @EventHandler + public void onTeleport(final PlayerTeleportEvent event) { + boostService.clearAttributeModifier(event.getPlayer()); + } + + /** + * Clears attribute state on world change to avoid cross-world carryover. + * + * @param event PlayerChangedWorldEvent + */ + @EventHandler + public void onWorldChange(final PlayerChangedWorldEvent event) { + boostService.clearAttributeModifier(event.getPlayer()); + } + + /** + * Clears attribute state on quit. + * + * @param event PlayerQuitEvent + */ + @EventHandler + public void onQuit(final PlayerQuitEvent event) { + boostService.clearAttributeModifier(event.getPlayer()); + } + + /** + * Resolves the active boost for the player by checking the block at the player's feet + * and the block directly below. + * + * @param player player whose position to evaluate + * @return optional BlockBoost when configured for either block + */ + private Optional resolveBoostFor(final Player player) { + final Block feetBlock = player.getLocation().getBlock(); + final Block belowBlock = feetBlock.getRelative(BlockFace.DOWN); + + final Material feetType = feetBlock.getType(); + final Material belowType = belowBlock.getType(); + + Optional byFeet = config.findBoost(feetType); + if (byFeet.isPresent()) return byFeet; + + return config.findBoost(belowType); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 1d244de..236e760 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,24 +1,31 @@ -# FasterPathways Configuration - -# Speed effect settings -speed: - # Set this to the level of the speed effect you'd like to be applied when a player steps on a dirt_path. Default is 2. - level: 3 - # The amount of time (in seconds) that the speed effect will be applied after stepping on a dirt path. Default is 1. - duration: 1 - -# Messages messages: - # The message that will be displayed in a player's hot bar while walking on a dirt path block (supports color codes with &) - actionBar: "&eTravel blocks are causing you to move faster!" # Whether to show the action bar message showActionBar: true -# List of blocks that will give speed effect -# Use Bukkit material names (see: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html) -speedBlocks: - - DIRT_PATH + # The message that will be displayed in a player's hot bar while walking on a dirt path block (supports color codes with &) + actionBar: "&eYou're moving faster!" # Worlds where the speed effect is disabled, you can specify "disabledWorlds: []" to allow it on all worlds -disabledWorlds: - - example_world \ No newline at end of file +disabledWorlds: [] + +# Preferred per-block configuration (takes priority if present) +# multiplier: 0.20 -> +20% movement speed +# Use Bukkit material names to specify blocks (see: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html) +blocks: + - material: DIRT_PATH + multiplier: 0.25 +# Additional examples: +# - material: STONE_BRICKS +# multiplier: 0.10 +# - material: PACKED_ICE +# multiplier: 0.35 + +# ------------------------------------------------------------- +# **Legacy fallback** +# if "blocks" is empty/missing: applies the same multiplier to all listed blocks. +# ------------------------------------------------------------- +speed: + # How fast do you want your players to be when stepping on the speed blocks + multiplier: 0.20 +speedBlocks: + - DIRT_PATH From 5dcd63a86425211fdc126fc596fe93e27e8f0854 Mon Sep 17 00:00:00 2001 From: Ofek B Date: Thu, 14 Aug 2025 02:08:39 +0300 Subject: [PATCH 2/3] fix: correct some of the modules --- pom.xml | 2 +- .../fasterpathways/FasterPathways.java | 2 +- .../fasterpathways/core/BoostService.java | 28 ++++++++++++------- .../fasterpathways/core/ConfigManager.java | 26 ++++++++--------- .../fasterpathways/core/MovementListener.java | 2 +- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index bd43cd1..2fc14b8 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ org.spigotmc spigot-api - 1.21.4-R0.1-SNAPSHOT + 1.21.8-R0.1-SNAPSHOT provided diff --git a/src/main/java/gemesil/fasterpathways/FasterPathways.java b/src/main/java/gemesil/fasterpathways/FasterPathways.java index 86df047..474cc4e 100644 --- a/src/main/java/gemesil/fasterpathways/FasterPathways.java +++ b/src/main/java/gemesil/fasterpathways/FasterPathways.java @@ -35,7 +35,7 @@ public void onEnable() { public void onDisable() { // Ensure no one remains boosted after disable/reload for (Player onlinePlayer : getServer().getOnlinePlayers()) { - if (onlinePlayer.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED) != null) { + if (onlinePlayer.getAttribute(Attribute.MOVEMENT_SPEED) != null) { boostService.clearAttributeModifier(onlinePlayer); } } diff --git a/src/main/java/gemesil/fasterpathways/core/BoostService.java b/src/main/java/gemesil/fasterpathways/core/BoostService.java index 3f1a68e..8efaf2d 100644 --- a/src/main/java/gemesil/fasterpathways/core/BoostService.java +++ b/src/main/java/gemesil/fasterpathways/core/BoostService.java @@ -6,7 +6,9 @@ import org.bukkit.entity.Player; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; /** * Applies and clears the movement speed attribute modifier in a stable, idempotent way. @@ -25,11 +27,11 @@ public final class BoostService { * @param multiplier +0.20 for +20% speed */ public void ensureAttribute(final Player player, final double multiplier) { + final UUID playerId = player.getUniqueId(); // this is fine on your API final double clamped = Math.max(0.0, multiplier); - final Double current = appliedMultiplierByPlayer.get(player.getUniqueId()); - if (current != null && Math.abs(current - clamped) < 1e-9) { - return; // already applied with the same value - } + + final Double current = appliedMultiplierByPlayer.get(playerId); + if (current != null && Math.abs(current - clamped) < 1e-9) return; clearAttributeModifier(player); addAttributeModifier(player, clamped); @@ -37,6 +39,7 @@ public void ensureAttribute(final Player player, final double multiplier) { /** * Removes the plugin's attribute modifier from the player if present. + * Avoids deprecated AttributeModifier#getUniqueId() by removing by name. * * @param player target player */ @@ -44,9 +47,9 @@ public void clearAttributeModifier(final Player player) { final AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); if (instance == null) return; - final UUID modifierId = stableUuidFor(player.getUniqueId()); + // Remove any modifier we previously added by NAME (non-deprecated path) instance.getModifiers().stream() - .filter(mod -> mod.getUniqueId().equals(modifierId) || MODIFIER_NAME.equals(mod.getName())) + .filter(mod -> MODIFIER_NAME.equals(mod.getName())) .toList() .forEach(instance::removeModifier); @@ -64,19 +67,24 @@ private void addAttributeModifier(final Player player, final double multiplier) final AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); if (instance == null) return; - final UUID modifierId = stableUuidFor(player.getUniqueId()); + final UUID modifierId = stableModifierUuidFor(player.getUniqueId()); + final AttributeModifier modifier = new AttributeModifier( modifierId, MODIFIER_NAME, multiplier, AttributeModifier.Operation.ADD_SCALAR ); + instance.addModifier(modifier); appliedMultiplierByPlayer.put(player.getUniqueId(), multiplier); } - private static UUID stableUuidFor(final UUID playerUniqueId) { - final String seed = "fasterpathways-" + playerUniqueId; + /** + * Deterministic UUID for this plugin's modifier per player. + */ + private static UUID stableModifierUuidFor(final UUID playerUuid) { + final String seed = "fasterpathways-" + playerUuid; return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)); } } diff --git a/src/main/java/gemesil/fasterpathways/core/ConfigManager.java b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java index 35a2bcc..7165aa6 100644 --- a/src/main/java/gemesil/fasterpathways/core/ConfigManager.java +++ b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java @@ -20,22 +20,17 @@ public final class ConfigManager { * Immutable per-block configuration entry. * multiplier: 0.20 means +20% movement speed. */ - public static final class BlockBoost { - public final Material material; - public final double multiplier; - + public record BlockBoost(Material material, double multiplier) { public BlockBoost(final Material material, final double multiplier) { this.material = material; this.multiplier = Math.max(0.0, multiplier); } } - private final Logger logger; private final Set disabledWorldNames = new HashSet<>(); private final Map boostsByMaterial = new EnumMap<>(Material.class); private final boolean showActionBar; private final String actionBarMessage; - private final double defaultMultiplier; /** * Constructs and loads configuration. @@ -45,7 +40,6 @@ public BlockBoost(final Material material, final double multiplier) { * @param server Bukkit server (used to warn on missing worlds) */ public ConfigManager(final FileConfiguration cfg, final Logger logger, final Server server) { - this.logger = logger; // Messages this.showActionBar = cfg.getBoolean("messages.showActionBar", true); @@ -54,7 +48,7 @@ public ConfigManager(final FileConfiguration cfg, final Logger logger, final Ser ); // Default multiplier (legacy/global fallback) - this.defaultMultiplier = Math.max(0.0, cfg.getDouble("speed.multiplier", 0.20)); + double defaultMultiplier = Math.max(0.0, cfg.getDouble("speed.multiplier", 0.20)); // Disabled worlds this.disabledWorldNames.addAll(cfg.getStringList("disabledWorlds")); @@ -67,14 +61,18 @@ public ConfigManager(final FileConfiguration cfg, final Logger logger, final Ser // Preferred per-block format if (cfg.isList("blocks")) { - for (Map rawEntry : cfg.getMapList("blocks")) { - if (!(rawEntry instanceof Map entry)) continue; - String materialName = String.valueOf(entry.getOrDefault("material", "")).toUpperCase(Locale.ROOT); + for (Object rawEntry : cfg.getMapList("blocks")) { + if (!(rawEntry instanceof Map)) continue; + + @SuppressWarnings("unchecked") final Map entry = (Map) rawEntry; + + final String materialName = String.valueOf(entry.getOrDefault("material", "")) + .toUpperCase(Locale.ROOT); if (materialName.isBlank()) continue; try { - Material material = Material.valueOf(materialName); - double multiplier = toDouble(entry.get("multiplier"), defaultMultiplier); + final Material material = Material.valueOf(materialName); + final double multiplier = toDouble(entry.get("multiplier"), defaultMultiplier); boostsByMaterial.put(material, new BlockBoost(material, multiplier)); } catch (IllegalArgumentException ex) { logger.log(Level.WARNING, "FasterPathways | Invalid material in blocks: " + materialName); @@ -85,7 +83,7 @@ public ConfigManager(final FileConfiguration cfg, final Logger logger, final Ser // Legacy fallback if no per-block entries present if (boostsByMaterial.isEmpty()) { List legacyList = cfg.getStringList("speedBlocks"); - if (legacyList == null || legacyList.isEmpty()) { + if (legacyList.isEmpty()) { legacyList = Collections.singletonList("DIRT_PATH"); logger.warning("FasterPathways | No blocks configured; adding DIRT_PATH as default"); } diff --git a/src/main/java/gemesil/fasterpathways/core/MovementListener.java b/src/main/java/gemesil/fasterpathways/core/MovementListener.java index 1346895..363c20c 100644 --- a/src/main/java/gemesil/fasterpathways/core/MovementListener.java +++ b/src/main/java/gemesil/fasterpathways/core/MovementListener.java @@ -54,7 +54,7 @@ public void onPlayerMove(final PlayerMoveEvent event) { final Optional boost = resolveBoostFor(player); if (boost.isPresent()) { - final double multiplier = boost.get().multiplier; + final double multiplier = boost.get().multiplier(); boostService.ensureAttribute(player, multiplier); if (config.isShowActionBar()) { From c5b7433f8f3b1345109da05b8a92f925ef46baf6 Mon Sep 17 00:00:00 2001 From: Ofek B Date: Thu, 14 Aug 2025 20:42:51 +0300 Subject: [PATCH 3/3] feat: add move_speed from attributes, make code more robust, update config.yml --- pom.xml | 2 +- .../fasterpathways/FasterPathways.java | 5 +- .../fasterpathways/core/BoostService.java | 133 ++++++++++-------- .../fasterpathways/core/ConfigManager.java | 31 +--- .../fasterpathways/core/MovementListener.java | 77 +++------- src/main/resources/config.yml | 46 +++--- 6 files changed, 127 insertions(+), 167 deletions(-) diff --git a/pom.xml b/pom.xml index 2fc14b8..172a43e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ gemesil FasterPathways - 1.3 + 1.4 jar FasterPathways diff --git a/src/main/java/gemesil/fasterpathways/FasterPathways.java b/src/main/java/gemesil/fasterpathways/FasterPathways.java index 474cc4e..5af78c9 100644 --- a/src/main/java/gemesil/fasterpathways/FasterPathways.java +++ b/src/main/java/gemesil/fasterpathways/FasterPathways.java @@ -27,8 +27,6 @@ public void onEnable() { new MovementListener(configManager, boostService), this ); - - getLogger().info("FasterPathways | Enabled (attribute-only)"); } @Override @@ -36,10 +34,9 @@ public void onDisable() { // Ensure no one remains boosted after disable/reload for (Player onlinePlayer : getServer().getOnlinePlayers()) { if (onlinePlayer.getAttribute(Attribute.MOVEMENT_SPEED) != null) { - boostService.clearAttributeModifier(onlinePlayer); + boostService.removeSpeedMultiplier(onlinePlayer); } } - boostService.clearAll(); } /** diff --git a/src/main/java/gemesil/fasterpathways/core/BoostService.java b/src/main/java/gemesil/fasterpathways/core/BoostService.java index 8efaf2d..e8cd8d2 100644 --- a/src/main/java/gemesil/fasterpathways/core/BoostService.java +++ b/src/main/java/gemesil/fasterpathways/core/BoostService.java @@ -6,85 +6,108 @@ import org.bukkit.entity.Player; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.UUID; /** - * Applies and clears the movement speed attribute modifier in a stable, idempotent way. - * Uses a deterministic UUID per player so re-application does not stack. + * Manages a player's movement-speed attribute modifier (Spigot 1.21.8). + * Uses a deterministic UUID per player to avoid duplicates and ensure safe replace/remove. */ public final class BoostService { - private static final String MODIFIER_NAME = "FasterPathways"; - private final Map appliedMultiplierByPlayer = new HashMap<>(); + private static final String SPEED_MODIFIER_NAME_PREFIX = "fasterpathways.speed:"; + private static final String LEGACY_NAME = "FasterPathways"; // cleanup from older builds /** - * Ensures the player has the specified multiplier applied. - * If a different value is already present, it will be replaced. - * - * @param player target player - * @param multiplier +0.20 for +20% speed + * Apply or update a +X% speed modifier. No-ops if the same value is already applied. */ - public void ensureAttribute(final Player player, final double multiplier) { - final UUID playerId = player.getUniqueId(); // this is fine on your API - final double clamped = Math.max(0.0, multiplier); - - final Double current = appliedMultiplierByPlayer.get(playerId); - if (current != null && Math.abs(current - clamped) < 1e-9) return; - - clearAttributeModifier(player); - addAttributeModifier(player, clamped); + public void applySpeedMultiplier(final Player player, final double multiplier) { + final AttributeInstance attr = player.getAttribute(Attribute.MOVEMENT_SPEED); + if (attr == null) return; + + final double amount = Math.max(0.0, multiplier); + final UUID targetId = stableModifierUuid(player); + final String targetName = speedModifierNameFor(player); + + // Check if the correct modifier is already present (same UUID + amount). + final AttributeModifier current = findByUuid(attr, targetId); + if (current != null + && current.getOperation() == AttributeModifier.Operation.ADD_SCALAR + && Math.abs(current.getAmount() - amount) < 1e-9) { + return; // already correct + } + + // Remove any prior copies that match our UUID or known names, then add the new value. + removeSpeedModifiers(attr, targetId, targetName); + + // Spigot exposes only the UUID constructor at runtime + final AttributeModifier updated = new AttributeModifier( + targetId, + targetName, + amount, + AttributeModifier.Operation.ADD_SCALAR + ); + attr.addModifier(updated); } /** - * Removes the plugin's attribute modifier from the player if present. - * Avoids deprecated AttributeModifier#getUniqueId() by removing by name. - * - * @param player target player + * Remove the speed modifier for this player (if present). */ - public void clearAttributeModifier(final Player player) { - final AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); - if (instance == null) return; + public void removeSpeedMultiplier(final Player player) { + final AttributeInstance attr = player.getAttribute(Attribute.MOVEMENT_SPEED); + if (attr == null) return; - // Remove any modifier we previously added by NAME (non-deprecated path) - instance.getModifiers().stream() - .filter(mod -> MODIFIER_NAME.equals(mod.getName())) - .toList() - .forEach(instance::removeModifier); + final UUID targetId = stableModifierUuid(player); + final String targetName = speedModifierNameFor(player); + removeSpeedModifiers(attr, targetId, targetName); + } - appliedMultiplierByPlayer.remove(player.getUniqueId()); + // ---------- helpers ---------- + + private static String speedModifierNameFor(final Player player) { + return SPEED_MODIFIER_NAME_PREFIX + player.getUniqueId(); + } + + private static UUID stableModifierUuid(final Player player) { + final String seed = "fasterpathways:" + player.getUniqueId(); + return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)); } /** - * Clears internal bookkeeping. Should be called on plugin disable. + * Find an existing modifier by UUID (uses deprecated getter, isolated here). */ - public void clearAll() { - appliedMultiplierByPlayer.clear(); + private static AttributeModifier findByUuid(final AttributeInstance attr, final UUID uuid) { + for (AttributeModifier m : attr.getModifiers()) { + if (uuidEquals(m, uuid)) { + return m; + } + } + return null; } - private void addAttributeModifier(final Player player, final double multiplier) { - final AttributeInstance instance = player.getAttribute(Attribute.MOVEMENT_SPEED); - if (instance == null) return; - - final UUID modifierId = stableModifierUuidFor(player.getUniqueId()); - - final AttributeModifier modifier = new AttributeModifier( - modifierId, - MODIFIER_NAME, - multiplier, - AttributeModifier.Operation.ADD_SCALAR - ); - - instance.addModifier(modifier); - appliedMultiplierByPlayer.put(player.getUniqueId(), multiplier); + /** + * Remove any modifier that matches our UUID or our known names (current + legacy). + */ + private static void removeSpeedModifiers(final AttributeInstance attr, final UUID uuid, final String name) { + final List toRemove = new ArrayList<>(); + for (AttributeModifier m : attr.getModifiers()) { + final boolean sameUuid = uuidEquals(m, uuid); + final boolean sameName = Objects.equals(m.getName(), name) || Objects.equals(m.getName(), LEGACY_NAME); + if (sameUuid || sameName) { + toRemove.add(m); + } + } + for (AttributeModifier m : toRemove) { + attr.removeModifier(m); + } } /** - * Deterministic UUID for this plugin's modifier per player. + * Compare via deprecated getter; isolated so the rest of the code stays modern. */ - private static UUID stableModifierUuidFor(final UUID playerUuid) { - final String seed = "fasterpathways-" + playerUuid; - return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)); + private static boolean uuidEquals(final AttributeModifier modifier, final UUID expected) { + return modifier.getUniqueId().equals(expected); } } diff --git a/src/main/java/gemesil/fasterpathways/core/ConfigManager.java b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java index 7165aa6..3e4e374 100644 --- a/src/main/java/gemesil/fasterpathways/core/ConfigManager.java +++ b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java @@ -47,15 +47,12 @@ public ConfigManager(final FileConfiguration cfg, final Logger logger, final Ser '&', cfg.getString("messages.actionBar", "&eYou're moving faster!") ); - // Default multiplier (legacy/global fallback) - double defaultMultiplier = Math.max(0.0, cfg.getDouble("speed.multiplier", 0.20)); - // Disabled worlds this.disabledWorldNames.addAll(cfg.getStringList("disabledWorlds")); for (String worldName : disabledWorldNames) { World world = server.getWorld(worldName); if (world == null) { - logger.warning("FasterPathways | World not found: " + worldName); + logger.warning("World not found: " + worldName); } } @@ -66,33 +63,17 @@ public ConfigManager(final FileConfiguration cfg, final Logger logger, final Ser @SuppressWarnings("unchecked") final Map entry = (Map) rawEntry; - final String materialName = String.valueOf(entry.getOrDefault("material", "")) - .toUpperCase(Locale.ROOT); + final String materialName = String.valueOf(entry.getOrDefault("material", "")).toUpperCase(Locale.ROOT); if (materialName.isBlank()) continue; try { final Material material = Material.valueOf(materialName); - final double multiplier = toDouble(entry.get("multiplier"), defaultMultiplier); - boostsByMaterial.put(material, new BlockBoost(material, multiplier)); - } catch (IllegalArgumentException ex) { - logger.log(Level.WARNING, "FasterPathways | Invalid material in blocks: " + materialName); - } - } - } + final double multiplier = toDouble(entry.get("multiplier"), 0); + if (multiplier == 0) continue; - // Legacy fallback if no per-block entries present - if (boostsByMaterial.isEmpty()) { - List legacyList = cfg.getStringList("speedBlocks"); - if (legacyList.isEmpty()) { - legacyList = Collections.singletonList("DIRT_PATH"); - logger.warning("FasterPathways | No blocks configured; adding DIRT_PATH as default"); - } - for (String name : legacyList) { - try { - Material material = Material.valueOf(name.toUpperCase(Locale.ROOT)); - boostsByMaterial.put(material, new BlockBoost(material, defaultMultiplier)); + boostsByMaterial.put(material, new BlockBoost(material, multiplier)); } catch (IllegalArgumentException ex) { - logger.log(Level.WARNING, "FasterPathways | Invalid material in speedBlocks: " + name); + logger.log(Level.WARNING, "Invalid material in blocks: " + materialName); } } } diff --git a/src/main/java/gemesil/fasterpathways/core/MovementListener.java b/src/main/java/gemesil/fasterpathways/core/MovementListener.java index 363c20c..c124772 100644 --- a/src/main/java/gemesil/fasterpathways/core/MovementListener.java +++ b/src/main/java/gemesil/fasterpathways/core/MovementListener.java @@ -2,6 +2,7 @@ import gemesil.fasterpathways.core.ConfigManager.BlockBoost; import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Material; import org.bukkit.block.Block; @@ -16,105 +17,69 @@ import java.util.Optional; -/** - * Listens for player movement and applies/removes attribute boosts - * based on the block under the player's feet or directly below. - */ public final class MovementListener implements Listener { private final ConfigManager config; private final BoostService boostService; - /** - * Creates the movement listener. - * - * @param config configuration accessor - * @param boostService attribute application service - */ public MovementListener(final ConfigManager config, final BoostService boostService) { this.config = config; this.boostService = boostService; } - /** - * Handles player movement. Evaluates blocks at feet and below every move. - * Does not early-return on "same block" to avoid Bedrock effect flicker. - * - * @param event PlayerMoveEvent - */ @EventHandler public void onPlayerMove(final PlayerMoveEvent event) { if (event.getTo() == null) return; final Player player = event.getPlayer(); if (config.isWorldDisabled(player.getWorld())) { - boostService.clearAttributeModifier(player); + boostService.removeSpeedMultiplier(player); return; } final Optional boost = resolveBoostFor(player); if (boost.isPresent()) { final double multiplier = boost.get().multiplier(); - boostService.ensureAttribute(player, multiplier); + boostService.applySpeedMultiplier(player, multiplier); if (config.isShowActionBar()) { - player.spigot().sendMessage( - ChatMessageType.ACTION_BAR, - TextComponent.fromLegacyText(config.getActionBarMessage()) - ); + final String legacy = org.bukkit.ChatColor + .translateAlternateColorCodes('&', config.getActionBarMessage()); + + // If you want a default color applied, use the 2-arg overload: + // final TextComponent comp = TextComponent.fromLegacy(legacy, ChatColor.WHITE); + + final BaseComponent comp = TextComponent.fromLegacy(legacy); + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, comp); // accepts a single BaseComponent } } else { - boostService.clearAttributeModifier(player); + boostService.removeSpeedMultiplier(player); } } - /** - * Clears attribute state on player teleport to avoid sticky modifiers. - * - * @param event PlayerTeleportEvent - */ @EventHandler public void onTeleport(final PlayerTeleportEvent event) { - boostService.clearAttributeModifier(event.getPlayer()); + boostService.removeSpeedMultiplier(event.getPlayer()); } - /** - * Clears attribute state on world change to avoid cross-world carryover. - * - * @param event PlayerChangedWorldEvent - */ @EventHandler public void onWorldChange(final PlayerChangedWorldEvent event) { - boostService.clearAttributeModifier(event.getPlayer()); + boostService.removeSpeedMultiplier(event.getPlayer()); } - /** - * Clears attribute state on quit. - * - * @param event PlayerQuitEvent - */ @EventHandler public void onQuit(final PlayerQuitEvent event) { - boostService.clearAttributeModifier(event.getPlayer()); + boostService.removeSpeedMultiplier(event.getPlayer()); } - /** - * Resolves the active boost for the player by checking the block at the player's feet - * and the block directly below. - * - * @param player player whose position to evaluate - * @return optional BlockBoost when configured for either block - */ private Optional resolveBoostFor(final Player player) { - final Block feetBlock = player.getLocation().getBlock(); - final Block belowBlock = feetBlock.getRelative(BlockFace.DOWN); - - final Material feetType = feetBlock.getType(); - final Material belowType = belowBlock.getType(); + final Block feet = player.getLocation().getBlock(); + final Block below = feet.getRelative(BlockFace.DOWN); - Optional byFeet = config.findBoost(feetType); - if (byFeet.isPresent()) return byFeet; + final Material feetType = feet.getType(); + final Material belowType = below.getType(); - return config.findBoost(belowType); + final Optional byFeet = config.findBoost(feetType); + return byFeet.isPresent() ? byFeet : config.findBoost(belowType); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 236e760..5bff6c4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,31 +1,25 @@ -messages: - # Whether to show the action bar message - showActionBar: true +# ──── 🪨 Block Boost Settings ───────────────────────────────────────────────────────────── +# Preferred per-block configuration +# Use Bukkit material names: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html +blocks: + - material: DIRT_PATH + multiplier: 0.65 # 0.65 → +65% movement speed + # Additional example: + # - material: PACKED_ICE + # multiplier: 1.0 - # The message that will be displayed in a player's hot bar while walking on a dirt path block (supports color codes with &) - actionBar: "&eYou're moving faster!" -# Worlds where the speed effect is disabled, you can specify "disabledWorlds: []" to allow it on all worlds +# ──── 🌍 Disabled Worlds ────────────────────────────────────────────────────────────────── +# List worlds where the speed boost is disabled. Use [] to enable on all worlds. Or remove the [] and add your world names below. disabledWorlds: [] +# - world_example -# Preferred per-block configuration (takes priority if present) -# multiplier: 0.20 -> +20% movement speed -# Use Bukkit material names to specify blocks (see: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html) -blocks: - - material: DIRT_PATH - multiplier: 0.25 -# Additional examples: -# - material: STONE_BRICKS -# multiplier: 0.10 -# - material: PACKED_ICE -# multiplier: 0.35 -# ------------------------------------------------------------- -# **Legacy fallback** -# if "blocks" is empty/missing: applies the same multiplier to all listed blocks. -# ------------------------------------------------------------- -speed: - # How fast do you want your players to be when stepping on the speed blocks - multiplier: 0.20 -speedBlocks: - - DIRT_PATH +# ──── 💬 Messages (Action Bar) ──────────────────────────────────────────────────────────── +# Text shown to the player while walking on boosted blocks. '&' color codes supported. +messages: + # Whether to show the action bar message + showActionBar: false + + # Text displayed while on a boosted block + actionBar: "&eYou're moving faster!"