From a272f5de91dc415fea94aad119cc714ec99271b2 Mon Sep 17 00:00:00 2001 From: langua Date: Mon, 30 Sep 2024 22:34:13 +0200 Subject: [PATCH] (fix) exp bottle not release exp storage correctly sometime on thrown --- pom.xml | 6 ++ .../cat/nyaa/ukit/utils/ExperienceUtils.java | 57 ++++++++++++++-- .../nyaa/ukit/xpstore/XpStoreFunction.java | 66 +++---------------- src/test/java/cat/nyaa/ukit/ExpUtilTest.java | 31 +++++++++ 4 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 src/test/java/cat/nyaa/ukit/ExpUtilTest.java diff --git a/pom.xml b/pom.xml index 8732e28..aad9ba8 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,12 @@ 2.20.1 provided + + org.junit.jupiter + junit-jupiter-api + 5.11.0 + test + diff --git a/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java b/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java index e907434..f0d7f30 100644 --- a/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java +++ b/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java @@ -1,12 +1,24 @@ package cat.nyaa.ukit.utils; import com.google.common.primitives.Ints; +import org.bukkit.Location; +import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.Player; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.util.Vector; + +import java.util.List; +import java.util.Random; + public final class ExperienceUtils { // From NyaaCore // https://github.com/NyaaCat/NyaaCore/blob/0bc366debf51b0f4dcd867b657be19e14e772100/src/main/java/cat/nyaa/nyaacore/utils/ExperienceUtils.java + // refer to https://minecraft.wiki/w/Experience + private static final List usableSplashExpList = List.of(1, 2, 6, 16, 36, 72, 148, 306, 616, 1236, 2476, 32767, 65535, 131071).reversed(); + private static final Random random = new Random(); + /** * How much exp points at least needed to reach this level. * i.e. getLevel() = level && getExp() == 0 @@ -43,14 +55,47 @@ public static void subtractExpPoints(Player p, int points) { /** * Which level the player at if he/she has this mount of exp points - * TODO optimization + * refer minecraft.wiki/w/Experience */ - public static int getLevelForExp(int exp) { - if (exp < 0) throw new IllegalArgumentException(); - for (int lv = 1; lv < 21000; lv++) { - if (getExpForLevel(lv) > exp) return lv - 1; + public static int getLevelForExp(Integer total) { + // 0-352 + // 353-1507 + // 1508+ + return (int) switch (total) { + case Integer i when i < 353 -> Math.sqrt(total + 9.0) - 3.0; + case Integer i when i < 1508 -> + 81.0 / 10.0 + Math.sqrt(2.0 / 5.0 * (total - 7839.0 / 40.0)); + default -> + conditionalRounding(325.0 / 18.0 + Math.sqrt(2.0 / 9.0 * (total - 54215.0 / 72.0))); + }; + } + + public static double conditionalRounding(double value) { + double threshold = 1e-8; + long nearestInt = Math.round(value); + double diff = Math.abs(value - nearestInt); + return diff < threshold ? nearestInt : value; + } + + public static void splashExp(int amount, Location location) { + while (amount > 0) { + var nextOrbValue = firstMatchedExp(amount); + var experienceOrb = location.getWorld().spawn(location, ExperienceOrb.class, CreatureSpawnEvent.SpawnReason.NATURAL); + experienceOrb.setExperience(nextOrbValue); + experienceOrb.setVelocity(randomVector().multiply(0.3)); + amount -= nextOrbValue; + } + } + + private static Vector randomVector() { + return new Vector(random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1); + } + + private static int firstMatchedExp(int remaining) { + for (int i : usableSplashExpList) { + if (i <= remaining) return i; } - throw new IllegalArgumentException("exp too large"); + throw new IllegalStateException("shouldn't be here"); } /** diff --git a/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java b/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java index 85eb7f5..6d29df6 100644 --- a/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java +++ b/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java @@ -12,28 +12,23 @@ import org.bukkit.NamespacedKey; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; +import org.bukkit.entity.ThrownExpBottle; import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.entity.ExpBottleEvent; -import org.bukkit.event.entity.ProjectileLaunchEvent; -import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.entity.ProjectileHitEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; -import java.util.*; +import java.util.ArrayList; +import java.util.List; public class XpStoreFunction implements SubCommandExecutor, SubTabCompleter, Listener { private final SpigotLoader pluginInstance; private final NamespacedKey EXPAmountKey; private final NamespacedKey LoreLineIndexKey; private final String EXPBOTTLE_PERMISSION_NODE = "ukit.xpstore"; - private final Map playerExpBottleMap = new HashMap<>(); private final List subCommands = List.of("store", "take"); public XpStoreFunction(SpigotLoader pluginInstance) { @@ -166,10 +161,6 @@ private boolean isExpContainer(ItemStack itemStack) { return itemStack.getItemMeta().getPersistentDataContainer().has(EXPAmountKey, PersistentDataType.INTEGER); } - private boolean isExpContainer(Entity entity) { - return entity.getPersistentDataContainer().has(EXPAmountKey, PersistentDataType.INTEGER); - } - private int getExpContained(ItemStack itemStack) { if (!isExpContainer(itemStack)) { return 0; @@ -178,13 +169,6 @@ private int getExpContained(ItemStack itemStack) { } } - private int getExpContained(Entity entity) { - if (!isExpContainer(entity)) - return 0; - else - return entity.getPersistentDataContainer().get(EXPAmountKey, PersistentDataType.INTEGER); - } - private ItemStack addExpToItemStack(ItemStack itemStack, int amount) { var itemMeta = itemStack.hasItemMeta() ? itemStack.getItemMeta() : Bukkit.getItemFactory().getItemMeta(itemStack.getType()); assert itemMeta != null; @@ -198,16 +182,6 @@ private ItemStack addExpToItemStack(ItemStack itemStack, int amount) { return itemStack; } - private void addExpToEntity(Entity entity, int amount) { - if (!isExpContainer(entity)) { - entity.getPersistentDataContainer().set(EXPAmountKey, PersistentDataType.INTEGER, amount); - } else { - entity.getPersistentDataContainer().set(EXPAmountKey, PersistentDataType.INTEGER, - entity.getPersistentDataContainer().get(EXPAmountKey, PersistentDataType.INTEGER) + amount - ); - } - } - private ItemMeta updateLore(ItemMeta itemMeta) { var loreIndex = itemMeta.getPersistentDataContainer().get(LoreLineIndexKey, PersistentDataType.INTEGER); var amount = itemMeta.getPersistentDataContainer().get(EXPAmountKey, PersistentDataType.INTEGER); @@ -236,33 +210,13 @@ private ItemMeta updateLore(ItemMeta itemMeta) { return itemMeta; } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onPlayerInteractWithExpBottle(PlayerInteractEvent event) { - if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.RIGHT_CLICK_AIR) { - return; - } - if (event.getItem() == null) - return; - playerExpBottleMap.put(event.getPlayer().getUniqueId(), getExpContained(event.getItem())); - } - - @EventHandler(ignoreCancelled = true) - public void onThrewExpBottleLaunch(ProjectileLaunchEvent event) { - if (!(event.getEntity().getShooter() instanceof Player shooterPlayer)) - return; - if (event.getEntity().getType() != EntityType.EXPERIENCE_BOTTLE) - return; - if (!playerExpBottleMap.containsKey(shooterPlayer.getUniqueId())) - return; - var amount = playerExpBottleMap.remove(shooterPlayer.getUniqueId()); - addExpToEntity(event.getEntity(), amount); - } - @EventHandler(ignoreCancelled = true) - public void onExpBottleHitGround(ExpBottleEvent event) { - if (!(event.getEntity().getShooter() instanceof Player)) + public void onExpBottleHit(ProjectileHitEvent event) { + if (!(event.getEntity() instanceof ThrownExpBottle thrownExpBottle)) return; - var amount = getExpContained(event.getEntity()); - event.setExperience(event.getExperience() + amount); + var item = thrownExpBottle.getItem(); + if (!isExpContainer(item)) return; + var expAmount = getExpContained(item); + ExperienceUtils.splashExp(expAmount, thrownExpBottle.getLocation()); } } diff --git a/src/test/java/cat/nyaa/ukit/ExpUtilTest.java b/src/test/java/cat/nyaa/ukit/ExpUtilTest.java new file mode 100644 index 0000000..017b891 --- /dev/null +++ b/src/test/java/cat/nyaa/ukit/ExpUtilTest.java @@ -0,0 +1,31 @@ +package cat.nyaa.ukit; + +import cat.nyaa.ukit.utils.ExperienceUtils; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ExpUtilTest { + @Test + public void testExpCalculationMatch() { + var levelsForTest = List.of(1, 2, 3, 5, 7, 9, 15, 16, 25, 30, 31, 40, 50, 70, 90, 120, 150, 200, 300, 500, 700, 1000, 2000, 4000, 8000, 1000, 12000, 15000, 18000, 21000); + // will break on lvl 21863 + 75705 offset with the total reach Integer.MAX_VALUE + for (int lvl : levelsForTest) { + var total = ExperienceUtils.getExpForLevel(lvl); + for (int offset = 0; offset < maxExpOffset(lvl); offset++) { + var result = ExperienceUtils.getLevelForExp(total + offset); + assertEquals(lvl, result); + } + } + } + + private int maxExpOffset(Integer level) { + return switch (level) { + case Integer i when i < 16 -> 2 * level + 7; + case Integer i when i < 31 -> 5 * level - 38; + default -> 9 * level - 158; + }; + } +}