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 @@ -194,6 +194,7 @@ public void bindItemMatcher(ItemMatcher matcher) {
// Callers (enable, reload, the pack-plugin hook) run only after onEnable built every item.
ItemMatcher bound = matcher == null ? ItemMatcher.vanillaOnly() : matcher;
trackerItem.setMatcher(bound);
tracker.itemChanged();
prospectItem.setMatcher(bound);
establishItem.setMatcher(bound);
brushItem.setMatcher(bound);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ private void tick() {
continue;
}
if (!net.tfminecraft.archaeo.ownership.CharacterActors.current(cycle.actor, player)) { finish(player); continue; }
ExcavationTool held = heldTool(player, player.getInventory().getItemInMainHand(), false);
ExcavationTool held = heldTool(state(player), player.getInventory().getItemInMainHand(), false);
if (held == null || !held.id().equals(cycle.tool.id())) {
finish(player);
continue;
Expand Down Expand Up @@ -423,29 +423,31 @@ private void tick() {
* Per-tick {@link #syncHeldTool} plus the HUD, with one tool match and at most one crosshair
* trace. The HUD shows while cycling, or while the crosshair is inside an established prism
* (opened air included, so neighbour traces can sit on the empty cube like a minesweeper
* number). The mining lock covers cycling and aiming at prism fill.
* number). The mining lock covers cycling and aiming at prism fill. The player's shortcuts
* are looked up once here and handed to each step.
*
* @param player online player
*/
private void syncAndShowHud(Player player) {
HeldState state = state(player);
ItemStack held = player.getInventory().getItemInMainHand();
if (heldTool(player, held, true) == null) {
keepVanillaMineUnlocked(player);
if (heldTool(state, held, true) == null) {
keepVanillaMineUnlocked(player, state);
return;
}
Cycle cycle = cycles.get(player.getUniqueId());
if (cycle != null) {
setVanillaMineLocked(player, true);
setVanillaMineLocked(player, state, true);
Block cell = player.getWorld().getBlockAt(cycle.x, cycle.y, cycle.z);
showHud(player, cycle, cell, establishedAt(cell));
return;
}
Block target = aimedNearSite(player);
Site site = target == null ? null : establishedAt(target);
if (site != null && PrismFill.isTerrainFill(target.getType())) {
setVanillaMineLocked(player, true);
setVanillaMineLocked(player, state, true);
} else {
keepVanillaMineUnlocked(player);
keepVanillaMineUnlocked(player, state);
}
if (site != null) {
showHud(player, null, target, site);
Expand All @@ -458,15 +460,14 @@ private void syncAndShowHud(Player player) {
* {@link #RECHECK_TICKS}. Matching asks every whitelist entry and the pack plugins, which is
* too slow to repeat for every online player on every tick.
*
* @param player holder
* @param state the holder's shortcuts
* @param stack current main-hand stack
* @param restore whether to strip a legacy mine lock from a whitelisted stack, as
* {@link DigTools#restoreAllowedTool} does; skipped while the stack is the one
* already restored, since a restored stack has nothing left to strip
* @return matching profile, or {@code null} when the stack is not an excavation tool
*/
private ExcavationTool heldTool(Player player, ItemStack stack, boolean restore) {
HeldState state = state(player);
private ExcavationTool heldTool(HeldState state, ItemStack stack, boolean restore) {
boolean same = stack != null
&& state.stack != null
&& gameTick - state.matchedTick < RECHECK_TICKS
Expand Down Expand Up @@ -496,13 +497,13 @@ private ExcavationTool heldTool(Player player, ItemStack stack, boolean restore)
* clears that confirmation when it does.
*
* @param player holder
* @param state the holder's shortcuts
*/
private void keepVanillaMineUnlocked(Player player) {
HeldState state = state(player);
private void keepVanillaMineUnlocked(Player player, HeldState state) {
if (state.unlocked && gameTick - state.unlockedTick < RECHECK_TICKS) {
return;
}
setVanillaMineLocked(player, false);
setVanillaMineLocked(player, state, false);
}

/**
Expand Down Expand Up @@ -850,6 +851,18 @@ private void playClang(Player player, Block block) {
* @param locked whether vanilla destroy progress must stay at zero on the client
*/
private void setVanillaMineLocked(Player player, boolean locked) {
setVanillaMineLocked(player, state(player), locked);
}

/**
* {@link #setVanillaMineLocked(Player, boolean)} for a caller that already holds the
* player's shortcuts, so they are not looked up again.
*
* @param player holder
* @param state the holder's shortcuts
* @param locked whether vanilla destroy progress must stay at zero on the client
*/
private void setVanillaMineLocked(Player player, HeldState state, boolean locked) {
AttributeInstance speed = player.getAttribute(Attribute.BLOCK_BREAK_SPEED);
if (speed == null) {
return;
Expand All @@ -861,7 +874,6 @@ private void setVanillaMineLocked(Player player, boolean locked) {
break;
}
}
HeldState state = state(player);
state.unlocked = !locked;
state.unlockedTick = gameTick;
if (locked && found == null) {
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/net/tfminecraft/archaeo/item/ItemMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,12 @@ private boolean matchesItemsAdder(Probe probe, ItemRef ref) {
*/
private boolean matchesMmoItems(Probe probe, ItemRef ref) {
String type = probe.mmoType();
String id = probe.mmoId();
if (type == null || id == null) {
if (type == null || !type.equalsIgnoreCase(ref.primary())) {
return false;
}
return type.equalsIgnoreCase(ref.primary()) && id.equalsIgnoreCase(ref.secondary());
// The id is a second read of the item's data, so it is asked only once the type fits.
String id = probe.mmoId();
return id != null && id.equalsIgnoreCase(ref.secondary());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,20 @@ public boolean wear(Player player, ItemStack pencil) {
/**
* Restores Archaeo how-to lore on a vanilla kit copy this plugin created. Unmarked paper,
* unmarked feathers, and pack items are left untouched so their own tooltip stays.
* A sheet or pencil configured as a pack item never gets lore, so its stacks are not
* matched at all: that match reads the item's data through the pack plugin.
*
* @param stack possible kit item
* @return whether lore was written
*/
public boolean stampInstructions(ItemStack stack) {
if (isPaper(stack) && isMarkedKit(stack, true) && ownsTooltip(paper, stack)) {
if (paper.kind() == ItemRef.Kind.VANILLA
&& isPaper(stack) && isMarkedKit(stack, true) && ownsTooltip(paper, stack)) {
stamped(stack, true);
return true;
}
if (isPencil(stack) && isMarkedKit(stack, false) && ownsTooltip(pencil, stack)) {
if (pencil.kind() == ItemRef.Kind.VANILLA
&& isPencil(stack) && isMarkedKit(stack, false) && ownsTooltip(pencil, stack)) {
stamped(stack, false);
return true;
}
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/net/tfminecraft/archaeo/sketch/SketchService.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ public class SketchService {
private final Map<UUID, SketchSession> sessions = new HashMap<>();
private final Map<Integer, SketchSheet> sheets = new HashMap<>();
private final Map<UUID, BossBar> bars = new HashMap<>();
/** Newest queued {@link #stampKitsLater} request per player; an older one leaves the pass to it. */
private final Map<UUID, Object> kitPasses = new HashMap<>();
private BukkitTask task;
private int autosaveClock;

Expand Down Expand Up @@ -600,12 +602,20 @@ private void handBackCabinet(Player player, Inventory top) {

/**
* Unstacks sketch maps and restores how-to lore on Archaeo-given vanilla kits only.
* Several inventory events in one tick (a pile of drops picked up, a click and a close) each
* ask before the scheduler runs; only the newest request for a player scans the bag, since
* it runs last and repeats the same pass.
*
* @param player holder
*/
public void stampKitsLater(Player player) {
var actor=net.tfminecraft.archaeo.ownership.CharacterActors.session(player).orElse(null);
Object pass = new Object();
kitPasses.put(player.getUniqueId(), pass);
plugin.getServer().getScheduler().runTask(plugin, () -> {
if (!kitPasses.remove(player.getUniqueId(), pass)) {
return;
}
if (!player.isOnline()||!net.tfminecraft.archaeo.ownership.CharacterActors.current(actor,player)) {
return;
}
Expand Down Expand Up @@ -1316,11 +1326,14 @@ private ItemStack duplicateSketch(World world, ItemStack original) {

/**
* Filled maps with the same view would otherwise stack; each sketch is one sheet.
* Only a filled map carries {@link MapMeta}, so every other item is skipped by its type
* without copying its metadata.
*
* @param stack sketch, or empty
*/
private static void markUnstackable(ItemStack stack) {
if (stack == null || !(stack.getItemMeta() instanceof MapMeta meta)) {
if (stack == null || stack.getType() != Material.FILLED_MAP
|| !(stack.getItemMeta() instanceof MapMeta meta)) {
return;
}
meta.setMaxStackSize(1);
Expand Down
61 changes: 58 additions & 3 deletions src/main/java/net/tfminecraft/archaeo/tracker/TrackerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,37 @@
import org.bukkit.SoundCategory;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
* Hold-to-scan tracker: arcs fire the way the player looks; pulse strength follows
* how well that look lines up with a hidden ruin and how far it is.
*/
public class TrackerService {
/**
* Ticks an unchanged pair of hands keeps its last answer before the tracker item is matched
* again (pack registries can reload under an unchanged stack).
*/
private static final int RECHECK_TICKS = 20;

private final JavaPlugin plugin;
private final SiteRepository sites;
private final TrackerItem item;
private TrackerSettings settings;
private final Map<UUID, Integer> lastBeepTick = new ConcurrentHashMap<>();
private final Map<UUID, Integer> lastDetectMessageTick = new ConcurrentHashMap<>();
private final Map<UUID, UUID> lockedSiteId = new ConcurrentHashMap<>();
/** Last hands checked per online player object; an entry goes when the player object does. */
private final Map<Player, HeldHands> held = new WeakHashMap<>();
private int tick;
private BukkitTask task;

Expand All @@ -48,10 +59,19 @@ public TrackerService(JavaPlugin plugin, SiteRepository sites, TrackerItem item,
}

/**
* @param settings latest values after a catalog reload
* @param settings latest values after a catalog reload, which can also change {@code tracker.item}
*/
public void setSettings(TrackerSettings settings) {
this.settings = settings;
itemChanged();
}

/**
* Forgets every remembered hand check, so the next pulse matches again. Call after
* {@code tracker.item} or the pack-plugin lookup behind it changes.
*/
public void itemChanged() {
held.clear();
}

/**
Expand All @@ -73,6 +93,7 @@ public void stop() {
lastBeepTick.clear();
lastDetectMessageTick.clear();
lockedSiteId.clear();
held.clear();
}

/**
Expand Down Expand Up @@ -108,12 +129,35 @@ public void pulse() {
}

/**
* Matches the hands again only when either stack differs from the pair checked last (any
* change to type, amount, or data) or after {@link #RECHECK_TICKS}. Matching a pack tracker
* reads the item's data through the pack plugin, which is too slow to repeat for every
* online player on every pulse.
*
* @param player online player
* @return {@code true} if main or off hand is a tracker
*/
private boolean holdingTracker(Player player) {
return item.isTracker(player.getInventory().getItemInMainHand())
|| item.isTracker(player.getInventory().getItemInOffHand());
ItemStack main = player.getInventory().getItemInMainHand();
ItemStack off = player.getInventory().getItemInOffHand();
HeldHands hands = held.get(player);
if (hands != null
&& tick - hands.checkedTick < RECHECK_TICKS
&& Objects.equals(hands.main, main)
&& Objects.equals(hands.off, off)) {
return hands.tracker;
}
boolean tracker = item.isTracker(main) || item.isTracker(off);
held.put(player, new HeldHands(copy(main), copy(off), tracker, tick));
return tracker;
}

/**
* @param stack live hand stack, or {@code null}
* @return a copy later changes to the live stack cannot reach
*/
private static ItemStack copy(ItemStack stack) {
return stack == null ? null : stack.clone();
}

/**
Expand Down Expand Up @@ -496,6 +540,17 @@ private void sendDetectIfReady(Player scanner) {
scanner.sendMessage("Prospect the area to determine the site location.");
}

/**
* One player's hands as last checked for a tracker.
*
* @param main copy of the main-hand stack
* @param off copy of the off-hand stack
* @param tracker whether either was the tracker
* @param checkedTick service tick of that check
*/
private record HeldHands(ItemStack main, ItemStack off, boolean tracker, int checkedTick) {
}

/**
* Hidden ruin still inside its detection bubble and in front of the scanner.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ public class ItemMatcherPackIT {
}
assertTrue(matcher.matches(null, ItemRef.air())); assertTrue(matcher.matches(dirt, ItemRef.vanilla(Material.DIRT)));
assertFalse(matcher.isCustom(dirt));
// The id is a second read of the item's data; a stack without the wanted type is not asked for it.
mi.verify(() -> mmoApi.getMethod("getID", ItemStack.class).invoke(null, dirt), never());
mi.when(() -> mmoApi.getMethod("getTypeName", ItemStack.class).invoke(null, dirt)).thenReturn("TOOL");
assertFalse(matcher.matches(dirt, ref("mi:tool:brush")));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ public void packPencilGivenByArchaeoKeepsThePackTooltipWhenRestamped() {
when(matcher.create(packPencil)).thenReturn(custom); supplies.setMatcher(matcher);
ItemStack given = supplies.createPencil(); when(matcher.matches(given, packPencil)).thenReturn(true);
assertFalse(supplies.stampInstructions(given)); assertEquals(List.of("Pack pencil"), given.getItemMeta().getLore());
// A pack pencil can never be stamped, so the pack plugin is not asked whether this stack is one.
verify(matcher, never()).matches(given, packPencil); verify(matcher).matches(given, paper);
// Switching sketch.pencil back to a vanilla stick must not overwrite pencils already handed out from the pack.
ItemRef vanillaStick = ItemRef.vanilla(Material.STICK); supplies.update(paper, vanillaStick, 0);
when(matcher.matches(given, vanillaStick)).thenReturn(true); when(matcher.isCustom(given)).thenReturn(true);
Expand All @@ -160,6 +162,11 @@ public void packItemsKeepNamesAndLoreAndMissingTemplatesStayMissing() {
assertEquals(List.of("Pack instructions"), custom.getItemMeta().getLore());
when(matcher.matches(custom, customRef)).thenReturn(true);
assertFalse(supplies.stampInstructions(custom));
// With both supplies from a pack, scanning a bag reads no item through the pack plugins.
supplies.stampAll(new ItemStack[]{custom, null, new ItemStack(Material.FEATHER)});
verify(matcher, never()).matches(any(), any());
verify(matcher, never()).isCustom(any());
assertEquals(List.of("Pack instructions"), custom.getItemMeta().getLore());
when(matcher.create(customRef)).thenReturn(null);
assertNull(supplies.createPaper());
when(matcher.create(customRef)).thenReturn(new ItemStack(Material.AIR));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,38 @@ public class SketchServiceTest {
assertTrue(player.getInventory().getItem(4).getItemMeta().hasLore());
}

@Test public void kitRequestsQueuedInOneTickScanTheBagOnceForEachPlayer() {
SketchSupplies counted = spy(supplies);
SketchService queued = new SketchService(plugin, counted, recovered, sites, catalogs, SketchSettings.defaults());
ItemStack sheet = supplies.createPaper(); var meta = sheet.getItemMeta(); meta.setLore(null); sheet.setItemMeta(meta);
player.getInventory().setItem(4, sheet);
// Three inventory events in one tick; the newest request runs last and makes the only pass.
queued.stampKitsLater(player); queued.stampKitsLater(player); queued.stampKitsLater(player);
server.getScheduler().performOneTick();
verify(counted, times(1)).stampAll(any());
assertTrue(player.getInventory().getItem(4).getItemMeta().hasLore());
// Nothing is left queued, and a request in a later tick is a pass of its own.
server.getScheduler().performOneTick();
verify(counted, times(1)).stampAll(any());
queued.stampKitsLater(player); server.getScheduler().performOneTick();
verify(counted, times(2)).stampAll(any());
// Another player's request never stands in for this one's.
PlayerMock other = server.addPlayer(); other.getInventory().setItem(2, supplies.createPencil());
queued.stampKitsLater(player); queued.stampKitsLater(other); queued.stampKitsLater(player);
server.getScheduler().performOneTick();
verify(counted, times(4)).stampAll(any());
}

@Test public void unstackingCarriedSketchesCopiesMetadataOnlyForFilledMaps() {
ItemStack stone = spy(new ItemStack(Material.STONE, 3)); ItemStack plain = new ItemStack(Material.FILLED_MAP);
org.bukkit.entity.Player carrier = mock(org.bukkit.entity.Player.class); PlayerInventory bag = mock(PlayerInventory.class);
when(carrier.getInventory()).thenReturn(bag); when(bag.getContents()).thenReturn(new ItemStack[]{stone, null, plain});
service.unstackCarried(carrier);
verify(stone, never()).getItemMeta(); assertEquals(3, stone.getAmount());
// Any filled map, sketch or not, is still capped at one per slot.
assertEquals(1, plain.getItemMeta().getMaxStackSize());
}

@Test public void cabinetAcceptsOnlySignedDrawingsAndDoesNotOverwriteExistingDeposit() {
Inventory cabinet = server.createInventory(null, 9);
ItemStack unsigned = service.createUnsigned(player.getWorld());
Expand Down
Loading
Loading