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!"