Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/net/tfminecraft/birdmessenger/mail/MailDelivery.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ public MailDelivery(BirdConfig config) {
this.config = config;
}

/** Checks storage slots without adding items or counting armour and offhand slots. */
public static boolean hasRoom(Player owner, ItemStack letter) {
var inventory = owner.getInventory();
int remaining = letter.getAmount();
int stackLimit = Math.min(letter.getMaxStackSize(), inventory.getMaxStackSize());
for (ItemStack slot : inventory.getStorageContents()) {
if (slot == null || slot.getType().isAir()) {
remaining -= stackLimit;
} else if (letter.isSimilar(slot)) {
remaining -= Math.max(0, stackLimit - slot.getAmount());
}
if (remaining <= 0) {
return true;
}
}
return false;
}

public static boolean isActiveCharacter(Player owner, String characterId) {
if (owner == null || characterId == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public void resume() {
if (!store.inFlight().isEmpty()) {
plugin.getLogger().info("Resumed " + store.inFlight().size() + " in-flight letter(s).");
}
Bukkit.getScheduler().runTaskTimer(plugin, this::flushPendingForOnlinePlayers, 20L, 20L);
}

public void flushPendingForOnlinePlayers() {
Expand All @@ -134,7 +135,8 @@ public void tryDeliverPending(Player owner, CharacterSession session) {
if (!SendSession.isCurrent(owner, session)) {
return;
}
store.drainPending(session.ref(), () -> SendSession.isCurrent(owner, session), pending -> {
store.drainPending(session.ref(), () -> SendSession.isCurrent(owner, session),
pending -> MailDelivery.hasRoom(owner, pending.item), pending -> {
if (pending.returned) {
delivery.returnUnsent(owner, pending.item);
} else if (pending.recipientDied) {
Expand All @@ -145,6 +147,7 @@ public void tryDeliverPending(Player owner, CharacterSession session) {
});
// Letters returned to the account, whichever of its characters is played.
store.drainReturned(session.ref().ownerId(), () -> SendSession.isCurrent(owner, session),
pending -> MailDelivery.hasRoom(owner, pending.item),
pending -> delivery.returnToSender(owner, pending.item, false));
}

Expand All @@ -163,7 +166,8 @@ private void complete(StoredMail mail) {
return;
}
Player owner = Bukkit.getPlayer(mail.getOwnerUuid());
if (owner != null && owner.isOnline() && MailDelivery.isActiveCharacter(owner, mail.getCharacterId())) {
if (owner != null && owner.isOnline() && MailDelivery.isActiveCharacter(owner, mail.getCharacterId())
&& MailDelivery.hasRoom(owner, mail.getItem())) {
store.removeInFlight(mail.getId());
delivery.deliver(owner, mail);
DiscordNotifyService.notifyFlightComplete(plugin, mail);
Expand Down
23 changes: 21 additions & 2 deletions src/main/java/net/tfminecraft/birdmessenger/mail/MailStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;

import org.bukkit.configuration.ConfigurationSection;
Expand Down Expand Up @@ -59,13 +60,19 @@ public Map<UUID, StoredMail> inFlight() {
* goes back to the head of its queue if the hand-over throws.
*/
public void drainPending(CharacterRef ref, BooleanSupplier current, Consumer<PendingLetter> deliver) {
drainPending(ref, current, letter -> true, deliver);
}

/** Leaves the next letter queued when its complete stack cannot be accepted. */
public void drainPending(CharacterRef ref, BooleanSupplier current, Predicate<PendingLetter> accepts,
Consumer<PendingLetter> deliver) {
if (ref == null) {
return;
}
boolean taken = false;
try {
while (current.getAsBoolean()) {
Taken next = removePending(ref);
Taken next = removePending(ref, accepts);
if (next == null) {
return;
}
Expand All @@ -87,7 +94,7 @@ public void drainPending(CharacterRef ref, BooleanSupplier current, Consumer<Pen
private record Taken(String key, PendingLetter letter) {
}

private Taken removePending(CharacterRef ref) {
private Taken removePending(CharacterRef ref, Predicate<PendingLetter> accepts) {
String characterId = ref.characterId().toString();
for (Map.Entry<String, List<PendingLetter>> entry : pending.entrySet()) {
if (!entry.getKey().equalsIgnoreCase(characterId)) {
Expand All @@ -98,6 +105,9 @@ private Taken removePending(CharacterRef ref) {
if (!ref.ownerId().equals(letters.get(i).ownerUuid)) {
continue;
}
if (!accepts.test(letters.get(i))) {
return null;
}
requireRecovered(pendingFile);
PendingLetter letter = letters.remove(i);
if (letters.isEmpty()) {
Expand Down Expand Up @@ -306,6 +316,12 @@ private static <K> void restore(Map<K, List<PendingLetter>> queues, Map<K, List<
* once. A letter goes back to the head of the queue if the hand-over throws.
*/
public void drainReturned(UUID account, BooleanSupplier current, Consumer<PendingLetter> deliver) {
drainReturned(account, current, letter -> true, deliver);
}

/** Leaves the next returned letter queued until its complete stack can be accepted. */
public void drainReturned(UUID account, BooleanSupplier current, Predicate<PendingLetter> accepts,
Consumer<PendingLetter> deliver) {
if (account == null) {
return;
}
Expand All @@ -316,6 +332,9 @@ public void drainReturned(UUID account, BooleanSupplier current, Consumer<Pendin
if (letters == null) {
return;
}
if (!accepts.test(letters.getFirst())) {
return;
}
requireRecovered(returnedFile);
PendingLetter letter = letters.removeFirst();
if (letters.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,45 @@
import org.junit.jupiter.api.Test;

class DeliverySessionCoverageTest {
@Test
void deliveryRequiresRoomForTheCompleteStackWithoutChangingTheInventory() {
Player player = mock(Player.class);
PlayerInventory inventory = mock(PlayerInventory.class);
when(player.getInventory()).thenReturn(inventory);
when(inventory.getMaxStackSize()).thenReturn(64);
ItemStack letter = mock(ItemStack.class);
when(letter.getAmount()).thenReturn(2);
when(letter.getMaxStackSize()).thenReturn(1);
when(inventory.getStorageContents()).thenReturn(new ItemStack[1]);
assertFalse(MailDelivery.hasRoom(player, letter));
when(inventory.getStorageContents()).thenReturn(new ItemStack[2]);
assertTrue(MailDelivery.hasRoom(player, letter));

ItemStack partial = mock(ItemStack.class);
Material material = mock(Material.class);
when(partial.getType()).thenReturn(material);
when(partial.getAmount()).thenReturn(63);
when(letter.isSimilar(partial)).thenReturn(true);
when(letter.getMaxStackSize()).thenReturn(64);
when(letter.getAmount()).thenReturn(65);
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {partial, null});
assertTrue(MailDelivery.hasRoom(player, letter));
when(letter.getAmount()).thenReturn(66);
assertFalse(MailDelivery.hasRoom(player, letter));
when(letter.getAmount()).thenReturn(65);
when(letter.isSimilar(partial)).thenReturn(false);
assertFalse(MailDelivery.hasRoom(player, letter));
when(material.isAir()).thenReturn(true);
assertTrue(MailDelivery.hasRoom(player, letter));

when(inventory.getMaxStackSize()).thenReturn(16);
when(letter.getAmount()).thenReturn(17);
when(inventory.getStorageContents()).thenReturn(new ItemStack[1]);
assertFalse(MailDelivery.hasRoom(player, letter));
verify(inventory, never()).addItem(any(ItemStack.class));
verify(player, never()).getWorld();
}

@Test
void inventoryOverflowDropsOnlyReturnedLeftovers() {
Player player = mock(Player.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ void olderLettersGoToTheWritersAccountQueueAndArriveOnAnyCharacter() throws Exce
restored.drainReturned(UUID.randomUUID(), () -> true, handed::add);
restored.drainReturned(null, () -> true, handed::add);
restored.drainReturned(WRITER.ownerId(), () -> false, handed::add);
restored.drainReturned(WRITER.ownerId(), () -> true, letter -> false, handed::add);
assertEquals(List.of(), handed);
restored = reload();
restored.drainReturned(WRITER.ownerId(), () -> true, handed::add);
assertEquals(1, handed.size());
assertEquals(letters.getFirst().id, handed.getFirst().id);
Expand Down
Loading
Loading