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
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public void onPlayerDeath(PlayerDeathEvent event) {
ItemStack offhand = cloneItem(inventory.getItemInOffHand());
Map<Integer, ItemStack> stash = new HashMap<>();
offhand = stripKept(storage, armor, offhand, stash);
ItemStack[] offhandSlot = new ItemStack[] { offhand };
ItemStack bundledTicket = null;
if (GraveLoader.isInsuranceEnabled() && GraveInsuranceTickets.bindableSlot(stash) == null) {
bundledTicket = GraveInsuranceExtract.pull(storage, armor, offhandSlot, stash.values());
offhand = offhandSlot[0];
}
if (!hasStoreableItems(storage, armor, offhand) && event.getDroppedExp() <= 0) {
return;
}
Expand All @@ -89,6 +95,12 @@ public void onPlayerDeath(PlayerDeathEvent event) {
}

GraveInsuranceTickets.Binding binding = GraveInsuranceTickets.bind(stash, grave);
if (!binding.bound() && bundledTicket != null) {
ItemStack bound = GraveInsuranceTickets.bindTicket(bundledTicket, grave);
if (bound != null) {
binding = new GraveInsuranceTickets.Binding(true, bound);
}
}
if (binding.split() != null) {
splitTicket.put(victim.getUniqueId(), binding.split());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package net.tfminecraft.rpcharacters.grave;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BundleMeta;
import org.bukkit.inventory.meta.ItemMeta;

/**
* Pulls one insurance ticket out of a death snapshot, including tickets stored in a bundle.
* The bundle stays where it was, with that one ticket removed.
*/
final class GraveInsuranceExtract {

private static final int MAX_DEPTH = 16;

private GraveInsuranceExtract() {}

/**
* Searches storage, armor, offhand, then any extra stacks (kept items). The first
* ticket that is not bound to a grave still in the world is removed and returned
* with amount 1. {@code offhand} is a one-slot array so the caller sees a cleared slot.
*/
static ItemStack pull(ItemStack[] storage, ItemStack[] armor, ItemStack[] offhand,
Iterable<ItemStack> extra) {
if (!GraveLoader.isInsuranceEnabled()) {
return null;
}
Items items = new Items();
ItemStack taken = pullSlots(storage, items);
if (taken != null) {
return taken;
}
taken = pullSlots(armor, items);
if (taken != null) {
return taken;
}
taken = pullSlots(offhand, items);
if (taken != null) {
return taken;
}
if (extra == null) {
return null;
}
for (ItemStack stack : extra) {
taken = pullOne(stack, items, ignored -> { });
if (taken != null) {
return taken;
}
}
return null;
}

static <T> T pullSlots(T[] slots, Stacks<T> stacks) {
if (slots == null) {
return null;
}
for (int i = 0; i < slots.length; i++) {
int slot = i;
T taken = pullOne(slots[i], stacks, remainder -> slots[slot] = remainder);
if (taken != null) {
return taken;
}
}
return null;
}

private static <T> T pullOne(T item, Stacks<T> stacks, Consumer<T> replace) {
Take<T> taken = take(item, stacks, 0);
if (taken == null) {
return null;
}
replace.accept(taken.remainder);
return taken.ticket;
}

private static <T> Take<T> take(T item, Stacks<T> stacks, int depth) {
if (item == null || stacks.blank(item) || depth > MAX_DEPTH) {
return null;
}
if (stacks.insurance(item)) {
if (stacks.keepBinding(item)) {
return null;
}
T ticket = stacks.one(item);
if (stacks.amount(item) <= 1) {
return new Take<>(ticket, null);
}
stacks.setAmount(item, stacks.amount(item) - 1);
return new Take<>(ticket, item);
}
if (!stacks.bundle(item)) {
return null;
}
List<T> contents = new ArrayList<>(stacks.contents(item));
for (int i = 0; i < contents.size(); i++) {
Take<T> inner = take(contents.get(i), stacks, depth + 1);
if (inner == null) {
continue;
}
if (inner.remainder == null) {
contents.remove(i);
} else {
contents.set(i, inner.remainder);
}
stacks.setContents(item, contents);
return new Take<>(inner.ticket, item);
}
return null;
}

private record Take<T>(T ticket, T remainder) {}

interface Stacks<T> {
boolean blank(T item);

int amount(T item);

void setAmount(T item, int amount);

boolean insurance(T item);

/** True when this ticket is already bound to a grave that still exists. */
boolean keepBinding(T item);

boolean bundle(T item);

List<T> contents(T bundle);

void setContents(T bundle, List<T> contents);

T one(T item);
}

private static final class Items implements Stacks<ItemStack> {
@Override
public boolean blank(ItemStack item) {
return Grave.isBlank(item);
}

@Override
public int amount(ItemStack item) {
return item.getAmount();
}

@Override
public void setAmount(ItemStack item, int amount) {
item.setAmount(amount);
}

@Override
public boolean insurance(ItemStack item) {
return GraveLoader.isInsuranceItem(item);
}

@Override
public boolean keepBinding(ItemStack item) {
return GraveInsuranceTickets.boundToLiveGrave(item);
}

@Override
public boolean bundle(ItemStack item) {
return item.getItemMeta() instanceof BundleMeta;
}

@Override
public List<ItemStack> contents(ItemStack bundle) {
ItemMeta meta = bundle.getItemMeta();
if (!(meta instanceof BundleMeta bundleMeta) || !bundleMeta.hasItems()) {
return List.of();
}
return bundleMeta.getItems();
}

@Override
public void setContents(ItemStack bundle, List<ItemStack> contents) {
ItemMeta meta = bundle.getItemMeta();
if (!(meta instanceof BundleMeta bundleMeta)) {
return;
}
List<ItemStack> kept = new ArrayList<>();
if (contents != null) {
for (ItemStack inner : contents) {
if (!Grave.isBlank(inner)) {
kept.add(inner);
}
}
}
bundleMeta.setItems(kept.isEmpty() ? null : kept);
bundle.setItemMeta(bundleMeta);
}

@Override
public ItemStack one(ItemStack item) {
ItemStack single = item.clone();
single.setAmount(1);
return single;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ record Binding(boolean bound, ItemStack split) {

private GraveInsuranceTickets() {}

/** The stash slot that {@link #bind} would use, or null when no loose ticket can be bound. */
static Integer bindableSlot(Map<Integer, ItemStack> stash) {
if (stash == null || stash.isEmpty()) {
return null;
}
List<Integer> slots = new ArrayList<>(stash.keySet());
slots.sort(null);
return pickSlot(slots, slot -> GraveLoader.isInsuranceItem(stash.get(slot)),
slot -> boundToLiveGrave(stash.get(slot)));
}

/** Tags one already-separated ticket and returns the bound copy. */
static ItemStack bindTicket(ItemStack ticket, Grave grave) {
if (Grave.isBlank(ticket) || grave == null) {
return null;
}
ItemStack single = ticket.clone();
single.setAmount(1);
return tag(single, grave);
}

/**
* Binds one ticket from the death stash to {@code grave}. A stacked ticket is split:
* the rest of the stack stays in its slot and the bound ticket is returned in
Expand All @@ -38,10 +59,7 @@ static Binding bind(Map<Integer, ItemStack> stash, Grave grave) {
if (stash == null || stash.isEmpty() || grave == null) {
return Binding.NONE;
}
List<Integer> slots = new ArrayList<>(stash.keySet());
slots.sort(null);
Integer slot = pickSlot(slots, s -> GraveLoader.isInsuranceItem(stash.get(s)),
s -> isBoundToLiveGrave(stash.get(s)));
Integer slot = bindableSlot(stash);
if (slot == null) {
return Binding.NONE;
}
Expand Down Expand Up @@ -105,7 +123,7 @@ static void unbind(ItemStack item) {
item.setItemMeta(meta);
}

private static boolean isBoundToLiveGrave(ItemStack item) {
static boolean boundToLiveGrave(ItemStack item) {
UUID id = boundGrave(item);
return id != null && GraveManager.get().getById(id) != null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/graves.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ excluded-items:
# Carry a ticket when you die and one ticket is bound to that grave. Right-click the
# bound ticket to recover that grave from anywhere, even in an unloaded chunk
# (consumes the ticket on success). Unbound tickets, or tickets bound to another grave,
# do nothing. Tickets never go into a grave.
# do nothing. Loose tickets never go into a grave. A ticket inside a bundle still
# counts: one is taken out, bound, and returned on respawn. The bundle goes in the grave.
insurance:
item: m.miscellanea.grave_insurance
consume: true
Expand Down
Loading
Loading