From a9c5aeccdc29792b69fb0a301f126c0db5f5ec4d Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Thu, 29 Oct 2026 08:20:03 -0400 Subject: [PATCH] feat(tree-feller): support Poplar progression --- .../games/dmg/treefeller/TreeSpecies.java | 1 + .../games/dmg/treefeller/TreeTaxonomy.java | 5 + src/main/resources/config.yml | 1 + .../AnimatedTreeFellingEngineTest.java | 27 ++++ .../treefeller/TreeFellingListenerTest.java | 30 ++++ .../treefeller/TreeProgressListenerTest.java | 35 +++++ .../dmg/treefeller/TreeUndoServiceTest.java | 18 +++ .../RegisteredCommandAutocompleteTest.java | 133 ++++++++++++++++++ .../TreeFellerAdminCommandTest.java | 54 ++++++- .../dmg/treefeller/TreeFellerCommandTest.java | 1 + .../treefeller/TreeFellerSettingsTest.java | 14 ++ .../treefeller/TreeStructureScannerTest.java | 21 +++ .../dmg/treefeller/TreeTaxonomyTest.java | 8 ++ .../YamlPlayerStateRepositoryTest.java | 31 ++++ 14 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 src/test/java/games/dmg/treefeller/RegisteredCommandAutocompleteTest.java diff --git a/src/main/java/games/dmg/treefeller/TreeSpecies.java b/src/main/java/games/dmg/treefeller/TreeSpecies.java index cc0a471..eb65b7d 100644 --- a/src/main/java/games/dmg/treefeller/TreeSpecies.java +++ b/src/main/java/games/dmg/treefeller/TreeSpecies.java @@ -15,6 +15,7 @@ public enum TreeSpecies { MANGROVE("mangrove", "Mangrove"), CHERRY("cherry", "Cherry"), PALE_OAK("pale-oak", "Pale Oak"), + POPLAR("poplar", "Poplar"), CRIMSON("crimson", "Crimson Fungus"), WARPED("warped", "Warped Fungus"), RED_MUSHROOM("red-mushroom", "Giant Red Mushroom"), diff --git a/src/main/java/games/dmg/treefeller/TreeTaxonomy.java b/src/main/java/games/dmg/treefeller/TreeTaxonomy.java index 9b00772..75edf0f 100644 --- a/src/main/java/games/dmg/treefeller/TreeTaxonomy.java +++ b/src/main/java/games/dmg/treefeller/TreeTaxonomy.java @@ -19,6 +19,7 @@ public final class TreeTaxonomy { Map.entry(Material.MANGROVE_LOG, TreeSpecies.MANGROVE), Map.entry(Material.CHERRY_LOG, TreeSpecies.CHERRY), Map.entry(Material.PALE_OAK_LOG, TreeSpecies.PALE_OAK), + Map.entry(Material.POPLAR_LOG, TreeSpecies.POPLAR), Map.entry(Material.CRIMSON_STEM, TreeSpecies.CRIMSON), Map.entry(Material.WARPED_STEM, TreeSpecies.WARPED)); @@ -64,6 +65,10 @@ public final class TreeTaxonomy { foliage.put(TreeSpecies.MANGROVE, Set.of(Material.MANGROVE_LEAVES)); foliage.put(TreeSpecies.CHERRY, Set.of(Material.CHERRY_LEAVES)); foliage.put(TreeSpecies.PALE_OAK, Set.of(Material.PALE_OAK_LEAVES)); + foliage.put(TreeSpecies.POPLAR, Set.of( + Material.ORANGE_POPLAR_LEAVES, + Material.RED_POPLAR_LEAVES, + Material.YELLOW_POPLAR_LEAVES)); foliage.put(TreeSpecies.CRIMSON, Set.of(Material.NETHER_WART_BLOCK)); foliage.put(TreeSpecies.WARPED, Set.of(Material.WARPED_WART_BLOCK)); foliage.put(TreeSpecies.RED_MUSHROOM, Set.of(Material.RED_MUSHROOM_BLOCK)); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 68d6302..c4d171e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -8,6 +8,7 @@ thresholds: mangrove: 100 cherry: 100 pale-oak: 100 + poplar: 100 crimson: 100 warped: 100 red-mushroom: 100 diff --git a/src/nativeTest/java/games/dmg/treefeller/AnimatedTreeFellingEngineTest.java b/src/nativeTest/java/games/dmg/treefeller/AnimatedTreeFellingEngineTest.java index 92a4527..4da61b7 100644 --- a/src/nativeTest/java/games/dmg/treefeller/AnimatedTreeFellingEngineTest.java +++ b/src/nativeTest/java/games/dmg/treefeller/AnimatedTreeFellingEngineTest.java @@ -65,6 +65,33 @@ class AnimatedTreeFellingEngineTest { assertEquals(3, completed.size()); } + @Test + void progressivelyFellsPoplarUsingNativeBlockAndToolMaterials() { + TestTree tree = new TestTree(); + BlockPoint origin = new BlockPoint(0, 0, 0); + tree.add(origin, Material.POPLAR_LOG); + tree.add(new BlockPoint(0, 1, 0), Material.POPLAR_LOG); + QueueScheduler scheduler = new QueueScheduler(); + when(tree.player().breakBlock(any(Block.class))).thenReturn(true); + List completed = new ArrayList<>(); + AnimatedTreeFellingEngine engine = new AnimatedTreeFellingEngine( + scheduler, + new AutomaticBreakRegistry(), + ignored -> true, + (ignored, snapshots) -> completed.addAll(snapshots), + ignored -> { }); + + assertTrue(engine.start( + tree.player(), + tree.block(origin), + new TreeStructure(TreeSpecies.POPLAR, List.of(origin, new BlockPoint(0, 1, 0))), + 2)); + scheduler.runAll(); + + assertEquals(List.of(Material.POPLAR_LOG, Material.POPLAR_LOG), + completed.stream().map(FelledBlockSnapshot::material).toList()); + } + @Test void stopsAfterAProtectionCancellationAndDoesNotRunAnOverlappingFelling() { TestTree tree = new TestTree(); diff --git a/src/nativeTest/java/games/dmg/treefeller/TreeFellingListenerTest.java b/src/nativeTest/java/games/dmg/treefeller/TreeFellingListenerTest.java index 1af05ee..20f7b39 100644 --- a/src/nativeTest/java/games/dmg/treefeller/TreeFellingListenerTest.java +++ b/src/nativeTest/java/games/dmg/treefeller/TreeFellingListenerTest.java @@ -57,6 +57,36 @@ class TreeFellingListenerTest { verify(starter, org.mockito.Mockito.times(1)).start(any(), any(), any(), eq(2)); } + @Test + void startsUnlockedPoplarFellingThroughTheNativeBoundary() { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + PlayerInventory inventory = mock(PlayerInventory.class); + Block block = mock(Block.class); + World world = mock(World.class); + when(block.getWorld()).thenReturn(world); + when(block.getType()).thenReturn(Material.POPLAR_LOG); + when(world.getUID()).thenReturn(UUID.randomUUID()); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Player"); + when(player.getGameMode()).thenReturn(GameMode.SURVIVAL); + when(player.getInventory()).thenReturn(inventory); + when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.IRON_AXE)); + TreeStructure tree = new TreeStructure( + TreeSpecies.POPLAR, List.of(new BlockPoint(0, 0, 0), new BlockPoint(0, 1, 0))); + PlayerStateStore states = mock(PlayerStateStore.class); + when(states.load(playerId)).thenReturn(Optional.of( + PlayerTreeFellerState.initial(playerId, "Player") + .withUnlocked(TreeSpecies.POPLAR, true))); + TreeFellingStarter starter = mock(TreeFellingStarter.class); + TreeFellingListener listener = new TreeFellingListener( + ignored -> Optional.of(tree), states, new AutomaticBreakRegistry(), starter, () -> 2); + + listener.onBlockBreak(new BlockBreakEvent(block, player)); + + verify(starter).start(player, block, tree, 2); + } + @Test void aNonAxeCanEarnProgressButNeverStartsAutomaticFelling() { UUID playerId = UUID.randomUUID(); diff --git a/src/nativeTest/java/games/dmg/treefeller/TreeProgressListenerTest.java b/src/nativeTest/java/games/dmg/treefeller/TreeProgressListenerTest.java index 8d5be9e..6f0a3ba 100644 --- a/src/nativeTest/java/games/dmg/treefeller/TreeProgressListenerTest.java +++ b/src/nativeTest/java/games/dmg/treefeller/TreeProgressListenerTest.java @@ -21,6 +21,41 @@ class TreeProgressListenerTest { @org.junit.jupiter.api.BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); } + @Test + void recordsAndPersistsPoplarProgressThroughUnlock() throws Exception { + UUID playerId = UUID.randomUUID(); + Player player = mock(Player.class); + PlayerInventory inventory = mock(PlayerInventory.class); + Block block = mock(Block.class); + World world = mock(World.class); + when(player.getUniqueId()).thenReturn(playerId); + when(player.getName()).thenReturn("Player"); + when(player.getGameMode()).thenReturn(GameMode.SURVIVAL); + when(player.getInventory()).thenReturn(inventory); + when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.IRON_AXE)); + when(block.getWorld()).thenReturn(world); + when(block.getType()).thenReturn(Material.POPLAR_LOG); + when(world.getUID()).thenReturn(UUID.randomUUID()); + when(block.getX()).thenReturn(1); + when(block.getY()).thenReturn(64); + when(block.getZ()).thenReturn(2); + InMemoryStateStore states = new InMemoryStateStore(); + states.state = PlayerTreeFellerState.initial(playerId, "Player") + .withProgress(TreeSpecies.POPLAR, 1); + TreeProgressListener listener = new TreeProgressListener( + ignored -> Optional.empty(), + states, + ignored -> 2, + new AutomaticBreakRegistry(), + (ignoredPlayer, ignoredUpdate) -> { }, + ignored -> { }); + + listener.onBlockBreak(new BlockBreakEvent(block, player)); + + assertEquals(2, states.state.progress(TreeSpecies.POPLAR)); + assertEquals(true, states.state.isUnlocked(TreeSpecies.POPLAR)); + } + @Test void recordsNaturalTrunkMaterialAfterTheTreeStructureIsNoLongerIntact() throws Exception { UUID playerId = UUID.randomUUID(); diff --git a/src/nativeTest/java/games/dmg/treefeller/TreeUndoServiceTest.java b/src/nativeTest/java/games/dmg/treefeller/TreeUndoServiceTest.java index d3b2376..c238011 100644 --- a/src/nativeTest/java/games/dmg/treefeller/TreeUndoServiceTest.java +++ b/src/nativeTest/java/games/dmg/treefeller/TreeUndoServiceTest.java @@ -67,6 +67,24 @@ class TreeUndoServiceTest { assertTrue(fixture.store.get(fixture.playerId).isEmpty()); } + @Test + void consumesAndRestoresOnlyFelledPoplarTrunks() { + Fixture fixture = new Fixture(FELLED_AT.plusSeconds(60)); + fixture.record(List.of( + fixture.snapshot(0, Material.POPLAR_LOG), + fixture.snapshot(1, Material.POPLAR_LOG))); + when(fixture.inventory.getStorageContents()).thenReturn(new ItemStack[] { + new ItemStack(Material.POPLAR_LOG, 2) + }); + + UndoResult result = fixture.service().undo(fixture.player, 6); + + assertEquals(UndoStatus.SUCCESS, result.status()); + verify(fixture.inventory).setItem(0, null); + verify(fixture.block, org.mockito.Mockito.times(2)) + .setBlockData(fixture.restoredData, false); + } + @Test void refusesTheWholeUndoWhenAnyRestorationPositionIsOccupied() { Fixture fixture = new Fixture(FELLED_AT.plusSeconds(60)); diff --git a/src/test/java/games/dmg/treefeller/RegisteredCommandAutocompleteTest.java b/src/test/java/games/dmg/treefeller/RegisteredCommandAutocompleteTest.java new file mode 100644 index 0000000..c8b64ce --- /dev/null +++ b/src/test/java/games/dmg/treefeller/RegisteredCommandAutocompleteTest.java @@ -0,0 +1,133 @@ +package games.dmg.treefeller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.bukkit.Server; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.SimpleCommandMap; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.Test; + +class RegisteredCommandAutocompleteTest { + @Test + void registeredPlayerCommandFiltersEveryApplicableArgumentPositionAndDeniesPermission() + throws Exception { + InMemoryCatalog states = new InMemoryCatalog(); + SimpleCommandMap commands = registered( + "treefeller", + "treefeller.command", + new TreeFellerCommand(states, ignored -> { })); + Player denied = mock(Player.class); + Player player = mock(Player.class); + when(player.hasPermission("treefeller.command")).thenReturn(true); + + assertEquals(List.of(), complete(commands, denied, "treefeller unl")); + assertEquals(List.of("unlocked"), complete(commands, player, "treefeller unl")); + assertEquals(List.of("off", "on"), complete(commands, player, "treefeller enabled ")); + assertEquals(List.of(), complete(commands, player, "treefeller unlocked extra")); + assertEquals(List.of(), complete(commands, player, "treefeller unsupported ")); + assertEquals(0, states.saveCount); + } + + @Test + void registeredAdminCommandFiltersPoplarAtEverySpeciesPositionAndNeverMutates() + throws Exception { + Player target = mock(Player.class); + when(target.getUniqueId()).thenReturn(UUID.randomUUID()); + when(target.getName()).thenReturn("Target"); + Server server = mock(Server.class); + Collection players = List.of(target); + when(server.getOnlinePlayers()).thenAnswer(ignored -> players); + when(server.getPlayer(target.getUniqueId())).thenReturn(target); + InMemoryCatalog states = new InMemoryCatalog(); + TreeFellerAdminCommand handler = new TreeFellerAdminCommand( + server, + states, + new TreeFellerSettingsService(defaults(), (species, threshold) -> { }), + ignored -> { }); + SimpleCommandMap commands = registered( + "treefelleradmin", "treefeller.admin", handler); + CommandSender denied = mock(CommandSender.class); + CommandSender administrator = mock(CommandSender.class); + when(administrator.hasPermission("treefeller.admin")).thenReturn(true); + + assertEquals(List.of(), complete(commands, denied, "treefelleradmin ")); + assertEquals(List.of("poplar"), complete( + commands, administrator, "treefelleradmin threshold pop")); + assertEquals(List.of("poplar"), complete( + commands, administrator, "treefelleradmin player Target tree pop")); + assertEquals(List.of("grant"), complete( + commands, administrator, "treefelleradmin player Target tree poplar g")); + assertEquals(List.of(), complete( + commands, administrator, "treefelleradmin player Target tree poplar grant extra")); + assertEquals(List.of(), complete( + commands, administrator, "treefelleradmin unsupported ")); + assertEquals(0, states.saveCount); + } + + private List complete( + SimpleCommandMap commands, CommandSender sender, String commandLine) { + List completions = commands.tabComplete(sender, commandLine); + if (completions == null) { + return List.of(); + } + return completions.stream() + .map(value -> value.substring(value.lastIndexOf(' ') + 1)) + .toList(); + } + + private SimpleCommandMap registered( + String name, String permission, CommandExecutor handler) throws Exception { + Constructor constructor = PluginCommand.class + .getDeclaredConstructor(String.class, Plugin.class); + constructor.setAccessible(true); + PluginCommand command = constructor.newInstance(name, mock(Plugin.class)); + command.setPermission(permission); + command.setExecutor(handler); + command.setTabCompleter((org.bukkit.command.TabCompleter) handler); + SimpleCommandMap commandMap = new SimpleCommandMap(mock(Server.class), new HashMap<>()); + commandMap.register("treefeller", command); + return commandMap; + } + + private TreeFellerSettings defaults() throws Exception { + try (InputStreamReader reader = new InputStreamReader( + getClass().getClassLoader().getResourceAsStream("config.yml"), + StandardCharsets.UTF_8)) { + return TreeFellerSettings.load(YamlConfiguration.loadConfiguration(reader)); + } + } + + private static final class InMemoryCatalog implements PlayerStateCatalog { + private int saveCount; + + @Override + public Optional load(UUID playerId) { + return Optional.empty(); + } + + @Override + public List loadAll() { + return List.of(); + } + + @Override + public void save(PlayerTreeFellerState changed) { + saveCount++; + } + } +} diff --git a/src/test/java/games/dmg/treefeller/TreeFellerAdminCommandTest.java b/src/test/java/games/dmg/treefeller/TreeFellerAdminCommandTest.java index 39474ae..017b5d1 100644 --- a/src/test/java/games/dmg/treefeller/TreeFellerAdminCommandTest.java +++ b/src/test/java/games/dmg/treefeller/TreeFellerAdminCommandTest.java @@ -84,20 +84,47 @@ class TreeFellerAdminCommandTest { @Test void changesAndPersistsAValidatedSpeciesThreshold() throws Exception { InMemoryCatalog states = new InMemoryCatalog(); + List saves = new ArrayList<>(); TreeFellerSettingsService settings = new TreeFellerSettingsService( - defaults(), (species, threshold) -> { }); + defaults(), (species, threshold) -> saves.add(species.id() + ":" + threshold)); TreeFellerAdminCommand handler = new TreeFellerAdminCommand( mock(Server.class), states, settings, ignored -> { }); CommandSender administrator = administrator(); handler.onCommand(administrator, mock(Command.class), "treefelleradmin", - new String[] {"threshold", "dark-oak", "25"}); + new String[] {"threshold", "poplar", "25"}); - assertEquals(25, settings.current().threshold(TreeSpecies.DARK_OAK)); - verify(administrator).sendMessage(contains("Dark Oak")); + assertEquals(List.of("poplar:25"), saves); + assertEquals(25, settings.current().threshold(TreeSpecies.POPLAR)); + verify(administrator).sendMessage(contains("Poplar")); verify(administrator).sendMessage(contains("25")); } + @Test + void administersPoplarAndRejectsUnsupportedArgumentsWithoutMutation() throws Exception { + UUID playerId = UUID.randomUUID(); + Player target = mock(Player.class); + when(target.getUniqueId()).thenReturn(playerId); + when(target.getName()).thenReturn("Target"); + InMemoryCatalog states = new InMemoryCatalog(); + states.state = PlayerTreeFellerState.initial(playerId, "Target") + .withProgress(TreeSpecies.POPLAR, 9); + TreeFellerAdminCommand handler = handler(serverWith(target), states); + CommandSender administrator = administrator(); + + handler.onCommand(administrator, mock(Command.class), "treefelleradmin", + new String[] {"player", "Target", "status"}); + verify(administrator).sendMessage(contains("Poplar: locked (9/100)")); + + handler.onCommand(administrator, mock(Command.class), "treefelleradmin", + new String[] {"player", "Target", "tree", "poplar", "grant"}); + assertTrue(states.state.isUnlocked(TreeSpecies.POPLAR)); + + handler.onCommand(administrator, mock(Command.class), "treefelleradmin", + new String[] {"player", "Target", "tree", "poplar", "grant", "extra"}); + assertEquals(1, states.saveCount); + } + @Test void autocompleteIsPermissionAwareAndPositionSpecific() throws Exception { Player target = mock(Player.class); @@ -113,11 +140,26 @@ class TreeFellerAdminCommandTest { administrator, mock(Command.class), "treefelleradmin", new String[] {"p"})); assertEquals(List.of("Target"), handler.onTabComplete( administrator, mock(Command.class), "treefelleradmin", new String[] {"player", "T"})); + assertEquals(List.of("poplar"), handler.onTabComplete( + administrator, + mock(Command.class), + "treefelleradmin", + new String[] {"threshold", "pop"})); + assertEquals(List.of("poplar"), handler.onTabComplete( + administrator, + mock(Command.class), + "treefelleradmin", + new String[] {"player", "Target", "tree", "pop"})); assertEquals(List.of("grant"), handler.onTabComplete( administrator, mock(Command.class), "treefelleradmin", - new String[] {"player", "Target", "tree", "oak", "g"})); + new String[] {"player", "Target", "tree", "poplar", "g"})); + assertEquals(List.of(), handler.onTabComplete( + administrator, + mock(Command.class), + "treefelleradmin", + new String[] {"player", "Target", "tree", "poplar", "grant", "extra"})); } private TreeFellerAdminCommand handler(Server server, InMemoryCatalog states) throws Exception { @@ -152,6 +194,7 @@ class TreeFellerAdminCommandTest { private static final class InMemoryCatalog implements PlayerStateCatalog { private PlayerTreeFellerState state; + private int saveCount; @Override public Optional load(UUID playerId) { @@ -168,6 +211,7 @@ class TreeFellerAdminCommandTest { @Override public void save(PlayerTreeFellerState changed) { state = changed; + saveCount++; } } } diff --git a/src/test/java/games/dmg/treefeller/TreeFellerCommandTest.java b/src/test/java/games/dmg/treefeller/TreeFellerCommandTest.java index 6d9d910..02289d5 100644 --- a/src/test/java/games/dmg/treefeller/TreeFellerCommandTest.java +++ b/src/test/java/games/dmg/treefeller/TreeFellerCommandTest.java @@ -73,6 +73,7 @@ class TreeFellerCommandTest { verify(player).sendMessage(contains("Oak: locked (12/100)")); verify(player).sendMessage(contains("Birch: unlocked")); + verify(player).sendMessage(contains("Poplar: locked (0/100)")); verify(player, atLeastOnce()).sendMessage(contains("Mushroom")); } diff --git a/src/test/java/games/dmg/treefeller/TreeFellerSettingsTest.java b/src/test/java/games/dmg/treefeller/TreeFellerSettingsTest.java index 3015522..7f7734a 100644 --- a/src/test/java/games/dmg/treefeller/TreeFellerSettingsTest.java +++ b/src/test/java/games/dmg/treefeller/TreeFellerSettingsTest.java @@ -24,6 +24,20 @@ class TreeFellerSettingsTest { assertEquals(6, settings.undoWindowMinutes()); } + @Test + void acceptsOnlyTheExistingThresholdRangeForPoplar() throws Exception { + TreeFellerSettings settings = TreeFellerSettings.load(loadDefaults()); + + assertEquals(1, settings.withThreshold(TreeSpecies.POPLAR, 1) + .threshold(TreeSpecies.POPLAR)); + assertEquals(1_000_000, settings.withThreshold(TreeSpecies.POPLAR, 1_000_000) + .threshold(TreeSpecies.POPLAR)); + assertThrows(IllegalArgumentException.class, + () -> settings.withThreshold(TreeSpecies.POPLAR, 0)); + assertThrows(IllegalArgumentException.class, + () -> settings.withThreshold(TreeSpecies.POPLAR, 1_000_001)); + } + @Test void rejectsUnsafeRequiredNumbers() throws Exception { YamlConfiguration configuration = loadDefaults(); diff --git a/src/test/java/games/dmg/treefeller/TreeStructureScannerTest.java b/src/test/java/games/dmg/treefeller/TreeStructureScannerTest.java index 72c80f8..4e26467 100644 --- a/src/test/java/games/dmg/treefeller/TreeStructureScannerTest.java +++ b/src/test/java/games/dmg/treefeller/TreeStructureScannerTest.java @@ -38,6 +38,27 @@ class TreeStructureScannerTest { .isEmpty()); } + @Test + void recognizesEveryPoplarFoliageColorAsTheSameSpecies() { + for (Material foliage : new Material[] { + Material.ORANGE_POPLAR_LEAVES, + Material.RED_POPLAR_LEAVES, + Material.YELLOW_POPLAR_LEAVES + }) { + TestBlocks blocks = new TestBlocks(); + blocks.put(0, 0, 0, Material.POPLAR_LOG); + blocks.put(0, 1, 0, Material.POPLAR_LOG); + blocks.put(1, 1, 0, foliage); + + TreeStructure tree = new TreeStructureScanner(100, 16) + .scan(blocks, new BlockPoint(0, 0, 0)) + .orElseThrow(); + + assertEquals(TreeSpecies.POPLAR, tree.species()); + assertEquals(2, tree.trunkBlocks().size()); + } + } + @Test void distinguishesGiantMushroomsByTheirCaps() { TestBlocks blocks = new TestBlocks(); diff --git a/src/test/java/games/dmg/treefeller/TreeTaxonomyTest.java b/src/test/java/games/dmg/treefeller/TreeTaxonomyTest.java index dc5fcca..b36bc8a 100644 --- a/src/test/java/games/dmg/treefeller/TreeTaxonomyTest.java +++ b/src/test/java/games/dmg/treefeller/TreeTaxonomyTest.java @@ -20,6 +20,7 @@ class TreeTaxonomyTest { Map.entry(Material.MANGROVE_LOG, TreeSpecies.MANGROVE), Map.entry(Material.CHERRY_LOG, TreeSpecies.CHERRY), Map.entry(Material.PALE_OAK_LOG, TreeSpecies.PALE_OAK), + Map.entry(Material.POPLAR_LOG, TreeSpecies.POPLAR), Map.entry(Material.CRIMSON_STEM, TreeSpecies.CRIMSON), Map.entry(Material.WARPED_STEM, TreeSpecies.WARPED)); @@ -29,5 +30,12 @@ class TreeTaxonomyTest { assertTrue(TreeTaxonomy.directSpecies(Material.OAK_WOOD).isEmpty()); assertTrue(TreeTaxonomy.directSpecies(Material.STRIPPED_OAK_LOG).isEmpty()); assertTrue(TreeTaxonomy.directSpecies(Material.CRIMSON_HYPHAE).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.POPLAR_WOOD).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.STRIPPED_POPLAR_LOG).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.STRIPPED_POPLAR_WOOD).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.POPLAR_PLANKS).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.POPLAR_SAPLING).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.FIREFLY_BUSH).isEmpty()); + assertTrue(TreeTaxonomy.directSpecies(Material.RED_SHRUB).isEmpty()); } } diff --git a/src/test/java/games/dmg/treefeller/YamlPlayerStateRepositoryTest.java b/src/test/java/games/dmg/treefeller/YamlPlayerStateRepositoryTest.java index 278de85..9ef1c0f 100644 --- a/src/test/java/games/dmg/treefeller/YamlPlayerStateRepositoryTest.java +++ b/src/test/java/games/dmg/treefeller/YamlPlayerStateRepositoryTest.java @@ -43,6 +43,37 @@ class YamlPlayerStateRepositoryTest { assertFalse(Files.exists(file.resolveSibling("players.yml.tmp"))); } + @Test + void loadsLegacyRecordsWithZeroLockedPoplarProgressAndPreservesExistingData() throws Exception { + Path file = temporaryDirectory.resolve("players.yml"); + UUID playerId = UUID.randomUUID(); + String root = "players." + playerId; + YamlConfiguration legacy = new YamlConfiguration(); + legacy.set(root + ".latest-name", "CurrentName"); + legacy.set(root + ".known-names", java.util.List.of("OldName", "CurrentName")); + legacy.set(root + ".enabled", false); + legacy.set(root + ".locked", true); + legacy.set(root + ".progress.oak", 73); + legacy.set(root + ".unlocked", java.util.List.of("birch")); + legacy.set(root + ".future-player", 42); + legacy.save(file.toFile()); + + YamlPlayerStateRepository repository = new YamlPlayerStateRepository(file); + PlayerTreeFellerState state = repository.load(playerId).orElseThrow(); + repository.save(state); + + assertEquals(0, state.progress(TreeSpecies.POPLAR)); + assertFalse(state.isUnlocked(TreeSpecies.POPLAR)); + assertEquals(73, state.progress(TreeSpecies.OAK)); + assertTrue(state.isUnlocked(TreeSpecies.BIRCH)); + assertFalse(state.enabled()); + assertTrue(state.locked()); + assertEquals(java.util.Set.of("OldName", "CurrentName"), state.knownNames()); + YamlConfiguration saved = YamlConfiguration.loadConfiguration(file.toFile()); + assertEquals(42, saved.getInt(root + ".future-player")); + assertEquals(0, saved.getLong(root + ".progress.poplar")); + } + @Test void rejectsARecordContainingUnknownUnlocksInsteadOfGrantingAccess() throws Exception { Path file = temporaryDirectory.resolve("players.yml");