Skip to content

Commit a272f5d

Browse files
committed
(fix) exp bottle not release exp storage correctly sometime on thrown
1 parent 39ec936 commit a272f5d

File tree

4 files changed

+98
-62
lines changed

4 files changed

+98
-62
lines changed

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@
8686
<version>2.20.1</version>
8787
<scope>provided</scope>
8888
</dependency>
89+
<dependency>
90+
<groupId>org.junit.jupiter</groupId>
91+
<artifactId>junit-jupiter-api</artifactId>
92+
<version>5.11.0</version>
93+
<scope>test</scope>
94+
</dependency>
8995
</dependencies>
9096

9197
<build>

src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java

+51-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
package cat.nyaa.ukit.utils;
22

33
import com.google.common.primitives.Ints;
4+
import org.bukkit.Location;
5+
import org.bukkit.entity.ExperienceOrb;
46
import org.bukkit.entity.Player;
7+
import org.bukkit.event.entity.CreatureSpawnEvent;
8+
import org.bukkit.util.Vector;
9+
10+
import java.util.List;
11+
import java.util.Random;
12+
513

614
public final class ExperienceUtils {
715
// From NyaaCore
816
// https://github.com/NyaaCat/NyaaCore/blob/0bc366debf51b0f4dcd867b657be19e14e772100/src/main/java/cat/nyaa/nyaacore/utils/ExperienceUtils.java
917

18+
// refer to https://minecraft.wiki/w/Experience
19+
private static final List<Integer> usableSplashExpList = List.of(1, 2, 6, 16, 36, 72, 148, 306, 616, 1236, 2476, 32767, 65535, 131071).reversed();
20+
private static final Random random = new Random();
21+
1022
/**
1123
* How much exp points at least needed to reach this level.
1224
* i.e. getLevel() = level &amp;&amp; getExp() == 0
@@ -43,14 +55,47 @@ public static void subtractExpPoints(Player p, int points) {
4355

4456
/**
4557
* Which level the player at if he/she has this mount of exp points
46-
* TODO optimization
58+
* refer <a href="https://minecraft.wiki/w/Experience">minecraft.wiki/w/Experience</a>
4759
*/
48-
public static int getLevelForExp(int exp) {
49-
if (exp < 0) throw new IllegalArgumentException();
50-
for (int lv = 1; lv < 21000; lv++) {
51-
if (getExpForLevel(lv) > exp) return lv - 1;
60+
public static int getLevelForExp(Integer total) {
61+
// 0-352
62+
// 353-1507
63+
// 1508+
64+
return (int) switch (total) {
65+
case Integer i when i < 353 -> Math.sqrt(total + 9.0) - 3.0;
66+
case Integer i when i < 1508 ->
67+
81.0 / 10.0 + Math.sqrt(2.0 / 5.0 * (total - 7839.0 / 40.0));
68+
default ->
69+
conditionalRounding(325.0 / 18.0 + Math.sqrt(2.0 / 9.0 * (total - 54215.0 / 72.0)));
70+
};
71+
}
72+
73+
public static double conditionalRounding(double value) {
74+
double threshold = 1e-8;
75+
long nearestInt = Math.round(value);
76+
double diff = Math.abs(value - nearestInt);
77+
return diff < threshold ? nearestInt : value;
78+
}
79+
80+
public static void splashExp(int amount, Location location) {
81+
while (amount > 0) {
82+
var nextOrbValue = firstMatchedExp(amount);
83+
var experienceOrb = location.getWorld().spawn(location, ExperienceOrb.class, CreatureSpawnEvent.SpawnReason.NATURAL);
84+
experienceOrb.setExperience(nextOrbValue);
85+
experienceOrb.setVelocity(randomVector().multiply(0.3));
86+
amount -= nextOrbValue;
87+
}
88+
}
89+
90+
private static Vector randomVector() {
91+
return new Vector(random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1);
92+
}
93+
94+
private static int firstMatchedExp(int remaining) {
95+
for (int i : usableSplashExpList) {
96+
if (i <= remaining) return i;
5297
}
53-
throw new IllegalArgumentException("exp too large");
98+
throw new IllegalStateException("shouldn't be here");
5499
}
55100

56101
/**

src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java

+10-56
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,23 @@
1212
import org.bukkit.NamespacedKey;
1313
import org.bukkit.command.Command;
1414
import org.bukkit.command.CommandSender;
15-
import org.bukkit.entity.Entity;
16-
import org.bukkit.entity.EntityType;
1715
import org.bukkit.entity.Player;
16+
import org.bukkit.entity.ThrownExpBottle;
1817
import org.bukkit.event.EventHandler;
19-
import org.bukkit.event.EventPriority;
2018
import org.bukkit.event.Listener;
21-
import org.bukkit.event.block.Action;
22-
import org.bukkit.event.entity.ExpBottleEvent;
23-
import org.bukkit.event.entity.ProjectileLaunchEvent;
24-
import org.bukkit.event.player.PlayerInteractEvent;
19+
import org.bukkit.event.entity.ProjectileHitEvent;
2520
import org.bukkit.inventory.ItemStack;
2621
import org.bukkit.inventory.meta.ItemMeta;
2722
import org.bukkit.persistence.PersistentDataType;
2823

29-
import java.util.*;
24+
import java.util.ArrayList;
25+
import java.util.List;
3026

3127
public class XpStoreFunction implements SubCommandExecutor, SubTabCompleter, Listener {
3228
private final SpigotLoader pluginInstance;
3329
private final NamespacedKey EXPAmountKey;
3430
private final NamespacedKey LoreLineIndexKey;
3531
private final String EXPBOTTLE_PERMISSION_NODE = "ukit.xpstore";
36-
private final Map<UUID, Integer> playerExpBottleMap = new HashMap<>();
3732
private final List<String> subCommands = List.of("store", "take");
3833

3934
public XpStoreFunction(SpigotLoader pluginInstance) {
@@ -166,10 +161,6 @@ private boolean isExpContainer(ItemStack itemStack) {
166161
return itemStack.getItemMeta().getPersistentDataContainer().has(EXPAmountKey, PersistentDataType.INTEGER);
167162
}
168163

169-
private boolean isExpContainer(Entity entity) {
170-
return entity.getPersistentDataContainer().has(EXPAmountKey, PersistentDataType.INTEGER);
171-
}
172-
173164
private int getExpContained(ItemStack itemStack) {
174165
if (!isExpContainer(itemStack)) {
175166
return 0;
@@ -178,13 +169,6 @@ private int getExpContained(ItemStack itemStack) {
178169
}
179170
}
180171

181-
private int getExpContained(Entity entity) {
182-
if (!isExpContainer(entity))
183-
return 0;
184-
else
185-
return entity.getPersistentDataContainer().get(EXPAmountKey, PersistentDataType.INTEGER);
186-
}
187-
188172
private ItemStack addExpToItemStack(ItemStack itemStack, int amount) {
189173
var itemMeta = itemStack.hasItemMeta() ? itemStack.getItemMeta() : Bukkit.getItemFactory().getItemMeta(itemStack.getType());
190174
assert itemMeta != null;
@@ -198,16 +182,6 @@ private ItemStack addExpToItemStack(ItemStack itemStack, int amount) {
198182
return itemStack;
199183
}
200184

201-
private void addExpToEntity(Entity entity, int amount) {
202-
if (!isExpContainer(entity)) {
203-
entity.getPersistentDataContainer().set(EXPAmountKey, PersistentDataType.INTEGER, amount);
204-
} else {
205-
entity.getPersistentDataContainer().set(EXPAmountKey, PersistentDataType.INTEGER,
206-
entity.getPersistentDataContainer().get(EXPAmountKey, PersistentDataType.INTEGER) + amount
207-
);
208-
}
209-
}
210-
211185
private ItemMeta updateLore(ItemMeta itemMeta) {
212186
var loreIndex = itemMeta.getPersistentDataContainer().get(LoreLineIndexKey, PersistentDataType.INTEGER);
213187
var amount = itemMeta.getPersistentDataContainer().get(EXPAmountKey, PersistentDataType.INTEGER);
@@ -236,33 +210,13 @@ private ItemMeta updateLore(ItemMeta itemMeta) {
236210
return itemMeta;
237211
}
238212

239-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
240-
public void onPlayerInteractWithExpBottle(PlayerInteractEvent event) {
241-
if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.RIGHT_CLICK_AIR) {
242-
return;
243-
}
244-
if (event.getItem() == null)
245-
return;
246-
playerExpBottleMap.put(event.getPlayer().getUniqueId(), getExpContained(event.getItem()));
247-
}
248-
249-
@EventHandler(ignoreCancelled = true)
250-
public void onThrewExpBottleLaunch(ProjectileLaunchEvent event) {
251-
if (!(event.getEntity().getShooter() instanceof Player shooterPlayer))
252-
return;
253-
if (event.getEntity().getType() != EntityType.EXPERIENCE_BOTTLE)
254-
return;
255-
if (!playerExpBottleMap.containsKey(shooterPlayer.getUniqueId()))
256-
return;
257-
var amount = playerExpBottleMap.remove(shooterPlayer.getUniqueId());
258-
addExpToEntity(event.getEntity(), amount);
259-
}
260-
261213
@EventHandler(ignoreCancelled = true)
262-
public void onExpBottleHitGround(ExpBottleEvent event) {
263-
if (!(event.getEntity().getShooter() instanceof Player))
214+
public void onExpBottleHit(ProjectileHitEvent event) {
215+
if (!(event.getEntity() instanceof ThrownExpBottle thrownExpBottle))
264216
return;
265-
var amount = getExpContained(event.getEntity());
266-
event.setExperience(event.getExperience() + amount);
217+
var item = thrownExpBottle.getItem();
218+
if (!isExpContainer(item)) return;
219+
var expAmount = getExpContained(item);
220+
ExperienceUtils.splashExp(expAmount, thrownExpBottle.getLocation());
267221
}
268222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cat.nyaa.ukit;
2+
3+
import cat.nyaa.ukit.utils.ExperienceUtils;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.List;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
public class ExpUtilTest {
11+
@Test
12+
public void testExpCalculationMatch() {
13+
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);
14+
// will break on lvl 21863 + 75705 offset with the total reach Integer.MAX_VALUE
15+
for (int lvl : levelsForTest) {
16+
var total = ExperienceUtils.getExpForLevel(lvl);
17+
for (int offset = 0; offset < maxExpOffset(lvl); offset++) {
18+
var result = ExperienceUtils.getLevelForExp(total + offset);
19+
assertEquals(lvl, result);
20+
}
21+
}
22+
}
23+
24+
private int maxExpOffset(Integer level) {
25+
return switch (level) {
26+
case Integer i when i < 16 -> 2 * level + 7;
27+
case Integer i when i < 31 -> 5 * level - 38;
28+
default -> 9 * level - 158;
29+
};
30+
}
31+
}

0 commit comments

Comments
 (0)