diff --git a/.gitignore b/.gitignore index 5e2fb46..46abb5a 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,10 @@ release.properties dependency-reduced-pom.xml buildNumber.properties + # Test artifacts +database/ +database_backup/ + # Intellij *.iml *.java___jb_tmp___ diff --git a/src/main/java/com/wasteofplastic/invswitcher/Store.java b/src/main/java/com/wasteofplastic/invswitcher/Store.java index 5ef1022..9454697 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/Store.java +++ b/src/main/java/com/wasteofplastic/invswitcher/Store.java @@ -320,7 +320,15 @@ private void setFood(InventoryStorage store, Player player, String overworldName private void setAdvancements(InventoryStorage store, Player player, String overworldName) { // Advancements - store.getAdvancements(overworldName).forEach((k, v) -> { + Map> advancements = store.getAdvancements(overworldName); + if (advancements.isEmpty()) { + return; + } + // Save current experience before granting advancements, because some advancements + // reward XP when their criteria are awarded, which would incorrectly increase the + // player's experience points. + int savedExp = getTotalExperience(player); + advancements.forEach((k, v) -> { Iterator it = Bukkit.advancementIterator(); while (it.hasNext()) { Advancement a = it.next(); @@ -330,7 +338,8 @@ private void setAdvancements(InventoryStorage store, Player player, String overw } } }); - + // Restore experience to prevent advancement rewards from modifying it + setTotalExperience(player, savedExp); } public void removeFromCache(Player player) { diff --git a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java index dfe5419..9cdb000 100644 --- a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java +++ b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java @@ -22,6 +22,7 @@ import java.nio.file.Path; import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -30,8 +31,11 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.World.Environment; +import org.bukkit.advancement.Advancement; +import org.bukkit.advancement.AdvancementProgress; import org.bukkit.attribute.AttributeInstance; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; @@ -200,6 +204,46 @@ public void testGetInventory() { verify(player).setTotalExperience(0); } + /** + * Test that advancement grants during {@link Store#getInventory} do not modify the player's + * experience points. Some advancements reward XP when their criteria are awarded; the store + * must save and restore XP around the advancement grant step. + */ + @Test + public void testGetInventoryAdvancementsPreservesExperience() { + sets.setAdvancements(true); + sets.setExperience(true); + sets.setStatistics(false); + sets.setIslandsActive(false); + + // Mock an advancement with awarded criteria + Advancement advancement = mock(Advancement.class); + NamespacedKey advKey = NamespacedKey.minecraft("story_mine_stone"); + when(advancement.getKey()).thenReturn(advKey); + + AdvancementProgress progress = mock(AdvancementProgress.class); + Set criteria = new HashSet<>(Set.of("mine_stone")); + when(progress.getAwardedCriteria()).thenReturn(criteria); + when(player.getAdvancementProgress(advancement)).thenReturn(progress); + + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + // Return a fresh iterator each time so both storeInventory and getInventory can iterate + mockedBukkit.when(Bukkit::advancementIterator).thenAnswer(inv -> List.of(advancement).iterator()); + + // Store inventory (saves advancement data and clears player including XP reset) + s.storeInventory(player, world); + + // Load inventory — experience set, advancements granted, XP restored + s.getInventory(player, world); + } + + // setTotalExperience should be called exactly 3 times: + // 1. clearPlayer during storeInventory (XP reset to 0) + // 2. experience loading during getInventory (XP set to stored value 0) + // 3. XP restoration inside setAdvancements after granting advancement criteria + verify(player, times(3)).setTotalExperience(0); + } + /** * Test method for {@link com.wasteofplastic.invswitcher.Store#removeFromCache(org.bukkit.entity.Player)}. */