diff --git a/pom.xml b/pom.xml
index bd43cd1..172a43e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
gemesil
FasterPathways
- 1.3
+ 1.4
jar
FasterPathways
@@ -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 8092f21..5af78c9 100644
--- a/src/main/java/gemesil/fasterpathways/FasterPathways.java
+++ b/src/main/java/gemesil/fasterpathways/FasterPathways.java
@@ -1,104 +1,51 @@
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);
-
- // Load action bar settings
- actionBarMessage = ChatColor.translateAlternateColorCodes('&',
- config.getString("messages.actionBar", "&eYou're moving faster!"));
- showActionBar = config.getBoolean("messages.showActionBar", true);
+ reloadAll();
- // 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);
- }
- }
+ // Register movement listener
+ Bukkit.getPluginManager().registerEvents(
+ new MovementListener(configManager, boostService),
+ this
+ );
+ }
- // 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.MOVEMENT_SPEED) != null) {
+ boostService.removeSpeedMultiplier(onlinePlayer);
}
}
-
- if (speedBlocks.isEmpty()) {
- getLogger().warning("FasterPathways | No valid speed blocks configured! Adding DIRT_PATH as default.");
- speedBlocks.add(Material.DIRT_PATH);
- }
}
- @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..e8cd8d2
--- /dev/null
+++ b/src/main/java/gemesil/fasterpathways/core/BoostService.java
@@ -0,0 +1,113 @@
+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.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * 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 SPEED_MODIFIER_NAME_PREFIX = "fasterpathways.speed:";
+ private static final String LEGACY_NAME = "FasterPathways"; // cleanup from older builds
+
+ /**
+ * Apply or update a +X% speed modifier. No-ops if the same value is already applied.
+ */
+ 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);
+ }
+
+ /**
+ * Remove the speed modifier for this player (if present).
+ */
+ public void removeSpeedMultiplier(final Player player) {
+ final AttributeInstance attr = player.getAttribute(Attribute.MOVEMENT_SPEED);
+ if (attr == null) return;
+
+ final UUID targetId = stableModifierUuid(player);
+ final String targetName = speedModifierNameFor(player);
+ removeSpeedModifiers(attr, targetId, targetName);
+ }
+
+ // ---------- 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));
+ }
+
+ /**
+ * Find an existing modifier by UUID (uses deprecated getter, isolated here).
+ */
+ private static AttributeModifier findByUuid(final AttributeInstance attr, final UUID uuid) {
+ for (AttributeModifier m : attr.getModifiers()) {
+ if (uuidEquals(m, uuid)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Compare via deprecated getter; isolated so the rest of the code stays modern.
+ */
+ 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
new file mode 100644
index 0000000..3e4e374
--- /dev/null
+++ b/src/main/java/gemesil/fasterpathways/core/ConfigManager.java
@@ -0,0 +1,129 @@
+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 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 Set disabledWorldNames = new HashSet<>();
+ private final Map boostsByMaterial = new EnumMap<>(Material.class);
+ private final boolean showActionBar;
+ private final String actionBarMessage;
+
+ /**
+ * 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) {
+
+ // Messages
+ this.showActionBar = cfg.getBoolean("messages.showActionBar", true);
+ this.actionBarMessage = ChatColor.translateAlternateColorCodes(
+ '&', cfg.getString("messages.actionBar", "&eYou're moving faster!")
+ );
+
+ // Disabled worlds
+ this.disabledWorldNames.addAll(cfg.getStringList("disabledWorlds"));
+ for (String worldName : disabledWorldNames) {
+ World world = server.getWorld(worldName);
+ if (world == null) {
+ logger.warning("World not found: " + worldName);
+ }
+ }
+
+ // Preferred per-block format
+ if (cfg.isList("blocks")) {
+ 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 {
+ final Material material = Material.valueOf(materialName);
+ final double multiplier = toDouble(entry.get("multiplier"), 0);
+ if (multiplier == 0) continue;
+
+ boostsByMaterial.put(material, new BlockBoost(material, multiplier));
+ } catch (IllegalArgumentException ex) {
+ logger.log(Level.WARNING, "Invalid material in blocks: " + materialName);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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..c124772
--- /dev/null
+++ b/src/main/java/gemesil/fasterpathways/core/MovementListener.java
@@ -0,0 +1,85 @@
+package gemesil.fasterpathways.core;
+
+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;
+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;
+
+public final class MovementListener implements Listener {
+
+ private final ConfigManager config;
+ private final BoostService boostService;
+
+ public MovementListener(final ConfigManager config, final BoostService boostService) {
+ this.config = config;
+ this.boostService = boostService;
+ }
+
+ @EventHandler
+ public void onPlayerMove(final PlayerMoveEvent event) {
+ if (event.getTo() == null) return;
+
+ final Player player = event.getPlayer();
+ if (config.isWorldDisabled(player.getWorld())) {
+ boostService.removeSpeedMultiplier(player);
+ return;
+ }
+
+ final Optional boost = resolveBoostFor(player);
+ if (boost.isPresent()) {
+ final double multiplier = boost.get().multiplier();
+ boostService.applySpeedMultiplier(player, multiplier);
+
+ if (config.isShowActionBar()) {
+ 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.removeSpeedMultiplier(player);
+ }
+ }
+
+ @EventHandler
+ public void onTeleport(final PlayerTeleportEvent event) {
+ boostService.removeSpeedMultiplier(event.getPlayer());
+ }
+
+ @EventHandler
+ public void onWorldChange(final PlayerChangedWorldEvent event) {
+ boostService.removeSpeedMultiplier(event.getPlayer());
+ }
+
+ @EventHandler
+ public void onQuit(final PlayerQuitEvent event) {
+ boostService.removeSpeedMultiplier(event.getPlayer());
+ }
+
+ private Optional resolveBoostFor(final Player player) {
+ final Block feet = player.getLocation().getBlock();
+ final Block below = feet.getRelative(BlockFace.DOWN);
+
+ final Material feetType = feet.getType();
+ final Material belowType = below.getType();
+
+ 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 1d244de..5bff6c4 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,24 +1,25 @@
-# FasterPathways Configuration
+# ──── 🪨 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
-# 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
+# ──── 🌍 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
+
+
+# ──── 💬 Messages (Action Bar) ────────────────────────────────────────────────────────────
+# Text shown to the player while walking on boosted blocks. '&' color codes supported.
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
+ showActionBar: false
-# 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
+ # Text displayed while on a boosted block
+ actionBar: "&eYou're moving faster!"