diff --git a/AGENTS.md b/AGENTS.md index 14ca093..7f0449e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,14 +5,16 @@ This file is the project's committed home for project-intrinsic agent knowledge: - Build with JDK 25 or newer - `maven-enforcer-plugin` (`requireJavaVersion [25,)`) fails `validate` on anything older. Emitted plugin bytecode is intentionally Java 8 (major version 52) via `8` in `pom.xml`; the target runtime is a Java 17+ Spigot server. Do not "modernize" the bytecode level. Build/verify: `JAVA_HOME= mvn -B clean verify`. - `de.tr7zw:functional-annotations` is pinned to `0.1-SNAPSHOT` because no released version has ever been published (CodeMC metadata lists only the SNAPSHOT). It is a real compile dependency (`de.tr7zw.annotations.FAUtil`, used by the `nbt` package), so it cannot be dropped. Revisit if upstream ever cuts a release. - Test harness: JUnit 5 + MockBukkit + Mockito + JaCoCo. `mvn verify` runs tests and produces `target/site/jacoco/` coverage report. JaCoCo excludes `**/nbt/**` (vendored) and `**/ArmorPlus.class` (bootstrap). -- Coverage gate (CORE set): JaCoCo `check-core` execution enforces 100% line AND branch on these pure-logic classes: `PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`, `ArmorHandItem`, `BasicDamageCalculator`, `model.id.TypedString`, `model.id.StringId`, `model.id.ItemHandle`, `model.id.TaskHandle`, `model.player.WornArmor`. Build fails if core coverage drops below 100%. Everything else remains report-only. Excluded from core: `LogUtil` (calls `ArmorPlus.get()` singleton), `nbt.NBTType` (vendored `**/nbt/**` exclusion). When modifying a core class, add tests in the same commit. +- Coverage gate (CORE set): JaCoCo `check-core` execution enforces 100% line AND branch on these pure-logic classes: `PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`, `ArmorHandItem`, `BasicDamageCalculator`, `model.id.TypedString`, `model.id.StringId`, `model.id.ItemHandle`, `model.id.TaskHandle`, `model.player.WornArmor`, `model.player.PlayerArmorWearerRegistry`, `model.set.ArmorSetRegistry`. Build fails if core coverage drops below 100%. Everything else remains report-only. Excluded from core: `LogUtil` (calls `ArmorPlus.get()` singleton), `nbt.NBTType` (vendored `**/nbt/**` exclusion). When modifying a core class, add tests in the same commit. - JDK/bytecode split: main code compiles to Java 8 (`maven.compiler.release=8`), test code compiles to Java 17 (`maven.compiler.testRelease=17`). The `testRelease` property is picked up automatically by maven-compiler-plugin's default-testCompile execution - no custom execution block needed. Both compile under the same JDK 25 build. MockBukkit + paper-api are Java 17 bytecode, so tests cannot target 8. - MockBukkit limitation: the plugin cannot be loaded via `MockBukkit.load(ArmorPlus.class)` because the vendored NBT-API performs reflection at enable time that MockBukkit does not model. Use pure-logic unit tests or Mockito for testing pieces that interact with Bukkit APIs. - Mockito on JDK 25: surefire must pass `-Dnet.bytebuddy.experimental=true` (configured in pom.xml ``). Without it, ByteBuddy fails to recognize the JDK 25 class file version. -- Characterization test safety net (Phase 4A): `src/test/java/gg/steve/mc/ap/{data,player,armor,message}` contains characterization tests pinning current behavior of damage calc (`HandSetData.calculateFinalDamage`, `BasicSetData.onHit/onDamage`), wearer tracking (`SetPlayerManager`), warp safety (`WarpUtil.isSafe`), XP multiplier (`ExperienceSetData.onTargetDeath`), potion checks (`SetStatusEffectsManager.potionCheck`), set detection (`Set.isWearingSet/verifyPiece`), and messaging (`MessageType/CommandDebug`). Rearch phases 4B+ MUST keep these tests green - they prove behavior is preserved during extraction. Do NOT "fix" surprising behavior these tests pin; document it and defer to a separate bug-fix PR. -- Integration-only (not netted): `Set.isWearingSet` full integration path (NBT reflection in real server), `SetPlayerManager.init()` (iterates Bukkit.getOnlinePlayers), GUI rendering, PAPI expansion, scheduler-dependent abilities (Fairy/ColorWay/Lightning/Engineer/Traveller tick logic). +- Characterization test safety net: `src/test/java/gg/steve/mc/ap/{data,player,armor,message}` contains characterization tests pinning current behavior of damage calc (`HandSetData.calculateFinalDamage`, `BasicSetData.onHit/onDamage`), wearer tracking (`PlayerArmorSetServiceTest`), warp safety (`WarpUtil.isSafe`), XP multiplier (`ExperienceSetData.onTargetDeath`), potion checks (`SetStatusEffectsManager.potionCheck`), set detection (`Set.isWearingSet/verifyPiece`), and messaging (`MessageType/CommandDebug`). Later refactors MUST keep these tests green - they prove behavior is preserved during extraction. Do NOT "fix" surprising behavior these tests pin; document it and defer to a separate bug-fix PR. +- Integration-only (not netted): `Set.isWearingSet` full integration path (NBT reflection in real server), `PlayerArmorSetService.init()` (iterates Bukkit.getOnlinePlayers), GUI rendering, PAPI expansion, scheduler-dependent abilities (Fairy/ColorWay/Lightning/Engineer/Traveller tick logic). - Domain model package: `gg.steve.mc.ap.model` with sub-packages `set`, `ability`, `combat`, `effect`, `notification`, `player`, `id`, `port`. Contains pure-Java value types with ZERO Bukkit/NMS/NBT imports. Uses Lombok (`@Value`, `@Builder`) for boilerplate elimination. The `lombok.config` at repo root sets `lombok.addLombokGeneratedAnnotation=true` so JaCoCo auto-excludes generated code. - Damage-calculation logic lives in `model.ability`: `BasicDamageCalculator` (attack damage multiplier, defense reduction + knockback) and `ArmorHandItem.calculateFinalDamage()` (hand-item combined multiplier). The old `BasicSetData` and `HandSetData` classes are now thin delegators - they translate Bukkit events into domain calls and apply results back. Do not add logic to those delegators; extend the domain classes instead. +- Wearer/set tracking state lives in pure registries: `model.player.PlayerArmorWearerRegistry` (`PlayerId` -> `ArmorSetWearer` map) and `model.set.ArmorSetRegistry` (`ArmorSetId` -> set, generic because no domain `ArmorSet` aggregate exists yet - config parsing owns it). The Bukkit-facing adapters over these registries are `armor.ArmorSetCatalog` (holds an `ArmorSetRegistry`; `loadSets`, `getSets`, `getSet`) and `player.PlayerArmorSetService` (holds a `PlayerArmorWearerRegistry` + the catalog; `addSetPlayer`/`removeSetPlayer`/`isWearingSet`/`getSetPlayer`/`getPiecesWearing`/`getSetFromHand`/`init`). Both are ordinary injected instances - NO static state, NO `get()` entry point (they replaced the former `SetManager`/`SetPlayerManager` singletons). `PlayerArmorSetService.getSetPlayer` reconstructs the Bukkit `SetPlayer` on demand from the wearer record via `catalog.getSet(...)`. `ArmorSetCatalog.getSet` guards null/empty names before `ArmorSetId.of` (which rejects empty). `getPiecesWearing`/`getSetFromHand` read live Bukkit inventory/NBT, not wearer-map state. Do not add tracking logic to the adapters; extend the registries instead. +- Dependency injection (Guice 5.1.0, JSR-330 `javax.inject`, chosen over Dagger to avoid a second annotation processor alongside Lombok and for lower Spigot-runtime ceremony): the composition root is `ArmorPlus.onEnable`, which builds one `Injector` from `ArmorPlusModule` and resolves the shared collaborators. `ArmorPlusModule` binds the two pure registries (no-arg construction, kept framework-annotation-free so the `model` layer imports zero DI types), `ArmorSetCatalog`, and `PlayerArmorSetService` as singletons; the catalog + service are constructor-injected (`@Inject com.google.inject.Inject`) and threaded to listeners/commands/expansion/GUI. Guice + guava + `javax.inject` + aopalliance are shaded and relocated under `gg.steve.mc.ap.lib.*` (mirrors the commons-lang3 relocation) to avoid classpath collisions on a shared server; annotation-only transitives (jsr305, checker-qual, error_prone_annotations, j2objc, listenablefuture) are excluded from the shaded jar. Remaining `ArmorPlus.get()` uses are deferred (scheduler/logger via `LogUtil`, ability tick tasks, `eco()`, `formatNumber()`, and the static `getApGui()` facade, which reads the injected catalog stored on the plugin). - Domain model conventions: No class may use the `Manager` suffix (use Registry, Service, Store, Factory, Source, Resolver, etc.). Use the typed ID wrappers in model.id (PlayerId, ArmorSetId, DamageCauseId, PotionEffectId, SoundId) as identity currency - never reference Bukkit Player/ItemStack/World in the model layer. ID wrappers extend the `TypedString`/`StringId` base hierarchy: `TypedString` (abstract) rejects null via commons-lang3 `Validate.notNull` in its constructor and implements `equals`/`hashCode` with `getClass()` checks (different subtypes wrapping the same string are never equal); `StringId` extends it adding `Validate.notEmpty`. Leaf classes are final with a private constructor and an `of()` factory; `PlayerId` extends `TypedString` directly, wrapping a `UUID`. Use `@Value` + `@Builder` for multi-field value types. Annotate `List`/`Set`/`Map` fields with `@Singular` so Lombok's builder produces genuinely unmodifiable defensive copies at `build()` time. - Lombok dependency: `org.projectlombok:lombok:1.18.38` (provided scope). Annotation processor configured in maven-compiler-plugin's ``. Works with JDK 25 and emits Java 8 bytecode. - No-Bukkit-in-model rule: verified by grep (no ArchUnit yet). The `model` package must have zero imports of `org.bukkit.*`, `net.minecraft.*`, `de.tr7zw.*`, or any plugin class outside `model`. diff --git a/README.md b/README.md index 195d8a9..c1624eb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For more information about the plugin, permissions, and commands please refer to ## Code Coverage JaCoCo runs on every CI build (`mvn verify`). -The build enforces 100% line and branch coverage on the pure-logic core classes (`PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`, `ArmorHandItem`, `BasicDamageCalculator`, `TypedString`, `StringId`, `ItemHandle`, `TaskHandle`, `WornArmor`); a coverage drop on any of these will fail the build. +The build enforces 100% line and branch coverage on the pure-logic core classes (`PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`, `ArmorHandItem`, `BasicDamageCalculator`, `TypedString`, `StringId`, `ItemHandle`, `TaskHandle`, `WornArmor`, `PlayerArmorWearerRegistry`, `ArmorSetRegistry`); a coverage drop on any of these will fail the build. The HTML coverage report is uploaded as the **jacoco-report** artifact on each workflow run - download it from the [Actions tab](https://github.com/nbdSteve/ArmorPlus/actions/workflows/ci.yml). ## Soft Dependencies diff --git a/pom.xml b/pom.xml index 37c799b..2c97e7d 100644 --- a/pom.xml +++ b/pom.xml @@ -121,6 +121,8 @@ gg.steve.mc.ap.model.id.ItemHandle gg.steve.mc.ap.model.id.TaskHandle gg.steve.mc.ap.model.player.WornArmor + gg.steve.mc.ap.model.player.PlayerArmorWearerRegistry + gg.steve.mc.ap.model.set.ArmorSetRegistry @@ -155,7 +157,40 @@ org.apache.commons.lang3 gg.steve.mc.ap.lib.commons.lang3 + + + com.google.inject + gg.steve.mc.ap.lib.inject + + + com.google.common + gg.steve.mc.ap.lib.guava + + + com.google.thirdparty + gg.steve.mc.ap.lib.guava.thirdparty + + + javax.inject + gg.steve.mc.ap.lib.javax.inject + + + org.aopalliance + gg.steve.mc.ap.lib.aopalliance + + + + + com.google.code.findbugs:jsr305 + org.checkerframework:checker-qual + com.google.errorprone:error_prone_annotations + com.google.j2objc:j2objc-annotations + com.google.guava:listenablefuture + + @@ -253,6 +288,15 @@ 3.18.0 compile + + + com.google.inject + guice + 5.1.0 + compile + com.mojang authlib diff --git a/src/main/java/gg/steve/mc/ap/ArmorPlus.java b/src/main/java/gg/steve/mc/ap/ArmorPlus.java index 7bbd2d5..91ea324 100644 --- a/src/main/java/gg/steve/mc/ap/ArmorPlus.java +++ b/src/main/java/gg/steve/mc/ap/ArmorPlus.java @@ -1,22 +1,26 @@ package gg.steve.mc.ap; -import gg.steve.mc.ap.armor.SetManager; +import com.google.inject.Guice; +import com.google.inject.Injector; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.gui.ApGui; import gg.steve.mc.ap.managers.ConfigManager; import gg.steve.mc.ap.managers.FileManager; import gg.steve.mc.ap.managers.SetupManager; import gg.steve.mc.ap.papi.ArmorPlusExpansion; -import gg.steve.mc.ap.player.SetPlayerManager; +import gg.steve.mc.ap.player.PlayerArmorSetService; import gg.steve.mc.ap.utils.LogUtil; import net.milkbowl.vault.economy.Economy; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; +import java.util.Objects; public final class ArmorPlus extends JavaPlugin { private static ArmorPlus instance; @@ -24,6 +28,9 @@ public final class ArmorPlus extends JavaPlugin { private static ApGui apGui; private static DecimalFormat numberFormat = new DecimalFormat("#,###.##"); + // kept on the plugin so the static getApGui() facade can reach it + private ArmorSetCatalog catalog; + @Override public void onEnable() { // Plugin startup logic @@ -31,10 +38,13 @@ public void onEnable() { // reset apgui to null for reloading apGui = null; SetupManager.setupFiles(new FileManager(instance)); - SetupManager.registerCommands(instance); - SetupManager.registerEvents(instance); - SetManager.loadSets(); - SetPlayerManager.init(); + Injector injector = Guice.createInjector(new ArmorPlusModule(this)); + this.catalog = injector.getInstance(ArmorSetCatalog.class); + PlayerArmorSetService playerArmorSetService = injector.getInstance(PlayerArmorSetService.class); + SetupManager.registerCommands(instance, catalog); + SetupManager.registerEvents(instance, catalog, playerArmorSetService); + catalog.loadSets(); + playerArmorSetService.init(); // verify that the server is running vault so that eco features can be used if (Bukkit.getPluginManager().getPlugin("Vault") != null) { LogUtil.info("Vault found, hooking into it now..."); @@ -50,7 +60,7 @@ public void onEnable() { // placeholder hook if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { LogUtil.info("PlaceholderAPI found, registering expansion with it now..."); - new ArmorPlusExpansion(instance).register(); + new ArmorPlusExpansion(instance, catalog, playerArmorSetService).register(); } // metrics, just doing it here for now Metrics metrics = new Metrics(this, 7719); @@ -80,7 +90,8 @@ public static Economy eco() { public static ApGui getApGui() { if (apGui == null) { - apGui = new ApGui(ConfigManager.CONFIG.get().getConfigurationSection("gui")); + YamlConfiguration config = Objects.requireNonNull(ConfigManager.CONFIG.get(), "armor+.yml config is not loaded"); + apGui = new ApGui(config.getConfigurationSection("gui"), instance.catalog); } else { apGui.refresh(); } diff --git a/src/main/java/gg/steve/mc/ap/ArmorPlusModule.java b/src/main/java/gg/steve/mc/ap/ArmorPlusModule.java new file mode 100644 index 0000000..b0966d2 --- /dev/null +++ b/src/main/java/gg/steve/mc/ap/ArmorPlusModule.java @@ -0,0 +1,28 @@ +package gg.steve.mc.ap; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import gg.steve.mc.ap.armor.ArmorSetCatalog; +import gg.steve.mc.ap.armor.Set; +import gg.steve.mc.ap.model.player.PlayerArmorWearerRegistry; +import gg.steve.mc.ap.model.set.ArmorSetRegistry; +import gg.steve.mc.ap.player.PlayerArmorSetService; + +public class ArmorPlusModule extends AbstractModule { + private final ArmorPlus plugin; + + public ArmorPlusModule(ArmorPlus plugin) { + this.plugin = plugin; + } + + @Override + protected void configure() { + bind(ArmorPlus.class).toInstance(plugin); + // Bind the pure model registries here rather than annotating them, so the domain layer stays DI-free. + bind(new TypeLiteral>() {}).in(Singleton.class); + bind(PlayerArmorWearerRegistry.class).in(Singleton.class); + bind(ArmorSetCatalog.class).in(Singleton.class); + bind(PlayerArmorSetService.class).in(Singleton.class); + } +} diff --git a/src/main/java/gg/steve/mc/ap/armor/ArmorSetCatalog.java b/src/main/java/gg/steve/mc/ap/armor/ArmorSetCatalog.java new file mode 100644 index 0000000..f88e008 --- /dev/null +++ b/src/main/java/gg/steve/mc/ap/armor/ArmorSetCatalog.java @@ -0,0 +1,46 @@ +package gg.steve.mc.ap.armor; + +import com.google.inject.Inject; +import gg.steve.mc.ap.ArmorPlus; +import gg.steve.mc.ap.managers.ConfigManager; +import gg.steve.mc.ap.model.id.ArmorSetId; +import gg.steve.mc.ap.model.set.ArmorSetRegistry; +import gg.steve.mc.ap.utils.YamlFileUtil; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.Map; +import java.util.Objects; + +public class ArmorSetCatalog { + private final ArmorSetRegistry registry; + private final ArmorPlus plugin; + + @Inject + public ArmorSetCatalog(ArmorSetRegistry registry, ArmorPlus plugin) { + this.registry = registry; + this.plugin = plugin; + } + + public ArmorSetRegistry getRegistry() { + return registry; + } + + public void loadSets() { + registry.clear(); + YamlConfiguration config = Objects.requireNonNull(ConfigManager.CONFIG.get(), "armor+.yml config is not loaded"); + for (String set : config.getStringList("loaded-sets")) { + YamlFileUtil fileUtil = new YamlFileUtil("sets" + File.separator + set + ".yml", plugin); + registry.register(ArmorSetId.of(set), new Set(set, fileUtil)); + } + } + + public Map getSets() { + return registry.getAll(); + } + + public Set getSet(String name) { + if (name == null || name.isEmpty()) return null; + return registry.get(ArmorSetId.of(name)).orElse(null); + } +} diff --git a/src/main/java/gg/steve/mc/ap/armor/SetManager.java b/src/main/java/gg/steve/mc/ap/armor/SetManager.java deleted file mode 100644 index a34e9a9..0000000 --- a/src/main/java/gg/steve/mc/ap/armor/SetManager.java +++ /dev/null @@ -1,32 +0,0 @@ -package gg.steve.mc.ap.armor; - -import gg.steve.mc.ap.ArmorPlus; -import gg.steve.mc.ap.managers.ConfigManager; -import gg.steve.mc.ap.utils.YamlFileUtil; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -public class SetManager { - private static Map setConfigs; - private static Map sets; - - public static void loadSets() { - setConfigs = new HashMap<>(); - sets = new HashMap<>(); - for (String set : ConfigManager.CONFIG.get().getStringList("loaded-sets")) { - YamlFileUtil fileUtil = new YamlFileUtil("sets" + File.separator + set + ".yml", ArmorPlus.get()); - setConfigs.put(set, fileUtil); - sets.put(set, new Set(set, fileUtil)); - } - } - - public static Map getSets() { - return sets; - } - - public static Set getSet(String name) { - return sets.get(name); - } -} diff --git a/src/main/java/gg/steve/mc/ap/cmd/ApCmd.java b/src/main/java/gg/steve/mc/ap/cmd/ApCmd.java index cf8ff0e..e0d07b7 100644 --- a/src/main/java/gg/steve/mc/ap/cmd/ApCmd.java +++ b/src/main/java/gg/steve/mc/ap/cmd/ApCmd.java @@ -1,5 +1,6 @@ package gg.steve.mc.ap.cmd; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.cmd.sub.GiveCmd; import gg.steve.mc.ap.cmd.sub.GuiCmd; import gg.steve.mc.ap.cmd.sub.HelpCmd; @@ -14,6 +15,11 @@ import java.util.List; public class ApCmd implements TabExecutor { + private final ArmorSetCatalog catalog; + + public ApCmd(ArmorSetCatalog catalog) { + this.catalog = catalog; + } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { @@ -39,7 +45,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St break; case "g": case "give": - GiveCmd.give(sender, args); + GiveCmd.give(sender, args, catalog); break; default: CommandDebug.INVALID_COMMAND.message(sender); diff --git a/src/main/java/gg/steve/mc/ap/cmd/sub/GiveCmd.java b/src/main/java/gg/steve/mc/ap/cmd/sub/GiveCmd.java index 20c0fac..cf90d22 100644 --- a/src/main/java/gg/steve/mc/ap/cmd/sub/GiveCmd.java +++ b/src/main/java/gg/steve/mc/ap/cmd/sub/GiveCmd.java @@ -1,8 +1,8 @@ package gg.steve.mc.ap.cmd.sub; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Piece; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import gg.steve.mc.ap.message.CommandDebug; import gg.steve.mc.ap.message.MessageType; import gg.steve.mc.ap.permission.PermissionNode; @@ -13,7 +13,7 @@ public class GiveCmd { - public static void give(CommandSender sender, String[] args) { + public static void give(CommandSender sender, String[] args, ArmorSetCatalog catalog) { // /ap g phantom nbdsteve helmet 1 if (args.length < 4) { CommandDebug.INVALID_NUMBER_OF_ARGUMENTS.message(sender); @@ -29,11 +29,11 @@ public static void give(CommandSender sender, String[] args) { return; } Set set; - if (SetManager.getSet(args[1]) == null) { + if (catalog.getSet(args[1]) == null) { CommandDebug.INVALID_SET.message(sender); return; } else { - set = SetManager.getSet(args[1]); + set = catalog.getSet(args[1]); } Piece piece = null; boolean all = false; diff --git a/src/main/java/gg/steve/mc/ap/gui/ApGui.java b/src/main/java/gg/steve/mc/ap/gui/ApGui.java index 5e20bf9..b250b58 100644 --- a/src/main/java/gg/steve/mc/ap/gui/ApGui.java +++ b/src/main/java/gg/steve/mc/ap/gui/ApGui.java @@ -1,20 +1,17 @@ package gg.steve.mc.ap.gui; -import gg.steve.mc.ap.armor.SetManager; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.utils.GuiItemUtil; import org.bukkit.configuration.ConfigurationSection; public class ApGui extends AbstractGui { private ConfigurationSection section; + private final ArmorSetCatalog catalog; - /** - * Constructor the create a new Gui - * - * @param section - */ - public ApGui(ConfigurationSection section) { + public ApGui(ConfigurationSection section, ArmorSetCatalog catalog) { super(section, section.getString("type"), section.getInt("size")); this.section = section; + this.catalog = catalog; refresh(); } @@ -33,7 +30,7 @@ public void refresh() { player.closeInventory(); break; default: - SetManager.getSet(section.getString(entry + ".action")).openGui(player); + catalog.getSet(section.getString(entry + ".action")).openGui(player); break; } }); diff --git a/src/main/java/gg/steve/mc/ap/listener/ArmorBuffListener.java b/src/main/java/gg/steve/mc/ap/listener/ArmorBuffListener.java index 9ed3b31..fcd15b1 100644 --- a/src/main/java/gg/steve/mc/ap/listener/ArmorBuffListener.java +++ b/src/main/java/gg/steve/mc/ap/listener/ArmorBuffListener.java @@ -1,10 +1,10 @@ package gg.steve.mc.ap.listener; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import gg.steve.mc.ap.nbt.NBTItem; +import gg.steve.mc.ap.player.PlayerArmorSetService; import gg.steve.mc.ap.player.SetPlayer; -import gg.steve.mc.ap.player.SetPlayerManager; import org.bukkit.Material; import org.bukkit.entity.Arrow; import org.bukkit.entity.Player; @@ -16,6 +16,13 @@ import org.bukkit.event.entity.FoodLevelChangeEvent; public class ArmorBuffListener implements Listener { + private final ArmorSetCatalog catalog; + private final PlayerArmorSetService playerArmorSetService; + + public ArmorBuffListener(ArmorSetCatalog catalog, PlayerArmorSetService playerArmorSetService) { + this.catalog = catalog; + this.playerArmorSetService = playerArmorSetService; + } @EventHandler public void onHit(EntityDamageByEntityEvent event) { @@ -31,16 +38,16 @@ public void onHit(EntityDamageByEntityEvent event) { if (damager == null) { damager = (Player) event.getDamager(); } - if (!SetPlayerManager.isWearingSet(damager)) { + if (!playerArmorSetService.isWearingSet(damager)) { if (damager.getItemInHand() == null || damager.getItemInHand().getType().equals(Material.AIR)) return; NBTItem nbtItem = new NBTItem(damager.getItemInHand()); if (nbtItem.getString("armor+.set").equalsIgnoreCase("")) return; - Set set = SetManager.getSet(nbtItem.getString("armor+.set")); + Set set = catalog.getSet(nbtItem.getString("armor+.set")); set.getHandData().hitWithoutSetBuff(event, damager); return; } - SetPlayer player = SetPlayerManager.getSetPlayer(damager); + SetPlayer player = playerArmorSetService.getSetPlayer(damager); player.getSet().onHit(event, damager); } @@ -48,8 +55,8 @@ public void onHit(EntityDamageByEntityEvent event) { public void onDamage(EntityDamageByEntityEvent event) { if (event.isCancelled()) return; if (!(event.getEntity() instanceof Player)) return; - if (!SetPlayerManager.isWearingSet((Player) event.getEntity())) return; - SetPlayer player = SetPlayerManager.getSetPlayer((Player) event.getEntity()); + if (!playerArmorSetService.isWearingSet((Player) event.getEntity())) return; + SetPlayer player = playerArmorSetService.getSetPlayer((Player) event.getEntity()); player.getSet().onDamage(event); } @@ -57,9 +64,9 @@ public void onDamage(EntityDamageByEntityEvent event) { public void fall(EntityDamageEvent event) { if (event.isCancelled()) return; if (!(event.getEntity() instanceof Player)) return; - if (!SetPlayerManager.isWearingSet((Player) event.getEntity())) return; + if (!playerArmorSetService.isWearingSet((Player) event.getEntity())) return; if (!event.getCause().equals(EntityDamageEvent.DamageCause.FALL)) return; - SetPlayer player = SetPlayerManager.getSetPlayer((Player) event.getEntity()); + SetPlayer player = playerArmorSetService.getSetPlayer((Player) event.getEntity()); player.getSet().onFall(event); } @@ -67,8 +74,8 @@ public void fall(EntityDamageEvent event) { public void hunger(FoodLevelChangeEvent event) { if (event.isCancelled()) return; if (!(event.getEntity() instanceof Player)) return; - if (!SetPlayerManager.isWearingSet((Player) event.getEntity())) return; - SetPlayer player = SetPlayerManager.getSetPlayer((Player) event.getEntity()); + if (!playerArmorSetService.isWearingSet((Player) event.getEntity())) return; + SetPlayer player = playerArmorSetService.getSetPlayer((Player) event.getEntity()); player.getSet().onHungerDeplete(event); } @@ -76,8 +83,8 @@ public void hunger(FoodLevelChangeEvent event) { public void death(EntityDeathEvent event) { if (event.getEntity().getKiller() == null) return; Player killer = event.getEntity().getKiller(); - if (!SetPlayerManager.isWearingSet(killer)) return; - SetPlayer player = SetPlayerManager.getSetPlayer(killer); + if (!playerArmorSetService.isWearingSet(killer)) return; + SetPlayer player = playerArmorSetService.getSetPlayer(killer); player.getSet().onTargetDeath(event, killer); } } diff --git a/src/main/java/gg/steve/mc/ap/listener/PlayerCommandListener.java b/src/main/java/gg/steve/mc/ap/listener/PlayerCommandListener.java index 9f42a01..d16816b 100644 --- a/src/main/java/gg/steve/mc/ap/listener/PlayerCommandListener.java +++ b/src/main/java/gg/steve/mc/ap/listener/PlayerCommandListener.java @@ -1,10 +1,11 @@ package gg.steve.mc.ap.listener; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Piece; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import gg.steve.mc.ap.message.CommandDebug; import gg.steve.mc.ap.message.MessageType; +import gg.steve.mc.ap.model.id.ArmorSetId; import gg.steve.mc.ap.permission.PermissionNode; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -14,19 +15,24 @@ import org.bukkit.inventory.ItemStack; public class PlayerCommandListener implements Listener { + private final ArmorSetCatalog catalog; + + public PlayerCommandListener(ArmorSetCatalog catalog) { + this.catalog = catalog; + } @EventHandler public void onCmd(PlayerCommandPreprocessEvent event) { String[] args = event.getMessage().split(" "); String setName = null; - for (String key : SetManager.getSets().keySet()) { + for (ArmorSetId key : catalog.getSets().keySet()) { if (args[0].equalsIgnoreCase("/" + key)) { - setName = key; + setName = key.toString(); event.setCancelled(true); } } if (setName == null) return; - Set set = SetManager.getSet(setName); + Set set = catalog.getSet(setName); if (args.length == 1) { if (!PermissionNode.GUI.hasPermission(event.getPlayer())) { CommandDebug.INSUFFICIENT_PERMISSION.message(event.getPlayer(), PermissionNode.GUI.get()); diff --git a/src/main/java/gg/steve/mc/ap/listener/PlayerEquipListener.java b/src/main/java/gg/steve/mc/ap/listener/PlayerEquipListener.java index 679299b..8449d80 100644 --- a/src/main/java/gg/steve/mc/ap/listener/PlayerEquipListener.java +++ b/src/main/java/gg/steve/mc/ap/listener/PlayerEquipListener.java @@ -1,12 +1,12 @@ package gg.steve.mc.ap.listener; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Piece; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import gg.steve.mc.ap.armorequipevent.ArmorEquipEvent; import gg.steve.mc.ap.managers.ConfigManager; import gg.steve.mc.ap.nbt.NBTItem; -import gg.steve.mc.ap.player.SetPlayerManager; +import gg.steve.mc.ap.player.PlayerArmorSetService; import gg.steve.mc.ap.utils.CommandUtil; import org.bukkit.Material; import org.bukkit.event.EventHandler; @@ -15,6 +15,13 @@ import org.bukkit.inventory.ItemStack; public class PlayerEquipListener implements Listener { + private final ArmorSetCatalog catalog; + private final PlayerArmorSetService playerArmorSetService; + + public PlayerEquipListener(ArmorSetCatalog catalog, PlayerArmorSetService playerArmorSetService) { + this.catalog = catalog; + this.playerArmorSetService = playerArmorSetService; + } @EventHandler public void equip(ArmorEquipEvent event) { @@ -43,7 +50,7 @@ public void equip(ArmorEquipEvent event) { } } String setName = nbtItem.getString("armor+.set"); - Set set = SetManager.getSet(setName); + Set set = catalog.getSet(setName); Piece piece = null; for (Piece pieces : set.getSetPieces().keySet()) { if (event.getNewArmorPiece().getType().equals(set.getPiece(pieces).getType())) piece = pieces; @@ -52,16 +59,16 @@ public void equip(ArmorEquipEvent event) { CommandUtil.execute(set.getConfig().getStringList("set-pieces." + piece.name().toLowerCase() + ".commands.apply"), event.getPlayer()); } if (set.isWearingSet(event.getPlayer(), event.getType(), event.getNewArmorPiece())) { - SetPlayerManager.addSetPlayer(event.getPlayer(), setName); + playerArmorSetService.addSetPlayer(event.getPlayer(), setName); set.apply(event.getPlayer()); } } @EventHandler public void join(PlayerJoinEvent event) { - for (Set set : SetManager.getSets().values()) { + for (Set set : catalog.getSets().values()) { if (!set.isWearingSet(event.getPlayer(), null, null)) continue; - SetPlayerManager.addSetPlayer(event.getPlayer(), set.getName()); + playerArmorSetService.addSetPlayer(event.getPlayer(), set.getName()); set.apply(event.getPlayer()); return; } diff --git a/src/main/java/gg/steve/mc/ap/listener/PlayerUnequipListener.java b/src/main/java/gg/steve/mc/ap/listener/PlayerUnequipListener.java index b8eb134..f1e757c 100644 --- a/src/main/java/gg/steve/mc/ap/listener/PlayerUnequipListener.java +++ b/src/main/java/gg/steve/mc/ap/listener/PlayerUnequipListener.java @@ -1,12 +1,12 @@ package gg.steve.mc.ap.listener; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Piece; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import gg.steve.mc.ap.armorequipevent.ArmorEquipEvent; import gg.steve.mc.ap.managers.ConfigManager; import gg.steve.mc.ap.nbt.NBTItem; -import gg.steve.mc.ap.player.SetPlayerManager; +import gg.steve.mc.ap.player.PlayerArmorSetService; import gg.steve.mc.ap.utils.CommandUtil; import org.bukkit.Material; import org.bukkit.event.EventHandler; @@ -14,6 +14,13 @@ import org.bukkit.event.player.PlayerQuitEvent; public class PlayerUnequipListener implements Listener { + private final ArmorSetCatalog catalog; + private final PlayerArmorSetService playerArmorSetService; + + public PlayerUnequipListener(ArmorSetCatalog catalog, PlayerArmorSetService playerArmorSetService) { + this.catalog = catalog; + this.playerArmorSetService = playerArmorSetService; + } @EventHandler public void unEquip(ArmorEquipEvent event) { @@ -26,7 +33,7 @@ public void unEquip(ArmorEquipEvent event) { event.setCancelled(true); } String name = nbtItem.getString("armor+.set"); - Set set = SetManager.getSet(name); + Set set = catalog.getSet(name); Piece piece = null; for (Piece pieces : set.getSetPieces().keySet()) { if (event.getOldArmorPiece().getType().equals(set.getPiece(pieces).getType())) piece = pieces; @@ -35,16 +42,16 @@ public void unEquip(ArmorEquipEvent event) { CommandUtil.execute(set.getConfig().getStringList("set-pieces." + piece.name().toLowerCase() + ".commands.remove"), event.getPlayer()); } if (set.isWearingSet(event.getPlayer(), event.getType(), event.getOldArmorPiece())) { - SetPlayerManager.removeSetPlayer(event.getPlayer()); + playerArmorSetService.removeSetPlayer(event.getPlayer()); set.remove(event.getPlayer()); } } @EventHandler public void quit(PlayerQuitEvent event) { - for (Set set : SetManager.getSets().values()) { + for (Set set : catalog.getSets().values()) { if (!set.isWearingSet(event.getPlayer(), null, null)) continue; - SetPlayerManager.removeSetPlayer(event.getPlayer()); + playerArmorSetService.removeSetPlayer(event.getPlayer()); set.remove(event.getPlayer()); return; } diff --git a/src/main/java/gg/steve/mc/ap/managers/SetupManager.java b/src/main/java/gg/steve/mc/ap/managers/SetupManager.java index b771073..4e5961e 100644 --- a/src/main/java/gg/steve/mc/ap/managers/SetupManager.java +++ b/src/main/java/gg/steve/mc/ap/managers/SetupManager.java @@ -1,12 +1,14 @@ package gg.steve.mc.ap.managers; import gg.steve.mc.ap.ArmorPlus; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armorequipevent.ArmorListener; import gg.steve.mc.ap.cmd.ApCmd; import gg.steve.mc.ap.data.utils.EngineerAttackUtil; import gg.steve.mc.ap.data.utils.TravellerAttackUtil; import gg.steve.mc.ap.gui.GuiClickListener; import gg.steve.mc.ap.listener.*; +import gg.steve.mc.ap.player.PlayerArmorSetService; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; @@ -31,23 +33,18 @@ public static void setupFiles(FileManager fileManager) { } } - public static void registerCommands(ArmorPlus instance) { - instance.getCommand("ap").setExecutor(new ApCmd()); + public static void registerCommands(ArmorPlus instance, ArmorSetCatalog catalog) { + instance.getCommand("ap").setExecutor(new ApCmd(catalog)); } - /** - * Register all of the events for the plugin - * - * @param instance Plugin, the main plugin instance - */ - public static void registerEvents(Plugin instance) { + public static void registerEvents(Plugin instance, ArmorSetCatalog catalog, PlayerArmorSetService playerArmorSetService) { PluginManager pm = instance.getServer().getPluginManager(); pm.registerEvents(new ArmorListener(ConfigManager.BLOCKED.get().getStringList("blocked")), instance); // pm.registerEvents(new DispenserArmorListener(), instance); - pm.registerEvents(new PlayerEquipListener(), instance); - pm.registerEvents(new PlayerUnequipListener(), instance); - pm.registerEvents(new PlayerCommandListener(), instance); - pm.registerEvents(new ArmorBuffListener(), instance); + pm.registerEvents(new PlayerEquipListener(catalog, playerArmorSetService), instance); + pm.registerEvents(new PlayerUnequipListener(catalog, playerArmorSetService), instance); + pm.registerEvents(new PlayerCommandListener(catalog), instance); + pm.registerEvents(new ArmorBuffListener(catalog, playerArmorSetService), instance); pm.registerEvents(new GuiClickListener(), instance); pm.registerEvents(new ArmorSwitchListener(), instance); pm.registerEvents(new TravellerAttackUtil(), instance); diff --git a/src/main/java/gg/steve/mc/ap/model/player/PlayerArmorWearerRegistry.java b/src/main/java/gg/steve/mc/ap/model/player/PlayerArmorWearerRegistry.java new file mode 100644 index 0000000..139a237 --- /dev/null +++ b/src/main/java/gg/steve/mc/ap/model/player/PlayerArmorWearerRegistry.java @@ -0,0 +1,40 @@ +package gg.steve.mc.ap.model.player; + +import gg.steve.mc.ap.model.id.ArmorSetId; +import gg.steve.mc.ap.model.id.PlayerId; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public final class PlayerArmorWearerRegistry { + private final Map wearers = new HashMap<>(); + + public void add(ArmorSetWearer wearer) { + wearers.put(wearer.getPlayerId(), wearer); + } + + public void add(PlayerId playerId, ArmorSetId setId) { + add(new ArmorSetWearer(playerId, setId)); + } + + public void remove(PlayerId playerId) { + wearers.remove(playerId); + } + + public boolean isWearing(PlayerId playerId) { + return wearers.containsKey(playerId); + } + + public Optional get(PlayerId playerId) { + return Optional.ofNullable(wearers.get(playerId)); + } + + public int count() { + return wearers.size(); + } + + public void clear() { + wearers.clear(); + } +} diff --git a/src/main/java/gg/steve/mc/ap/model/set/ArmorSetRegistry.java b/src/main/java/gg/steve/mc/ap/model/set/ArmorSetRegistry.java new file mode 100644 index 0000000..148031d --- /dev/null +++ b/src/main/java/gg/steve/mc/ap/model/set/ArmorSetRegistry.java @@ -0,0 +1,33 @@ +package gg.steve.mc.ap.model.set; + +import gg.steve.mc.ap.model.id.ArmorSetId; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +// Generic over the set type: the pure domain has no ArmorSet aggregate yet, so the platform layer supplies its own. +public final class ArmorSetRegistry { + private final Map sets = new LinkedHashMap<>(); + + public void register(ArmorSetId id, T set) { + sets.put(id, set); + } + + public Optional get(ArmorSetId id) { + return Optional.ofNullable(sets.get(id)); + } + + public Map getAll() { + return Collections.unmodifiableMap(sets); + } + + public int count() { + return sets.size(); + } + + public void clear() { + sets.clear(); + } +} diff --git a/src/main/java/gg/steve/mc/ap/papi/ArmorPlusExpansion.java b/src/main/java/gg/steve/mc/ap/papi/ArmorPlusExpansion.java index bc1a4f1..c2ea10f 100644 --- a/src/main/java/gg/steve/mc/ap/papi/ArmorPlusExpansion.java +++ b/src/main/java/gg/steve/mc/ap/papi/ArmorPlusExpansion.java @@ -1,10 +1,10 @@ package gg.steve.mc.ap.papi; import gg.steve.mc.ap.ArmorPlus; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import gg.steve.mc.ap.managers.ConfigManager; -import gg.steve.mc.ap.player.SetPlayerManager; +import gg.steve.mc.ap.player.PlayerArmorSetService; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.ChatColor; import org.bukkit.entity.Player; @@ -13,9 +13,13 @@ public class ArmorPlusExpansion extends PlaceholderExpansion { private ArmorPlus instance; + private final ArmorSetCatalog catalog; + private final PlayerArmorSetService playerArmorSetService; - public ArmorPlusExpansion(ArmorPlus instance) { + public ArmorPlusExpansion(ArmorPlus instance, ArmorSetCatalog catalog, PlayerArmorSetService playerArmorSetService) { this.instance = instance; + this.catalog = catalog; + this.playerArmorSetService = playerArmorSetService; } @Override @@ -48,25 +52,25 @@ public String onPlaceholderRequest(Player player, String identifier) { if (player == null) return ""; String fallback = ConfigManager.CONFIG.get().getString("fallback-placeholder"); if (identifier.equalsIgnoreCase("current_name")) { - if (SetPlayerManager.isWearingSet(player)) { - return SetPlayerManager.getSetPlayer(player).getSet().getName(); + if (playerArmorSetService.isWearingSet(player)) { + return playerArmorSetService.getSetPlayer(player).getSet().getName(); } else { return fallback; } } if (identifier.contains("current_increase")) { double increase = 0; - if (SetPlayerManager.getSetFromHand(player) != null) { - Set set = SetPlayerManager.getSetFromHand(player); + if (playerArmorSetService.getSetFromHand(player) != null) { + Set set = playerArmorSetService.getSetFromHand(player); if (set.getHandData() != null) { if (identifier.endsWith("raw")) { - if (set.getHandData().requiresFullSet() && SetPlayerManager.isWearingSet(player) && SetPlayerManager.getSetPlayer(player).getSet().equals(set)) { + if (set.getHandData().requiresFullSet() && playerArmorSetService.isWearingSet(player) && playerArmorSetService.getSetPlayer(player).getSet().equals(set)) { increase += set.getHandData().getIncrease() - 1; } else if (!set.getHandData().requiresFullSet()) { increase += set.getHandData().getIncrease() - 1; } } else if (identifier.endsWith("percentage")) { - if (set.getHandData().requiresFullSet() && SetPlayerManager.isWearingSet(player) && SetPlayerManager.getSetPlayer(player).getSet().equals(set)) { + if (set.getHandData().requiresFullSet() && playerArmorSetService.isWearingSet(player) && playerArmorSetService.getSetPlayer(player).getSet().equals(set)) { increase += (set.getHandData().getIncrease() - 1) * 100; } else if (!set.getHandData().requiresFullSet()) { increase += (set.getHandData().getIncrease() - 1) * 100; @@ -74,8 +78,8 @@ public String onPlaceholderRequest(Player player, String identifier) { } } } - if (SetPlayerManager.isWearingSet(player)) { - Set set = SetPlayerManager.getSetPlayer(player).getSet(); + if (playerArmorSetService.isWearingSet(player)) { + Set set = playerArmorSetService.getSetPlayer(player).getSet(); if (set.getBasicData().getIncrease() == -1 && increase == 0) { return "Default"; } @@ -85,7 +89,7 @@ public String onPlaceholderRequest(Player player, String identifier) { if (set.getBasicData().getIncrease() != -1) increase += (set.getBasicData().getIncrease() - 1) * 100; } } - if (!SetPlayerManager.isWearingSet(player) && increase == 0) { + if (!playerArmorSetService.isWearingSet(player) && increase == 0) { return fallback; } if (identifier.endsWith("raw")) { @@ -97,46 +101,46 @@ public String onPlaceholderRequest(Player player, String identifier) { } } if (identifier.contains("current_reduction")) { - if (SetPlayerManager.isWearingSet(player)) { - if (SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getReduction() == -1) { + if (playerArmorSetService.isWearingSet(player)) { + if (playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getReduction() == -1) { return "Default"; } if (identifier.endsWith("raw")) { - return String.valueOf(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getReduction()); + return String.valueOf(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getReduction()); } else if (identifier.endsWith("formatted")) { - return ArmorPlus.formatNumber(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getReduction()); + return ArmorPlus.formatNumber(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getReduction()); } else if (identifier.endsWith("percentage")) { - return ArmorPlus.formatNumber(Math.abs((SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getReduction() - 1) * 100)) + "%"; + return ArmorPlus.formatNumber(Math.abs((playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getReduction() - 1) * 100)) + "%"; } } else { return fallback; } } if (identifier.contains("current_kb")) { - if (SetPlayerManager.isWearingSet(player)) { - if (SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getKb() == -1) { + if (playerArmorSetService.isWearingSet(player)) { + if (playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getKb() == -1) { return "Default"; } if (identifier.endsWith("raw")) { - return String.valueOf(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getKb()); + return String.valueOf(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getKb()); } else if (identifier.endsWith("formatted")) { - return ArmorPlus.formatNumber(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getKb()); + return ArmorPlus.formatNumber(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getKb()); } } else { return fallback; } } if (identifier.contains("current_health")) { - if (SetPlayerManager.isWearingSet(player)) { - if (SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getHealth() == -1) { + if (playerArmorSetService.isWearingSet(player)) { + if (playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getHealth() == -1) { return "Default"; } if (identifier.endsWith("raw")) { - return String.valueOf(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getHealth()); + return String.valueOf(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getHealth()); } else if (identifier.endsWith("formatted")) { - return ArmorPlus.formatNumber(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getHealth()); + return ArmorPlus.formatNumber(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getHealth()); } else if (identifier.endsWith("hearts")) { - return ArmorPlus.formatNumber(SetPlayerManager.getSetPlayer(player).getSet().getBasicData().getHealth() / 2) + ChatColor.RED + "❤"; + return ArmorPlus.formatNumber(playerArmorSetService.getSetPlayer(player).getSet().getBasicData().getHealth() / 2) + ChatColor.RED + "❤"; } } else { return fallback; @@ -145,15 +149,15 @@ public String onPlaceholderRequest(Player player, String identifier) { if (identifier.endsWith("pieces_wearing")) { String[] parts = identifier.split("_"); Set set; - if ((set = SetManager.getSet(parts[0])) == null) { + if ((set = catalog.getSet(parts[0])) == null) { return "Invalid set"; } - return String.valueOf(SetPlayerManager.getPiecesWearing(set, player)); + return String.valueOf(playerArmorSetService.getPiecesWearing(set, player)); } if (identifier.endsWith("pieces_total")) { String[] parts = identifier.split("_"); Set set; - if ((set = SetManager.getSet(parts[0])) == null) { + if ((set = catalog.getSet(parts[0])) == null) { return "Invalid set"; } return String.valueOf(set.getSetPieces().size()); diff --git a/src/main/java/gg/steve/mc/ap/player/SetPlayerManager.java b/src/main/java/gg/steve/mc/ap/player/PlayerArmorSetService.java similarity index 51% rename from src/main/java/gg/steve/mc/ap/player/SetPlayerManager.java rename to src/main/java/gg/steve/mc/ap/player/PlayerArmorSetService.java index 38f497b..c33dabc 100644 --- a/src/main/java/gg/steve/mc/ap/player/SetPlayerManager.java +++ b/src/main/java/gg/steve/mc/ap/player/PlayerArmorSetService.java @@ -1,55 +1,58 @@ package gg.steve.mc.ap.player; +import com.google.inject.Inject; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Piece; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; +import gg.steve.mc.ap.model.id.ArmorSetId; +import gg.steve.mc.ap.model.id.PlayerId; +import gg.steve.mc.ap.model.player.PlayerArmorWearerRegistry; import gg.steve.mc.ap.nbt.NBTItem; -import gg.steve.mc.ap.utils.LogUtil; -import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +public class PlayerArmorSetService { + private final PlayerArmorWearerRegistry registry; + private final ArmorSetCatalog catalog; -public class SetPlayerManager { - private static Map playersWearingSets; + @Inject + public PlayerArmorSetService(PlayerArmorWearerRegistry registry, ArmorSetCatalog catalog) { + this.registry = registry; + this.catalog = catalog; + } - public static void init() { - playersWearingSets = new HashMap<>(); + public void init() { + registry.clear(); for (Player player : Bukkit.getOnlinePlayers()) { - for (Set set : SetManager.getSets().values()) { + for (Set set : catalog.getSets().values()) { if (!set.isWearingSet(player, null, null)) continue; - SetPlayerManager.addSetPlayer(player, set.getName()); + addSetPlayer(player, set.getName()); set.apply(player); return; } } } - public static void addSetPlayer(Player player, String setName) { - if (playersWearingSets == null) playersWearingSets = new HashMap<>(); - if (playersWearingSets.containsKey(player.getUniqueId())) removeSetPlayer(player); - playersWearingSets.put(player.getUniqueId(), new SetPlayer(player, setName)); + public void addSetPlayer(Player player, String setName) { + registry.add(PlayerId.of(player.getUniqueId()), ArmorSetId.of(setName)); } - public static void removeSetPlayer(Player player) { - if (playersWearingSets == null) playersWearingSets = new HashMap<>(); - playersWearingSets.remove(player.getUniqueId()); + public void removeSetPlayer(Player player) { + registry.remove(PlayerId.of(player.getUniqueId())); } - public static boolean isWearingSet(Player player) { - if (playersWearingSets == null) playersWearingSets = new HashMap<>(); - return playersWearingSets.containsKey(player.getUniqueId()); + public boolean isWearingSet(Player player) { + return registry.isWearing(PlayerId.of(player.getUniqueId())); } - public static SetPlayer getSetPlayer(Player player) { - return playersWearingSets.get(player.getUniqueId()); + public SetPlayer getSetPlayer(Player player) { + return registry.get(PlayerId.of(player.getUniqueId())) + .map(wearer -> new SetPlayer(player, catalog.getSet(wearer.getSetId().toString()))) + .orElse(null); } - public static int getPiecesWearing(Set set, Player player) { + public int getPiecesWearing(Set set, Player player) { int wearing = 0; for (Piece piece : set.getSetPieces().keySet()) { switch (piece) { @@ -73,10 +76,10 @@ public static int getPiecesWearing(Set set, Player player) { return wearing; } - public static Set getSetFromHand(Player player) { + public Set getSetFromHand(Player player) { if (player.getItemInHand() == null || player.getItemInHand().getType().equals(Material.AIR)) return null; NBTItem hand = new NBTItem(player.getItemInHand()); if (hand.getString("armor+.set").equalsIgnoreCase("")) return null; - return SetManager.getSet(hand.getString("armor+.set")); + return catalog.getSet(hand.getString("armor+.set")); } -} \ No newline at end of file +} diff --git a/src/main/java/gg/steve/mc/ap/player/SetPlayer.java b/src/main/java/gg/steve/mc/ap/player/SetPlayer.java index 034d48a..1f23978 100644 --- a/src/main/java/gg/steve/mc/ap/player/SetPlayer.java +++ b/src/main/java/gg/steve/mc/ap/player/SetPlayer.java @@ -1,16 +1,15 @@ package gg.steve.mc.ap.player; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; import org.bukkit.entity.Player; public class SetPlayer { private Player player; private Set set; - public SetPlayer(Player player, String setName) { + public SetPlayer(Player player, Set set) { this.player = player; - this.set = SetManager.getSet(setName); + this.set = set; } public Player getPlayer() { diff --git a/src/test/java/gg/steve/mc/ap/ArmorPlusModuleTest.java b/src/test/java/gg/steve/mc/ap/ArmorPlusModuleTest.java new file mode 100644 index 0000000..2cf373d --- /dev/null +++ b/src/test/java/gg/steve/mc/ap/ArmorPlusModuleTest.java @@ -0,0 +1,104 @@ +package gg.steve.mc.ap; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import gg.steve.mc.ap.armor.ArmorSetCatalog; +import gg.steve.mc.ap.armor.Set; +import gg.steve.mc.ap.model.id.ArmorSetId; +import gg.steve.mc.ap.model.player.PlayerArmorWearerRegistry; +import gg.steve.mc.ap.model.set.ArmorSetRegistry; +import gg.steve.mc.ap.player.PlayerArmorSetService; +import gg.steve.mc.ap.player.SetPlayer; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** Verifies the real Guice composition root wires shared singletons over the pure registries. */ +@ExtendWith(MockitoExtension.class) +class ArmorPlusModuleTest { + + private static final Path EVIDENCE = Path.of( + System.getProperty("java.io.tmpdir"), + "no-mistakes-evidence", "01KY78FKDPAVYJW7ME7YQYCTNA", + "guice-composition-root-transcript.txt"); + + private static final Key> SET_REGISTRY_KEY = + Key.get(new TypeLiteral>() {}); + + @Mock private ArmorPlus plugin; + @Mock private Set dragonSet; + @Mock private Player player; + + @Test + void injectorWiresSharedSingletonsAndTheyRoundTripState() { + List transcript = new ArrayList<>(); + + Injector injector = Guice.createInjector(new ArmorPlusModule(plugin)); + + ArmorSetCatalog catalog = injector.getInstance(ArmorSetCatalog.class); + PlayerArmorSetService service = injector.getInstance(PlayerArmorSetService.class); + assertSame(catalog, injector.getInstance(ArmorSetCatalog.class), + "catalog must be a shared singleton (replacing the old static SetManager)"); + assertSame(service, injector.getInstance(PlayerArmorSetService.class), + "player service must be a shared singleton (replacing the old static SetPlayerManager)"); + transcript.add("injector.getInstance(ArmorSetCatalog) -> singleton " + (catalog != null)); + transcript.add("injector.getInstance(PlayerArmorSetService) -> singleton " + (service != null)); + + ArmorSetRegistry sharedRegistry = injector.getInstance(SET_REGISTRY_KEY); + assertSame(sharedRegistry, catalog.getRegistry(), + "the catalog must own the singleton ArmorSetRegistry the injector binds"); + assertSame(injector.getInstance(PlayerArmorWearerRegistry.class), + injector.getInstance(PlayerArmorWearerRegistry.class), + "wearer registry must be a shared singleton too"); + transcript.add("catalog.getRegistry() == injected ArmorSetRegistry singleton -> " + + (sharedRegistry == catalog.getRegistry())); + + // round-trips only if Guice threaded one shared catalog into the service + UUID id = UUID.fromString("00000000-0000-0000-0000-0000000000aa"); + org.mockito.Mockito.when(player.getUniqueId()).thenReturn(id); + + catalog.getRegistry().register(ArmorSetId.of("dragon"), dragonSet); + service.addSetPlayer(player, "dragon"); + + assertTrue(service.isWearingSet(player), "service must see the player it just recorded"); + SetPlayer resolved = service.getSetPlayer(player); + assertNotNull(resolved, "getSetPlayer must reconstruct the wearer"); + assertSame(dragonSet, resolved.getSet(), + "the set resolved through the service must be the one registered on the shared catalog"); + transcript.add("catalog.register(dragon); service.addSetPlayer(player, \"dragon\")"); + transcript.add("service.isWearingSet(player) -> " + service.isWearingSet(player)); + transcript.add("service.getSetPlayer(player).getSet() -> same instance registered on catalog: " + + (resolved.getSet() == dragonSet)); + + writeTranscript(transcript); + } + + private void writeTranscript(List body) { + List lines = new ArrayList<>(); + lines.add("=== Guice composition root wires shared singletons over the pure registries ==="); + lines.add("(built from the real ArmorPlusModule via a live Injector - the same wiring ArmorPlus.onEnable uses)"); + lines.addAll(body); + try { + Files.createDirectories(EVIDENCE.getParent()); + Files.write(EVIDENCE, (String.join(System.lineSeparator(), lines) + System.lineSeparator()) + .getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/test/java/gg/steve/mc/ap/armor/ArmorSetCatalogTest.java b/src/test/java/gg/steve/mc/ap/armor/ArmorSetCatalogTest.java new file mode 100644 index 0000000..b11a247 --- /dev/null +++ b/src/test/java/gg/steve/mc/ap/armor/ArmorSetCatalogTest.java @@ -0,0 +1,40 @@ +package gg.steve.mc.ap.armor; + +import gg.steve.mc.ap.ArmorPlus; +import gg.steve.mc.ap.model.set.ArmorSetRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; + +// getSet must return null for null/blank names: callers rely on it, but ArmorSetId.of rejects them. +@ExtendWith(MockitoExtension.class) +class ArmorSetCatalogTest { + + @Mock private ArmorPlus plugin; + + private ArmorSetCatalog catalog; + + @BeforeEach + void setUp() { + catalog = new ArmorSetCatalog(new ArmorSetRegistry<>(), plugin); + } + + @Test + void getSet_nullName_returnsNullNotThrows() { + assertNull(catalog.getSet(null)); + } + + @Test + void getSet_emptyName_returnsNullNotThrows() { + assertNull(catalog.getSet("")); + } + + @Test + void getSet_unknownName_returnsNull() { + assertNull(catalog.getSet("no-such-set")); + } +} diff --git a/src/test/java/gg/steve/mc/ap/listener/PlayerCommandListenerTest.java b/src/test/java/gg/steve/mc/ap/listener/PlayerCommandListenerTest.java new file mode 100644 index 0000000..f656ac6 --- /dev/null +++ b/src/test/java/gg/steve/mc/ap/listener/PlayerCommandListenerTest.java @@ -0,0 +1,135 @@ +package gg.steve.mc.ap.listener; + +import gg.steve.mc.ap.ArmorPlus; +import gg.steve.mc.ap.armor.ArmorSetCatalog; +import gg.steve.mc.ap.armor.Set; +import gg.steve.mc.ap.managers.FileManager; +import gg.steve.mc.ap.model.id.ArmorSetId; +import gg.steve.mc.ap.model.set.ArmorSetRegistry; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PlayerCommandListenerTest { + + private static final Path EVIDENCE = Path.of( + System.getProperty("java.io.tmpdir"), + "no-mistakes-evidence", "01KY69XZQMQHZWP62V198YSZ3P", + "player-command-intercept-transcript.txt"); + + @Mock private Player player; + @Mock private Set dragonSet; + @Mock private Set knightSet; + @Mock private YamlConfiguration permissionsConfig; + @Mock private ArmorPlus plugin; + + private ArmorSetCatalog catalog; + private PlayerCommandListener listener; + private final List transcript = new ArrayList<>(); + + @BeforeEach + void setUp() { + catalog = new ArmorSetCatalog(new ArmorSetRegistry<>(), plugin); + catalog.getRegistry().register(ArmorSetId.of("dragon"), dragonSet); + catalog.getRegistry().register(ArmorSetId.of("knight"), knightSet); + listener = new PlayerCommandListener(catalog); + } + + private PlayerCommandPreprocessEvent type(String typed) { + // explicit recipient set avoids the internal getServer().getOnlinePlayers() the two-arg form calls + PlayerCommandPreprocessEvent event = + new PlayerCommandPreprocessEvent(player, typed, Collections.emptySet()); + listener.onCmd(event); + transcript.add(String.format("player types %-22s -> intercepted=%s", "\"" + typed + "\"", event.isCancelled())); + return event; + } + + private void writeTranscript(String title) { + List lines = new ArrayList<>(); + lines.add("=== " + title + " ==="); + lines.add("Registry keys (ArmorSetId): " + new ArrayList<>(catalog.getSets().keySet())); + lines.addAll(transcript); + try { + Files.createDirectories(EVIDENCE.getParent()); + Files.write(EVIDENCE, (String.join(System.lineSeparator(), lines) + System.lineSeparator()) + .getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Test + void typingSetName_intercepted_opensThatSetsGui() { + when(player.hasPermission(anyString())).thenReturn(true); + try (MockedStatic fm = mockStatic(FileManager.class)) { + fm.when(() -> FileManager.get("PERMISSIONS")).thenReturn(permissionsConfig); + when(permissionsConfig.getString("command.gui")).thenReturn("armorplus.command.gui"); + + PlayerCommandPreprocessEvent event = type("/dragon"); + + assertTrue(event.isCancelled(), "typing a known set name must be intercepted"); + verify(dragonSet).openGui(player); + verify(knightSet, never()).openGui(any()); + writeTranscript("Player types a set command -> that set's GUI opens"); + } + } + + @Test + void typingSetName_isCaseInsensitive() { + when(player.hasPermission(anyString())).thenReturn(true); + try (MockedStatic fm = mockStatic(FileManager.class)) { + fm.when(() -> FileManager.get("PERMISSIONS")).thenReturn(permissionsConfig); + when(permissionsConfig.getString("command.gui")).thenReturn("armorplus.command.gui"); + + PlayerCommandPreprocessEvent event = type("/DRAGON"); + + assertTrue(event.isCancelled(), "matching is case-insensitive, as before"); + verify(dragonSet).openGui(player); + } + } + + @Test + void typingUnknownCommand_passesThroughUnintercepted() { + PlayerCommandPreprocessEvent event = type("/spawn"); + + assertFalse(event.isCancelled(), "commands that are not set names must not be intercepted"); + verify(dragonSet, never()).openGui(any()); + verify(knightSet, never()).openGui(any()); + } + + @Test + void eachSetNameRoutesToItsOwnSet() { + when(player.hasPermission(anyString())).thenReturn(true); + try (MockedStatic fm = mockStatic(FileManager.class)) { + fm.when(() -> FileManager.get("PERMISSIONS")).thenReturn(permissionsConfig); + when(permissionsConfig.getString("command.gui")).thenReturn("armorplus.command.gui"); + + type("/dragon"); + type("/knight"); + type("/spawn"); + + verify(dragonSet).openGui(player); + verify(knightSet).openGui(player); + writeTranscript("Each set command routes to its own set; unknown command passes through"); + } + } +} diff --git a/src/test/java/gg/steve/mc/ap/model/player/PlayerArmorWearerRegistryTest.java b/src/test/java/gg/steve/mc/ap/model/player/PlayerArmorWearerRegistryTest.java new file mode 100644 index 0000000..ac05153 --- /dev/null +++ b/src/test/java/gg/steve/mc/ap/model/player/PlayerArmorWearerRegistryTest.java @@ -0,0 +1,102 @@ +package gg.steve.mc.ap.model.player; + +import gg.steve.mc.ap.model.id.ArmorSetId; +import gg.steve.mc.ap.model.id.PlayerId; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class PlayerArmorWearerRegistryTest { + + private final PlayerId player = PlayerId.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); + private final PlayerId other = PlayerId.of(UUID.fromString("00000000-0000-0000-0000-000000000002")); + private final ArmorSetId dragon = ArmorSetId.of("dragon"); + private final ArmorSetId knight = ArmorSetId.of("knight"); + + @Test + void freshRegistryHasNoWearers() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + + assertEquals(0, registry.count()); + assertFalse(registry.isWearing(player)); + assertEquals(Optional.empty(), registry.get(player)); + } + + @Test + void addFromParts_recordsWearer() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + registry.add(player, dragon); + + assertTrue(registry.isWearing(player)); + assertEquals(1, registry.count()); + assertEquals(Optional.of(new ArmorSetWearer(player, dragon)), registry.get(player)); + } + + @Test + void addWearer_recordsWearer() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + ArmorSetWearer wearer = new ArmorSetWearer(player, dragon); + registry.add(wearer); + + assertTrue(registry.isWearing(player)); + assertEquals(Optional.of(wearer), registry.get(player)); + } + + @Test + void add_samePlayerTwice_replacesExisting() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + registry.add(player, dragon); + registry.add(player, knight); + + assertEquals(1, registry.count()); + assertEquals(Optional.of(new ArmorSetWearer(player, knight)), registry.get(player)); + } + + @Test + void remove_forgetsWearer() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + registry.add(player, dragon); + registry.remove(player); + + assertFalse(registry.isWearing(player)); + assertEquals(Optional.empty(), registry.get(player)); + assertEquals(0, registry.count()); + } + + @Test + void remove_absentPlayer_isNoOp() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + + assertDoesNotThrow(() -> registry.remove(player)); + assertEquals(0, registry.count()); + } + + @Test + void multiplePlayers_trackedIndependently() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + registry.add(player, dragon); + registry.add(other, knight); + + assertEquals(2, registry.count()); + registry.remove(player); + + assertFalse(registry.isWearing(player)); + assertTrue(registry.isWearing(other)); + assertEquals(Optional.of(new ArmorSetWearer(other, knight)), registry.get(other)); + } + + @Test + void clear_dropsEveryWearer() { + PlayerArmorWearerRegistry registry = new PlayerArmorWearerRegistry(); + registry.add(player, dragon); + registry.add(other, knight); + registry.clear(); + + assertEquals(0, registry.count()); + assertFalse(registry.isWearing(player)); + assertFalse(registry.isWearing(other)); + } +} diff --git a/src/test/java/gg/steve/mc/ap/model/set/ArmorSetRegistryTest.java b/src/test/java/gg/steve/mc/ap/model/set/ArmorSetRegistryTest.java new file mode 100644 index 0000000..abf927a --- /dev/null +++ b/src/test/java/gg/steve/mc/ap/model/set/ArmorSetRegistryTest.java @@ -0,0 +1,85 @@ +package gg.steve.mc.ap.model.set; + +import gg.steve.mc.ap.model.id.ArmorSetId; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class ArmorSetRegistryTest { + + private final ArmorSetId dragon = ArmorSetId.of("dragon"); + private final ArmorSetId knight = ArmorSetId.of("knight"); + private final ArmorSetId mage = ArmorSetId.of("mage"); + + @Test + void freshRegistryIsEmpty() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + + assertEquals(0, registry.count()); + assertEquals(Optional.empty(), registry.get(dragon)); + assertTrue(registry.getAll().isEmpty()); + } + + @Test + void register_thenGet_returnsStoredSet() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + registry.register(dragon, "dragon-set"); + + assertEquals(Optional.of("dragon-set"), registry.get(dragon)); + assertEquals(1, registry.count()); + } + + @Test + void get_unknownId_returnsEmpty() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + registry.register(dragon, "dragon-set"); + + assertEquals(Optional.empty(), registry.get(knight)); + } + + @Test + void register_sameId_replacesExisting() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + registry.register(dragon, "first"); + registry.register(dragon, "second"); + + assertEquals(Optional.of("second"), registry.get(dragon)); + assertEquals(1, registry.count()); + } + + @Test + void getAll_preservesRegistrationOrder() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + registry.register(mage, "mage-set"); + registry.register(dragon, "dragon-set"); + registry.register(knight, "knight-set"); + + List order = new ArrayList<>(registry.getAll().keySet()); + assertEquals(List.of(mage, dragon, knight), order); + } + + @Test + void getAll_isUnmodifiable() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + registry.register(dragon, "dragon-set"); + + Map all = registry.getAll(); + assertThrows(UnsupportedOperationException.class, () -> all.put(knight, "knight-set")); + } + + @Test + void clear_dropsEverySet() { + ArmorSetRegistry registry = new ArmorSetRegistry<>(); + registry.register(dragon, "dragon-set"); + registry.register(knight, "knight-set"); + registry.clear(); + + assertEquals(0, registry.count()); + assertEquals(Optional.empty(), registry.get(dragon)); + } +} diff --git a/src/test/java/gg/steve/mc/ap/player/SetPlayerManagerTest.java b/src/test/java/gg/steve/mc/ap/player/PlayerArmorSetServiceTest.java similarity index 67% rename from src/test/java/gg/steve/mc/ap/player/SetPlayerManagerTest.java rename to src/test/java/gg/steve/mc/ap/player/PlayerArmorSetServiceTest.java index fa467e9..f6189ba 100644 --- a/src/test/java/gg/steve/mc/ap/player/SetPlayerManagerTest.java +++ b/src/test/java/gg/steve/mc/ap/player/PlayerArmorSetServiceTest.java @@ -1,17 +1,16 @@ package gg.steve.mc.ap.player; +import gg.steve.mc.ap.armor.ArmorSetCatalog; import gg.steve.mc.ap.armor.Piece; import gg.steve.mc.ap.armor.Set; -import gg.steve.mc.ap.armor.SetManager; +import gg.steve.mc.ap.model.player.PlayerArmorWearerRegistry; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import java.util.LinkedHashMap; @@ -21,91 +20,76 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -/** - * Characterization tests for SetPlayerManager state transitions. - * Pins add/remove/isWearing/getPiecesWearing before 4E extracts WearerRegistry. - */ @ExtendWith(MockitoExtension.class) -class SetPlayerManagerTest { +class PlayerArmorSetServiceTest { @Mock private Player player1; @Mock private Player player2; @Mock private Set mockSet; + @Mock private ArmorSetCatalog catalog; private final UUID uuid1 = UUID.randomUUID(); private final UUID uuid2 = UUID.randomUUID(); - private MockedStatic setManagerMock; + private PlayerArmorSetService service; @BeforeEach void setUp() { - when(player1.getUniqueId()).thenReturn(uuid1); + lenient().when(player1.getUniqueId()).thenReturn(uuid1); lenient().when(player2.getUniqueId()).thenReturn(uuid2); + lenient().when(catalog.getSet(anyString())).thenReturn(mockSet); - // SetPlayer constructor calls SetManager.getSet(name) - mock it - setManagerMock = mockStatic(SetManager.class); - setManagerMock.when(() -> SetManager.getSet(anyString())).thenReturn(mockSet); - - // Reset the static state by removing any players - SetPlayerManager.removeSetPlayer(player1); - SetPlayerManager.removeSetPlayer(player2); - } - - @AfterEach - void tearDown() { - setManagerMock.close(); + service = new PlayerArmorSetService(new PlayerArmorWearerRegistry(), catalog); } @Test void addSetPlayer_playerNotWearing_addsSuccessfully() { - SetPlayerManager.addSetPlayer(player1, "warrior"); + service.addSetPlayer(player1, "warrior"); - assertTrue(SetPlayerManager.isWearingSet(player1)); + assertTrue(service.isWearingSet(player1)); } @Test void addSetPlayer_playerAlreadyWearing_replacesExisting() { - SetPlayerManager.addSetPlayer(player1, "warrior"); - SetPlayerManager.addSetPlayer(player1, "mage"); + service.addSetPlayer(player1, "warrior"); + service.addSetPlayer(player1, "mage"); - assertTrue(SetPlayerManager.isWearingSet(player1)); - SetPlayer sp = SetPlayerManager.getSetPlayer(player1); + assertTrue(service.isWearingSet(player1)); + SetPlayer sp = service.getSetPlayer(player1); assertNotNull(sp); } @Test void removeSetPlayer_playerWearing_removesSuccessfully() { - SetPlayerManager.addSetPlayer(player1, "warrior"); - SetPlayerManager.removeSetPlayer(player1); + service.addSetPlayer(player1, "warrior"); + service.removeSetPlayer(player1); - assertFalse(SetPlayerManager.isWearingSet(player1)); + assertFalse(service.isWearingSet(player1)); } @Test void removeSetPlayer_playerNotWearing_noError() { - assertDoesNotThrow(() -> SetPlayerManager.removeSetPlayer(player1)); + assertDoesNotThrow(() -> service.removeSetPlayer(player1)); } @Test void isWearingSet_uninitializedMap_returnsFalse() { - assertFalse(SetPlayerManager.isWearingSet(player2)); + assertFalse(service.isWearingSet(player2)); } @Test void multiplePlayers_independentTracking() { - SetPlayerManager.addSetPlayer(player1, "warrior"); - SetPlayerManager.addSetPlayer(player2, "mage"); + service.addSetPlayer(player1, "warrior"); + service.addSetPlayer(player2, "mage"); - assertTrue(SetPlayerManager.isWearingSet(player1)); - assertTrue(SetPlayerManager.isWearingSet(player2)); + assertTrue(service.isWearingSet(player1)); + assertTrue(service.isWearingSet(player2)); - SetPlayerManager.removeSetPlayer(player1); - assertFalse(SetPlayerManager.isWearingSet(player1)); - assertTrue(SetPlayerManager.isWearingSet(player2)); + service.removeSetPlayer(player1); + assertFalse(service.isWearingSet(player1)); + assertTrue(service.isWearingSet(player2)); } - // --- getPiecesWearing characterization --- - @Test void getPiecesWearing_allPiecesVerified_countsAll( @Mock Set set, @Mock PlayerInventory inv, @@ -132,7 +116,7 @@ void getPiecesWearing_allPiecesVerified_countsAll( when(set.verifyPiece(boots)).thenReturn(true); when(set.verifyPiece(hand)).thenReturn(true); - int result = SetPlayerManager.getPiecesWearing(set, player1); + int result = service.getPiecesWearing(set, player1); assertEquals(5, result); } @@ -156,7 +140,7 @@ void getPiecesWearing_noPiecesVerified_returnsZero( when(set.verifyPiece(any(ItemStack.class))).thenReturn(false); - int result = SetPlayerManager.getPiecesWearing(set, player1); + int result = service.getPiecesWearing(set, player1); assertEquals(0, result); } @@ -183,7 +167,7 @@ void getPiecesWearing_partialSet_countsOnlyVerified( when(set.verifyPiece(legs)).thenReturn(true); when(set.verifyPiece(boots)).thenReturn(false); - int result = SetPlayerManager.getPiecesWearing(set, player1); + int result = service.getPiecesWearing(set, player1); assertEquals(2, result); } }