From c72e78903dbee8e54a0a929bf95b7c80c38afcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Veill=C3=A9?= Date: Mon, 26 Oct 2026 12:50:27 +0100 Subject: [PATCH 1/6] Create optimisation_roadmap.md --- docs/optimisation_roadmap.md | 335 +++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 docs/optimisation_roadmap.md diff --git a/docs/optimisation_roadmap.md b/docs/optimisation_roadmap.md new file mode 100644 index 0000000..29fc9e3 --- /dev/null +++ b/docs/optimisation_roadmap.md @@ -0,0 +1,335 @@ +# Roadmap de correction et d'optimisation du plugin SMPplugin + +## Objectif + +Ce document résume les points de friction actuels du plugin, les causes de performance, et la roadmap recommandée pour améliorer l'architecture sans réécrire tout le projet d'un coup. + +Le projet a une bonne base structurale, mais plusieurs zones critiques sont encore trop "hot path" et doivent être refactorisées pour rester scalable et maintenable. + +--- + +## 1. Diagnostic rapide + +### Points forts + +- Structure logique par modules : `core`, `modules/perk`, `modules/spell`, `listeners`, `commands`. +- Séparation des listeners, managers et logique de gameplay. +- Le repo a une vraie intention architecturale, ce qui est bon pour la maintenabilité. +- Les systèmes de perks et de spells sont bien pensés sur le plan conceptuel. + +### Points faibles + +- `PerkTasks` exécute trop de travail dans une boucle tick-based. +- Le plugin recalcul souvent les niveaux/perks à partir des items PDC. +- Les spells deviennent vite monolithiques et contiennent trop de logique métier dans une même classe. +- Les classes de gestion globale (ex. `CorePlugin`, `PerkManager`, `AbstractPerk`) portent trop de responsabilités. +- Le design est encore orienté "prototype / MVP" plutôt que "système extensible et optimisé". + +--- + +## 2. Problèmes de performance observés + +### 2.1 Boucle de tick trop heavy + +La classe `PerkTasks` est la plus critique. + +Actuellement, le code parcourt les joueurs et répète plusieurs opérations à chaque tick : + +- lecture du niveau des perks sur l'armure +- analyse du PDC des items +- traitement de la vision nocturne +- traitement de l'aimant +- traitement du double saut + +Cela génère du travail inutile sur chaque tick et est coûteux sur un serveur Minecraft. + +### 2.2 Recalcul constant des données armure/perk + +Les méthodes comme `getArmorPerkLevels()` et `getEquippedPerk()` relisent souvent les données à partir des items. + +Le problème n'est pas seulement que cela est lent ; c'est surtout que cela se répète dans des chemins très critiques, alors que les données ne changent pas souvent. + +### 2.3 Les effets visuels et de gameplay sont trop imbriqués + +Dans `FlameSpell`, la logique mélange : + +- position de départ +- direction +- particules +- collision +- dégâts +- knockback +- cooldown + +C'est faisable pour 1 ou 2 sorts, mais ça ne scale pas quand il y a beaucoup de sorts, projectiles et effets. + +### 2.4 Architecture trop centralisée + +`CorePlugin` agit comme une classe de bootstrap + manager + orchestrateur + lifecycle + données globales. + +Ce n'est pas fatal, mais cela limite : + +- l'extensibilité +- la testabilité +- le découpage des responsabilités +- la capacité à ajouter des services sans faire grossir la classe de manière incontrôlée + +--- + +## 3. Architecture cible recommandée + +L'objectif est de passer à un modèle plus robuste : + +- registry / manager +- cache d'état du joueur +- services dédiés +- contexte d'exécution +- scheduler unique pour les effets + +### Structure cible + +```text +modules/ + perk/ + PerkManager.java + PerkCache.java + PerkSnapshot.java + services/ + NightVisionService.java + MagnetService.java + DoubleJumpService.java + + spell/ + SpellManager.java + SpellRegistry.java + SpellContext.java + SpellCooldownService.java + SpellExecutionService.java + type/ + FlameSpell.java + FireballSpell.java + BeamSpell.java + + listeners/ + InventoryListener.java + ArmorListener.java + PlayerJoinListener.java +``` + +### Principe clé + +Le `hot path` ne doit plus reparcourir les items et relire le PDC à chaque tick. + +Il faut : + +- invalider le cache lors d'un changement d'armure / inventaire / reload +- recalculer les données seulement quand nécessaire +- lire uniquement les données déjà mises en cache pendant le runtime + +--- + +## 4. Roadmap prioritaire + +## Phase 1 — Corriger les points de perf bloquants + +### Tâches + +- Introduire un `PlayerPerkCache` ou `PerkSnapshot`. +- Invalider le cache sur : + - join + - leave + - armor equip / unequip + - changement d'inventaire + - reload +- Réduire le travail de `PerkTasks.run()`. +- Remplacer les recalculs complets par un accès au snapshot. +- Limiter les traitements d'aimant et de vision nocturne à des conditions plus strictes. + +### Livrables + +- `PerkCache` ajouté +- `markDirty()` / invalidation centralisée +- `PerkTasks` ne lit plus les items PDC à chaque tick + +--- + +## Phase 2 — Séparer les responsabilités + +### Tâches + +- Créer des services séparés pour : + - `NightVisionService` + - `MagnetService` + - `DoubleJumpService` +- Déplacer la logique de compétence hors des classes de gestion globales. +- Rendre `PerkManager` plus "registry" et moins "traitement de tout". +- Faire de `AbstractPerk` une abstraction plus légère. + +### Livrables + +- plus de classes petites et spécialisées +- logique de gameplay isolée dans ses services +- code plus facile à tester + +--- + +## Phase 3 — Refactor des spells + +### Tâches + +- Ajouter `SpellContext`. +- Ajouter `SpellCooldownService`. +- Ajouter `SpellExecutionService`. +- Séparer : + - config du spell + - état runtime + - logique d'exécution + - effets visuels +- Centraliser la gestion des projectiles / effets d'aoe / faisceaux. + +### Livrables + +- un système de spell extensible +- chaque spell ne gère que son propre comportement +- moins de duplicated logic + +--- + +## Phase 4 — Refactor architecture globale + +### Tâches + +- Réduire la responsabilité de `CorePlugin`. +- Déplacer les initialisations dans une structure de bootstrap propre. +- Séparer bootstrap, managers, runtime state et lifecycle. +- Mettre en place un vrai service layer si le projet grandit. + +### Livrables + +- plugin plus propre à maintenir +- initialisation plus stable +- reload plus robuste + +--- + +## 5. Points à corriger dans le code actuel + +### `PerkTasks.java` + +Priorité absolue. + +À faire : + +- supprimer les recalculs complets dans `run()` +- utiliser un cache de state joueur +- réduire la fréquence de certains traitements +- séparer les services + +### `PerkManager.java` + +À faire : + +- garder la registry, mais diminuer le travail de parsing dans les chemins fréquents +- ajouter un snapshot par joueur +- ne plus reconstruire les données à partir des items à chaque tick + +### `AbstractPerk.java` + +À faire : + +- séparer config et runtime effect logic +- réduire la taille de cette classe +- garder seulement les éléments centraux à la définition du perk + +### `AbstractSpell.java` + +À faire : + +- corriger la logique de permission et cooldown +- centraliser le cooldown dans un service dédié +- éviter les dépendances directes trop lourdes dans les classes dérivées + +### `FlameSpell.java` + +À faire : + +- séparer le comportement d'impact, la création du projectile et les effets visuels +- utiliser un gestionnaire central de projectiles / effets +- remplacer la logique monolithique par un système plus extensible + +--- + +## 6. Priorités recommandées + +### Priorité 1 : performance + +1. `PerkTasks` +2. `PerkManager` +3. invalidation de cache + +### Priorité 2 : architecture + +4. `AbstractPerk` +5. `AbstractSpell` +6. `CorePlugin` + +### Priorité 3 : extensibilité + +7. `FlameSpell` +8. Spell execution system +9. services centralisés + +--- + +## 7. Verdict final + +Le repo est déjà bien parti, mais il a encore un défaut majeur : il est trop dépendant des boucles périodiques et du recalcul de données que l'on ne devrait pas recalculer à chaque tick. + +La meilleure stratégie n'est pas de réécrire tout le plugin, mais de faire un refactor ciblé sur : + +- cache player state +- invalidation par événements +- services spécialisés +- centralisation des effets +- séparation config/runtime/execution + +C'est la bonne voie pour monter le système vers un niveau de maintenabilité et de performance nettement supérieur. + +--- + +## 8. Exemple de plan de refactor en 3 itérations + +### Itération 1 + +- `PerkCache` +- invalidation sur events +- `PerkTasks` plus léger + +### Itération 2 + +- `SpellCooldownService` +- `SpellContext` +- architecture de sorts plus modulaire + +### Itération 3 + +- `CorePlugin` moins chargé +- services d'effets plus centralisés +- architecture production-ready + +--- + +## 9. Conclusion + +Le code actuel est correct pour un plugin fonctionnel, mais pas encore pour un système de gameplay qui doit rester performant et extensible. + +Le meilleur retour sur investissement est de corriger en priorité : + +- `PerkTasks` +- `PerkManager` +- le cache des données liées aux perks +- la séparation des services de spell / cooldown / effects + +Si ces points sont traités correctement, le repo pourra passer d'un bon prototype à un plugin beaucoup plus solide et plus facile à évoluer. + +Si tu veux, je peux aussi te générer une version encore plus courte, ou une version “README-style” plus propre pour le dépôt. \ No newline at end of file From 0c64e5fe19f8e9ca66d4d6b5ec2239a4761a991b Mon Sep 17 00:00:00 2001 From: Antidino35 Date: Mon, 26 Oct 2026 15:23:20 +0100 Subject: [PATCH 2/6] fix(onEntityRemove) : Check if item is falling into the world --- .../antidino/smp/modules/listeners/SavingPerkListeners.java | 1 + .../java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java | 4 ++++ src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java | 4 ++++ 3 files changed, 9 insertions(+) create mode 100644 src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java create mode 100644 src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java diff --git a/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java b/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java index 620a756..fe403ca 100644 --- a/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java +++ b/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java @@ -40,6 +40,7 @@ public void onEntityDamage(EntityDamageEvent event) { @EventHandler public void onEntityRemove(EntityRemoveFromWorldEvent event) { if (!(event.getEntity() instanceof Item item)) return; + if (item.getLocation().getY() > 0) return; if (!isAPerkItem(item.getItemStack()))return; Location loc = item.getLocation(); World world = loc.getWorld(); diff --git a/src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java b/src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java new file mode 100644 index 0000000..27f3290 --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java @@ -0,0 +1,4 @@ +package fr.antidino.smp.modules.perk.modal; + +public class ArmorPerk { +} diff --git a/src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java b/src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java new file mode 100644 index 0000000..a2a462e --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java @@ -0,0 +1,4 @@ +package fr.antidino.smp.modules.perk.modal; + +public class Perk { +} From 4a349e26030cfb12c5c50476a83047e6d1e4a0ec Mon Sep 17 00:00:00 2001 From: Antidino35 Date: Mon, 26 Oct 2026 15:24:59 +0100 Subject: [PATCH 3/6] fix(onEntityRemove) : Check if item is falling into the world --- .../fr/antidino/smp/modules/listeners/SavingPerkListeners.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java b/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java index 620a756..7b2714f 100644 --- a/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java +++ b/src/main/java/fr/antidino/smp/modules/listeners/SavingPerkListeners.java @@ -41,6 +41,7 @@ public void onEntityDamage(EntityDamageEvent event) { public void onEntityRemove(EntityRemoveFromWorldEvent event) { if (!(event.getEntity() instanceof Item item)) return; if (!isAPerkItem(item.getItemStack()))return; + if (item.getLocation().getBlockY()>0) return; Location loc = item.getLocation(); World world = loc.getWorld(); Location chestLoc = new Location(world, loc.getBlockX(), SAFE_Y, loc.getBlockZ()); From 6c06fba47886c12531aee0864ad8ff0067b16083 Mon Sep 17 00:00:00 2001 From: Antidino35 Date: Mon, 26 Oct 2026 20:04:38 +0100 Subject: [PATCH 4/6] perf(perk): introduce in-memory perk cache to fix TPS drops - Cache player equipment perks in memory to replace redundant PDC reads - Update cache on armor change events instead of reading PDC every tick - Eliminate multiple perk evaluations per player in PerkTasks main loop --- src/main/java/fr/antidino/smp/CorePlugin.java | 13 +-- .../smp/modules/commands/AdminCommands.java | 1 + .../listeners/PlayerChangeArmorListener.java | 26 ++++++ ...Listener.java => PlayerJoinQuitEvent.java} | 13 ++- .../antidino/smp/modules/perk/PerkTasks.java | 50 ++++++++--- .../antidino/smp/modules/perk/PerksCache.java | 23 +++++ .../smp/modules/perk/PlayerPerkCache.java | 83 +++++++++++++++++++ .../smp/modules/perk/modal/ArmorPerk.java | 4 - .../antidino/smp/modules/perk/modal/Perk.java | 4 - 9 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 src/main/java/fr/antidino/smp/modules/listeners/PlayerChangeArmorListener.java rename src/main/java/fr/antidino/smp/modules/listeners/{PlayerJoinListener.java => PlayerJoinQuitEvent.java} (73%) create mode 100644 src/main/java/fr/antidino/smp/modules/perk/PerksCache.java create mode 100644 src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java delete mode 100644 src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java delete mode 100644 src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java diff --git a/src/main/java/fr/antidino/smp/CorePlugin.java b/src/main/java/fr/antidino/smp/CorePlugin.java index 7414a21..c7121a7 100644 --- a/src/main/java/fr/antidino/smp/CorePlugin.java +++ b/src/main/java/fr/antidino/smp/CorePlugin.java @@ -2,6 +2,7 @@ import fr.antidino.smp.modules.listeners.*; import fr.antidino.smp.core.MessageManager; +import fr.antidino.smp.modules.perk.*; import fr.antidino.smp.modules.perk.slotExpander.SlotExpander; import fr.antidino.smp.modules.perk.slotExpander.SlotManager; import fr.antidino.smp.modules.perk.codex.CodexListener; @@ -10,12 +11,8 @@ import fr.antidino.smp.modules.commands.PerksCommand; import fr.antidino.smp.core.database.DatabaseManager; import fr.antidino.smp.core.database.DatabaseProvider; -import fr.antidino.smp.modules.perk.SmithingRegisterAllItem; import fr.antidino.smp.modules.perk.config.ConfigPerkManager; -import fr.antidino.smp.modules.perk.AbstractPerk; import fr.antidino.smp.modules.perk.listeners.PerkEffectListener; -import fr.antidino.smp.modules.perk.PerkManager; -import fr.antidino.smp.modules.perk.PerkTasks; import fr.antidino.smp.modules.perk.rarity.RarityManager; import fr.antidino.smp.core.Utils.PDCBuilder; import org.bukkit.Bukkit; @@ -36,20 +33,26 @@ public final class CorePlugin extends JavaPlugin { public PerkTasks perkTasks; public SlotExpander slotExpander; public RarityManager rarityManager; + public PlayerPerkCache playerPerkCache; private DatabaseManager databaseManager; public DatabaseProvider databaseProvider; @Override public void onEnable() { + + + configPerkManager = new ConfigPerkManager(this); initConfig(); messageManager = new MessageManager(this); + playerPerkCache = new PlayerPerkCache(this); getServer().getPluginManager().registerEvents(new CraftingListener(this), this); getServer().getPluginManager().registerEvents(new SmithingListener(this), this); getServer().getPluginManager().registerEvents(new ItemBreakListener(this), this); - getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerJoinQuitEvent(this), this); getServer().getPluginManager().registerEvents(new SavingPerkListeners(this), this); + getServer().getPluginManager().registerEvents(new PlayerChangeArmorListener(this), this); rarityManager = new RarityManager(this); pdcBuilder = new PDCBuilder(this); diff --git a/src/main/java/fr/antidino/smp/modules/commands/AdminCommands.java b/src/main/java/fr/antidino/smp/modules/commands/AdminCommands.java index cdec232..45d1d78 100644 --- a/src/main/java/fr/antidino/smp/modules/commands/AdminCommands.java +++ b/src/main/java/fr/antidino/smp/modules/commands/AdminCommands.java @@ -51,6 +51,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command send(sender, messageManager.get("config_reloaded",vars)); } case "give" -> give(sender, args); + default -> send(sender, messageManager.get("admin_command_usage")); } return true; diff --git a/src/main/java/fr/antidino/smp/modules/listeners/PlayerChangeArmorListener.java b/src/main/java/fr/antidino/smp/modules/listeners/PlayerChangeArmorListener.java new file mode 100644 index 0000000..26c4d30 --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/listeners/PlayerChangeArmorListener.java @@ -0,0 +1,26 @@ +package fr.antidino.smp.modules.listeners; + +import com.destroystokyo.paper.event.player.PlayerArmorChangeEvent; +import fr.antidino.smp.CorePlugin; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.inventory.EquipmentSlot; + +public class PlayerChangeArmorListener implements Listener { + private final CorePlugin plugin; + + public PlayerChangeArmorListener(CorePlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerArmorChange(PlayerArmorChangeEvent event) { + Player player = event.getPlayer(); + EquipmentSlot slot = event.getSlot(); + plugin.playerPerkCache.updateSlot(player, slot); + Bukkit.getLogger().info(player.getName() + " has changed armor!"); + } +} diff --git a/src/main/java/fr/antidino/smp/modules/listeners/PlayerJoinListener.java b/src/main/java/fr/antidino/smp/modules/listeners/PlayerJoinQuitEvent.java similarity index 73% rename from src/main/java/fr/antidino/smp/modules/listeners/PlayerJoinListener.java rename to src/main/java/fr/antidino/smp/modules/listeners/PlayerJoinQuitEvent.java index ea845fd..322b666 100644 --- a/src/main/java/fr/antidino/smp/modules/listeners/PlayerJoinListener.java +++ b/src/main/java/fr/antidino/smp/modules/listeners/PlayerJoinQuitEvent.java @@ -7,15 +7,18 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + import java.util.List; -public class PlayerJoinListener implements Listener { +public class PlayerJoinQuitEvent implements Listener { CorePlugin plugin; - public PlayerJoinListener(CorePlugin plugin) { + public PlayerJoinQuitEvent(CorePlugin plugin) { this.plugin = plugin; } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { + List keys = plugin.perkManager.getPerks().stream() .map(AbstractPerk::getKey) .toList(); @@ -23,9 +26,15 @@ public void onPlayerJoin(PlayerJoinEvent event) { event.getPlayer().discoverRecipes(keys); }, 20L); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + if ( !plugin.databaseProvider.hasEntry(event.getPlayer().getUniqueId().toString())){ plugin.databaseProvider.insertPlayerSQL(event.getPlayer()); } + plugin.playerPerkCache.update(event.getPlayer()); }); } + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + plugin.playerPerkCache.cleanup(event.getPlayer()); + } } diff --git a/src/main/java/fr/antidino/smp/modules/perk/PerkTasks.java b/src/main/java/fr/antidino/smp/modules/perk/PerkTasks.java index d4b3daf..2e3db8b 100644 --- a/src/main/java/fr/antidino/smp/modules/perk/PerkTasks.java +++ b/src/main/java/fr/antidino/smp/modules/perk/PerkTasks.java @@ -10,16 +10,13 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Item; import org.bukkit.entity.Player; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; /** * Tâche répétée (toutes les 4 ticks) pour les perks "actifs en continu" portés sur l'armure : @@ -57,13 +54,41 @@ public boolean isManaged(UUID id) { private void run() { tick++; - boolean refreshNightVision = tick % 10 == 0; + boolean refreshNightVision = (tick % 10 == 0); + for (Player player : Bukkit.getOnlinePlayers()) { if (player.isDead()) continue; - Map levels = plugin.perkManager.getArmorPerkLevels(player); - nightVision(player, levels.getOrDefault(PerkTypeId.NIGHT_VISION, 0), refreshNightVision); - magnet(player, levels.getOrDefault(PerkTypeId.MAGNET, 0)); - doubleJump(player, levels.getOrDefault(PerkTypeId.DOUBLE_JUMP, 0)); + + var cacheWrapper = plugin.playerPerkCache.getPerksCache(player.getUniqueId()); + if (cacheWrapper == null) continue; + + Map> perkcache = cacheWrapper.get(); + if (perkcache == null || perkcache.isEmpty()) continue; + + + boolean hasNightVision = false; + boolean hasMagnet = false; + boolean hasDoubleJump = false; + + + for (List entries : perkcache.values()) { + if (entries == null) continue; + + if (!hasNightVision && entries.contains(PerkTypeId.NIGHT_VISION)) { + hasNightVision = true; + } + if (!hasMagnet && entries.contains(PerkTypeId.MAGNET)) { + hasMagnet = true; + } + if (!hasDoubleJump && entries.contains(PerkTypeId.DOUBLE_JUMP)) { + hasDoubleJump = true; + } + } + + + nightVision(player, hasNightVision ? 1 : 0, refreshNightVision); + magnet(player, hasMagnet ? 3 : 0); + doubleJump(player, hasDoubleJump ? 1 : 0); } } @@ -72,7 +97,6 @@ private void run() { private void nightVision(Player player, int level, boolean refresh) { UUID id = player.getUniqueId(); if (level > 0) { - // 25s de durée, renouvelée toutes les 2s : jamais de clignotement if (refresh || !nightVision.contains(id)) { player.addPotionEffect(new PotionEffect(PotionEffectType.NIGHT_VISION, 500, 0, true, false, false)); nightVision.add(id); @@ -95,12 +119,12 @@ private void magnet(Player player, int level) { for (Entity entity : player.getNearbyEntities(radius, radius, radius)) { if (!(entity instanceof Item item)) continue; if (item.getPickupDelay() > 0) continue; - // ne ramène pas les items que le joueur vient de jeter lui-même + UUID thrower = item.getThrower(); if (thrower != null && thrower.equals(player.getUniqueId())) continue; Vector direction = target.clone().subtract(item.getLocation().toVector()); - if (direction.lengthSquared() < 0.64) continue; // le ramassage vanilla prend le relais + if (direction.lengthSquared() < 0.64) continue; item.setVelocity(direction.normalize().multiply(speed)); } } diff --git a/src/main/java/fr/antidino/smp/modules/perk/PerksCache.java b/src/main/java/fr/antidino/smp/modules/perk/PerksCache.java new file mode 100644 index 0000000..381b689 --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/perk/PerksCache.java @@ -0,0 +1,23 @@ +package fr.antidino.smp.modules.perk; + + +import org.bukkit.inventory.EquipmentSlot; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PerksCache { + private final Map> perks = new HashMap<>(); + + public Map> get() { + return perks; + } + + + public void set(EquipmentSlot slot, List perks_list) { + perks.put(slot, perks_list); + } + + + +} diff --git a/src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java b/src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java new file mode 100644 index 0000000..34d2eea --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java @@ -0,0 +1,83 @@ +package fr.antidino.smp.modules.perk; + + +import fr.antidino.smp.CorePlugin; +import fr.antidino.smp.core.Utils.PDCBuilder; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import java.util.*; + +public class PlayerPerkCache { + private static final Logger log = LoggerFactory.getLogger(PlayerPerkCache.class); + private final CorePlugin plugin; + + private final Map cache = new HashMap<>(); + + public PlayerPerkCache(CorePlugin plugin) { + this.plugin = plugin; + } + /** Clean Cache Perk **/ + public void cleanup(Player player) { + cache.remove(player.getUniqueId()); + } + /** Clean Cache Perk **/ + public void cleanup(UUID uuid) { + cache.remove(uuid); + } + public Map getCache() { + return cache; + } + public PerksCache getPerksCache(UUID uuid) { + return cache.get(uuid); + } + + public void update(Player player) { + PerksCache perksCache = cache.get(player.getUniqueId()); + if (perksCache == null) { + perksCache = new PerksCache(); + } + for (EquipmentSlot slot : EquipmentSlot.values()) { + ItemStack item = getItemInSlot(player, slot); + List perks = readEntries(item); + perksCache.set(slot, perks); + } + cache.put(player.getUniqueId(), perksCache); + log.info("PlayerPerkCache updated"); + + } + public void updateSlot(Player player, EquipmentSlot slot) { + PerksCache perksCache = cache.get(player.getUniqueId()); + if (perksCache == null) { + perksCache = new PerksCache(); + } + ItemStack item = getItemInSlot(player, slot); + List perks = readEntries(item); + perksCache.set(slot, perks); + cache.put(player.getUniqueId(), perksCache); + } + + private ItemStack getItemInSlot(Player player, EquipmentSlot slot) { + return switch (slot) { + case LEGS -> player.getInventory().getLeggings(); + case FEET -> player.getInventory().getBoots(); + case HEAD -> player.getInventory().getHelmet(); + case CHEST -> player.getInventory().getChestplate(); + case OFF_HAND -> player.getInventory().getItemInOffHand(); + case HAND -> player.getInventory().getItemInMainHand(); + default -> null; + }; + } + + private List readEntries(ItemStack item) { + if (item == null || !item.hasItemMeta()) return null; + return item.getItemMeta().getPersistentDataContainer().get(PDCBuilder.SLOTS, PersistentDataType.LIST.strings()); + } + +} \ No newline at end of file diff --git a/src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java b/src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java deleted file mode 100644 index 27f3290..0000000 --- a/src/main/java/fr/antidino/smp/modules/perk/modal/ArmorPerk.java +++ /dev/null @@ -1,4 +0,0 @@ -package fr.antidino.smp.modules.perk.modal; - -public class ArmorPerk { -} diff --git a/src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java b/src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java deleted file mode 100644 index a2a462e..0000000 --- a/src/main/java/fr/antidino/smp/modules/perk/modal/Perk.java +++ /dev/null @@ -1,4 +0,0 @@ -package fr.antidino.smp.modules.perk.modal; - -public class Perk { -} From 815d8bb19e316715cb72b296337a4b850f876989 Mon Sep 17 00:00:00 2001 From: Antidino35 Date: Tue, 27 Oct 2026 13:04:52 +0100 Subject: [PATCH 5/6] feat(papi) : implement datacache and register placeholderapi exansion - Add PlaceholderAPI dependency and set as softdepend in plugin.yml - Implement DataCache to store perk craft counts in memory and eliminate database queries on craft - Register PapiExpansion with identifier 'smp' to display perk craft counts (%smp_perk_number_%) - Asynchronously load and unload craft statistics on plugin start and reload --- pom.xml | 10 ++++ src/main/java/fr/antidino/smp/CorePlugin.java | 18 +++++++ .../smp/core/database/DatabaseProvider.java | 2 +- .../modules/listeners/CraftingListener.java | 5 +- .../smp/modules/perk/PerkManager.java | 3 ++ .../smp/modules/perk/cache/DataCache.java | 47 ++++++++++++++++++ .../modules/perk/{ => cache}/PerksCache.java | 2 +- .../perk/{ => cache}/PlayerPerkCache.java | 3 +- .../smp/modules/perk/papi/PapiExpansion.java | 49 +++++++++++++++++++ src/main/resources/plugin.yml | 5 +- 10 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/main/java/fr/antidino/smp/modules/perk/cache/DataCache.java rename src/main/java/fr/antidino/smp/modules/perk/{ => cache}/PerksCache.java (90%) rename src/main/java/fr/antidino/smp/modules/perk/{ => cache}/PlayerPerkCache.java (97%) create mode 100644 src/main/java/fr/antidino/smp/modules/perk/papi/PapiExpansion.java diff --git a/pom.xml b/pom.xml index 9aa6151..48e097a 100644 --- a/pom.xml +++ b/pom.xml @@ -55,9 +55,19 @@ papermc-repo https://repo.papermc.io/repository/maven-public/ + + placeholderapi + https://repo.helpch.at/releases/ + + + me.clip + placeholderapi + 2.12.3 + provided + io.papermc.paper paper-api diff --git a/src/main/java/fr/antidino/smp/CorePlugin.java b/src/main/java/fr/antidino/smp/CorePlugin.java index c7121a7..7193f0b 100644 --- a/src/main/java/fr/antidino/smp/CorePlugin.java +++ b/src/main/java/fr/antidino/smp/CorePlugin.java @@ -3,6 +3,9 @@ import fr.antidino.smp.modules.listeners.*; import fr.antidino.smp.core.MessageManager; import fr.antidino.smp.modules.perk.*; +import fr.antidino.smp.modules.perk.cache.DataCache; +import fr.antidino.smp.modules.perk.cache.PlayerPerkCache; +import fr.antidino.smp.modules.perk.papi.PapiExpansion; import fr.antidino.smp.modules.perk.slotExpander.SlotExpander; import fr.antidino.smp.modules.perk.slotExpander.SlotManager; import fr.antidino.smp.modules.perk.codex.CodexListener; @@ -19,11 +22,15 @@ import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.sql.Connection; import java.sql.SQLException; import java.util.List; public final class CorePlugin extends JavaPlugin { + private static final Logger log = LoggerFactory.getLogger(CorePlugin.class); public PDCBuilder pdcBuilder; public PerkManager perkManager; public SlotManager slotManager; @@ -37,11 +44,19 @@ public final class CorePlugin extends JavaPlugin { private DatabaseManager databaseManager; public DatabaseProvider databaseProvider; + public DataCache dataCache; @Override public void onEnable() { + try { + new PapiExpansion(this).register(); + }catch (Exception e){ + log.info("PAPI Expansion Error"); + } + dataCache = new DataCache(this); + dataCache.load(); configPerkManager = new ConfigPerkManager(this); initConfig(); messageManager = new MessageManager(this); @@ -105,7 +120,10 @@ public int fullReload() { for (Player player : Bukkit.getOnlinePlayers()) { player.discoverRecipes(keys); } + dataCache.unload(); + dataCache.load(); return perkManager.getPerks().size(); + } @Override diff --git a/src/main/java/fr/antidino/smp/core/database/DatabaseProvider.java b/src/main/java/fr/antidino/smp/core/database/DatabaseProvider.java index bf742e6..cc08f19 100644 --- a/src/main/java/fr/antidino/smp/core/database/DatabaseProvider.java +++ b/src/main/java/fr/antidino/smp/core/database/DatabaseProvider.java @@ -63,7 +63,7 @@ public void InsertPerkCraftSQL(String perk_id,String uuid){ } } - public int NumberOfPerkCrafted(String perk_id){ + public int getNumberOfCraft(String perk_id){ try (Connection connection = plugin.getConnexion()){ PreparedStatement ps = connection.prepareStatement(GET_PERK_CRAFT_SQL); ps.setString(1, perk_id); diff --git a/src/main/java/fr/antidino/smp/modules/listeners/CraftingListener.java b/src/main/java/fr/antidino/smp/modules/listeners/CraftingListener.java index ec81e34..a770ffe 100644 --- a/src/main/java/fr/antidino/smp/modules/listeners/CraftingListener.java +++ b/src/main/java/fr/antidino/smp/modules/listeners/CraftingListener.java @@ -44,7 +44,7 @@ public void onCraftingEvent(CraftItemEvent event) { String id = plugin.perkManager.getPerkId(resultType); UUID playerId = player.getUniqueId(); if (id == null) return; - int number_of_perk = plugin.databaseProvider.NumberOfPerkCrafted(id); + int number_of_perk = plugin.dataCache.getPerkCraftCount(id); AbstractPerk perk = plugin.perkManager.getPerk(id); if (perk.getMaxPossibleCraft() <= number_of_perk && perk.getMaxPossibleCraft() != -1) { event.setCancelled(true); @@ -59,8 +59,7 @@ public void onCraftingEvent(CraftItemEvent event) { } cooldownManager.setCooldown(playerId,0.3); Bukkit.getScheduler().runTaskAsynchronously(plugin,()->{ - plugin.rarityManager.onCraft(player, perk, plugin.pdcBuilder.getPerkLevel(resultType)); - plugin.databaseProvider.InsertPerkCraftSQL(id,playerId.toString()); + plugin.dataCache.addPerkCraftCount(id); }); } diff --git a/src/main/java/fr/antidino/smp/modules/perk/PerkManager.java b/src/main/java/fr/antidino/smp/modules/perk/PerkManager.java index 4b6c388..d9586d1 100644 --- a/src/main/java/fr/antidino/smp/modules/perk/PerkManager.java +++ b/src/main/java/fr/antidino/smp/modules/perk/PerkManager.java @@ -145,4 +145,7 @@ public Map getArmorPerkLevels(Player player) { } return levels; } + public List listPerks(){ + return new ArrayList<>(registeredPerks.keySet()); + } } diff --git a/src/main/java/fr/antidino/smp/modules/perk/cache/DataCache.java b/src/main/java/fr/antidino/smp/modules/perk/cache/DataCache.java new file mode 100644 index 0000000..28effb1 --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/perk/cache/DataCache.java @@ -0,0 +1,47 @@ +package fr.antidino.smp.modules.perk.cache; + +import fr.antidino.smp.CorePlugin; +import org.bukkit.Bukkit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class DataCache { + private static final Logger log = LoggerFactory.getLogger(DataCache.class); + private final HashMap perk_craft_count = new HashMap<>(); + private final CorePlugin plugin; + public DataCache(CorePlugin plugin) { + this.plugin = plugin; + } + public void load() { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + for (String id : plugin.perkManager.listPerks()){ + int numberOfCraft = plugin.databaseProvider.getNumberOfCraft(id); + perk_craft_count.put(id, numberOfCraft); + } + log.info("Loaded {} perks", perk_craft_count.size()); + }); + } + public void unload() { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + for (String id : plugin.perkManager.listPerks()){ + perk_craft_count.remove(id); + } + }); + } + public int getPerkCraftCount(String id) { + return perk_craft_count.getOrDefault(id,0); + } + public void addPerkCraftCount(String id) { + int numberOfCraft = getPerkCraftCount(id); + perk_craft_count.put(id,numberOfCraft+1); + } + + + /**Debug **/ + public Map getPerkCraftCount() { + return perk_craft_count; + } +} diff --git a/src/main/java/fr/antidino/smp/modules/perk/PerksCache.java b/src/main/java/fr/antidino/smp/modules/perk/cache/PerksCache.java similarity index 90% rename from src/main/java/fr/antidino/smp/modules/perk/PerksCache.java rename to src/main/java/fr/antidino/smp/modules/perk/cache/PerksCache.java index 381b689..27834fc 100644 --- a/src/main/java/fr/antidino/smp/modules/perk/PerksCache.java +++ b/src/main/java/fr/antidino/smp/modules/perk/cache/PerksCache.java @@ -1,4 +1,4 @@ -package fr.antidino.smp.modules.perk; +package fr.antidino.smp.modules.perk.cache; import org.bukkit.inventory.EquipmentSlot; diff --git a/src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java b/src/main/java/fr/antidino/smp/modules/perk/cache/PlayerPerkCache.java similarity index 97% rename from src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java rename to src/main/java/fr/antidino/smp/modules/perk/cache/PlayerPerkCache.java index 34d2eea..c211fea 100644 --- a/src/main/java/fr/antidino/smp/modules/perk/PlayerPerkCache.java +++ b/src/main/java/fr/antidino/smp/modules/perk/cache/PlayerPerkCache.java @@ -1,11 +1,10 @@ -package fr.antidino.smp.modules.perk; +package fr.antidino.smp.modules.perk.cache; import fr.antidino.smp.CorePlugin; import fr.antidino.smp.core.Utils.PDCBuilder; import org.bukkit.entity.Player; import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; import org.slf4j.Logger; diff --git a/src/main/java/fr/antidino/smp/modules/perk/papi/PapiExpansion.java b/src/main/java/fr/antidino/smp/modules/perk/papi/PapiExpansion.java new file mode 100644 index 0000000..b4544a3 --- /dev/null +++ b/src/main/java/fr/antidino/smp/modules/perk/papi/PapiExpansion.java @@ -0,0 +1,49 @@ +package fr.antidino.smp.modules.perk.papi; + +import fr.antidino.smp.CorePlugin; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PapiExpansion extends PlaceholderExpansion { + private static final Logger log = LoggerFactory.getLogger(PapiExpansion.class); + private final CorePlugin plugin; + public PapiExpansion(CorePlugin plugin) { + this.plugin = plugin; + } + + @Override + @NotNull + public String getAuthor() { + return "Antidino72"; // + } + + @Override + @NotNull + public String getIdentifier() { + return "smp"; // + } + + @Override + @NotNull + public String getVersion() { + return "1.0.0"; // + } + + + @Override + public String onPlaceholderRequest(Player player, @NotNull String params) { + if (player == null) { + return ""; + } + if (params.startsWith("perk_number")) { + String subParam = params.replace("perk_number_", ""); + return String.valueOf(plugin.dataCache.getPerkCraftCount(subParam)); + } + return null; + } + + +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f4d26b7..2588117 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,9 +1,12 @@ name: SMPplugin version: '${version}' - +author: "Antidino72" main: fr.antidino.smp.CorePlugin api-version: '26.3' load: STARTUP +softdepend: [ + "PlaceholderAPI", +] commands : perk: permission : smp.admin.perk From 7573765c2388e2c34033339faf0ddb761deabc62 Mon Sep 17 00:00:00 2001 From: Antidino35 Date: Fri, 30 Oct 2026 15:58:28 +0100 Subject: [PATCH 6/6] fix(attribute perk) : fix applying perk in addPerkToItem --- .../fr/antidino/smp/core/Utils/PDCBuilder.java | 15 ++++++++------- src/main/resources/perks/health.yml | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/fr/antidino/smp/core/Utils/PDCBuilder.java b/src/main/java/fr/antidino/smp/core/Utils/PDCBuilder.java index 3fbacb9..dda86e3 100644 --- a/src/main/java/fr/antidino/smp/core/Utils/PDCBuilder.java +++ b/src/main/java/fr/antidino/smp/core/Utils/PDCBuilder.java @@ -88,29 +88,30 @@ public ArrayList getAppliedPerks(ItemStack item) { } return new ArrayList<>(perksArray); + } + public ItemStack addPerkToItem(ItemStack item, String perkId) { if (item == null || !item.hasItemMeta()) return item; - ItemMeta meta = item.getItemMeta(); - PersistentDataContainer pdc = meta.getPersistentDataContainer(); + item = plugin.perkManager.getPerk(perkId).onApply(item); List perks = getAppliedPerks(item); + ItemMeta meta = item.getItemMeta(); + PersistentDataContainer pdc = meta.getPersistentDataContainer(); perks.add(perkId); - pdc.set(SLOTS, PersistentDataType.LIST.strings(), perks); pdc.set(USED_SLOTS, PersistentDataType.INTEGER, perks.size()); - item.setItemMeta(meta); - - - return updateItemLore(item); + item = updateItemLore(item); + return item; } public ItemStack updateItemLore(ItemStack item) { + log.info(item.toString()); ItemMeta meta = item.getItemMeta(); if (meta == null) { plugin.getLogger().warning("[SMPplugin] Item is null"); diff --git a/src/main/resources/perks/health.yml b/src/main/resources/perks/health.yml index 66fcbda..951eab9 100644 --- a/src/main/resources/perks/health.yml +++ b/src/main/resources/perks/health.yml @@ -1,7 +1,7 @@ id : "sprinter" item: - material : "SUGAR" - display_name : "Sprinter" + material : "REDSTONE" + display_name : "HEALTH BOOST" craft : number : -1 shape : @@ -18,12 +18,24 @@ level: modifiers: - health: + health-head: attribute: "MAX_HEALTH" amount: 2.0 amount_per_level: 1.0 operation: "ADD_NUMBER" slot_group: "HEAD" + health-body: + attribute: "MAX_HEALTH" + amount: 2.0 + amount_per_level: 1.0 + operation: "ADD_NUMBER" + slot_group: "CHEST" + health-leg: + attribute: "MAX_HEALTH" + amount: 2.0 + amount_per_level: 1.0 + operation: "ADD_NUMBER" + slot_group: "LEGS" description: - "Augmente votre vie maximale"