diff --git a/.github/compatibility/tlibs-2.3.xml b/.github/compatibility/tlibs-2.3.xml
new file mode 100644
index 0000000..2dc907c
--- /dev/null
+++ b/.github/compatibility/tlibs-2.3.xml
@@ -0,0 +1,22 @@
+
+
+ 4.0.0
+ net.tfminecraft
+ magic-compatibility-fixture
+ main-SNAPSHOT
+
+ 21
+ UTF-8
+ 2.3.0
+
+
+
+ me.plugins
+ tlibs
+ ${tlibs.version}
+ provided
+
+
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9dfce1b..c2d0725 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,6 +26,13 @@ jobs:
with:
mode: pinned
+ - name: Install older API compatibility fixture
+ uses: TF-Minecraft/TLibs/.github/actions/setup-plugins@8ef41de589f121c98ff892b10696e7c1feb4ca74
+ with:
+ mode: pinned
+ pom: .github/compatibility/tlibs-2.3.xml
+ lock: .build/legacy-plugin-dependencies.json
+
- name: Prepare pinned dependencies
env:
GH_TOKEN: ${{ secrets.DEPS_TOKEN }}
diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml
index c8c049f..39902d4 100644
--- a/.github/workflows/maven-release.yml
+++ b/.github/workflows/maven-release.yml
@@ -51,6 +51,13 @@ jobs:
with:
mode: pinned
+ - name: Install older API compatibility fixture
+ uses: TF-Minecraft/TLibs/.github/actions/setup-plugins@8ef41de589f121c98ff892b10696e7c1feb4ca74
+ with:
+ mode: pinned
+ pom: .github/compatibility/tlibs-2.3.xml
+ lock: .build/legacy-plugin-dependencies.json
+
- name: Prepare pinned dependencies
env:
GH_TOKEN: ${{ secrets.DEPS_TOKEN }}
diff --git a/pom.xml b/pom.xml
index 1441572..279096d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
full
0.2.0
2.16.0
- 2.3.0
+ 2.6.0
diff --git a/src/main/java/net/tfminecraft/magic/Magic.java b/src/main/java/net/tfminecraft/magic/Magic.java
index 795dee6..8e4f707 100644
--- a/src/main/java/net/tfminecraft/magic/Magic.java
+++ b/src/main/java/net/tfminecraft/magic/Magic.java
@@ -157,6 +157,7 @@ public void onEnable() {
getServer().getScheduler().runTask(this, ArtifactDisplayIndex::rebuildFromSaved);
getServer().getScheduler().runTask(this, GearStationStore::load);
GearOrbService.start();
+ net.tfminecraft.magic.gear.ownership.GearOwnershipLifecycle.start(this);
if (ItemScanService.get() != null) {
ItemScanService.get().subscribe(attuneScanHandler);
}
diff --git a/src/main/java/net/tfminecraft/magic/gear/GearChargeService.java b/src/main/java/net/tfminecraft/magic/gear/GearChargeService.java
index a47d24e..1f78a69 100644
--- a/src/main/java/net/tfminecraft/magic/gear/GearChargeService.java
+++ b/src/main/java/net/tfminecraft/magic/gear/GearChargeService.java
@@ -31,6 +31,9 @@ private GearChargeService() {}
private static final class Confirm {
private final String stationKey;
private final long expiresAt;
+ private net.tfminecraft.magic.gear.ownership.GearIdentity.Token token;
+ private UUID generation;
+ private String itemHash;
private Confirm(String stationKey, long expiresAt) {
this.stationKey = stationKey;
@@ -52,7 +55,8 @@ public static void tryApply(Player player, Location station, ItemStack hand) {
player.sendMessage(Messages.get("gear.charge.blank"));
return;
}
- if (!SessionGuard.ready(player)) {
+ var token = GearStationStore.property().identity().current(player);
+ if (token.isEmpty()) {
player.sendMessage(Messages.get("gear.charge.no_character"));
return;
}
@@ -72,14 +76,16 @@ public static void tryApply(Player player, Location station, ItemStack hand) {
}
String stationKey = GearStationStore.key(station);
List over = overResonance(player, charge);
- if (!over.isEmpty() && !confirmed(player, stationKey)) {
+ if (!over.isEmpty() && !confirmed(player, stationKey, occupancy, token.orElseThrow())) {
player.sendMessage(Messages.get("gear.charge.warn"));
for (String line : over) {
player.sendMessage(line);
}
player.sendMessage(Messages.get("gear.charge.warn_again"));
- CONFIRMS.put(player.getUniqueId(), new Confirm(
- stationKey, System.currentTimeMillis() + GearCache.confirmMillis));
+ Confirm confirm = new Confirm(stationKey, System.currentTimeMillis() + GearCache.confirmMillis);
+ confirm.token = token.orElseThrow(); confirm.generation = occupancy.generation();
+ confirm.itemHash = net.tfminecraft.magic.gear.ownership.GearPropertyStore.hash(net.tfminecraft.magic.gear.ownership.GearItems.bytes(occupancy.getItem()));
+ CONFIRMS.put(player.getUniqueId(), confirm);
player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.8f);
return;
}
@@ -91,24 +97,20 @@ public static void tryApply(Player player, Location station, ItemStack hand) {
snapshot.put(elementId, fill);
}
}
- // The charge is spent up front. What reaches the weapon is decided by the orbs.
- occupancy.setOrbSessionActive(true);
- if (!GearOrbService.begin(player, station, snapshot, charge.primaryElementId(), charge.getTier())) {
- occupancy.setOrbSessionActive(false);
- player.sendMessage(Messages.get("gear.orbs.busy"));
- return;
+ if (!net.tfminecraft.magic.gear.ownership.GearEffects.charge(player, token.orElseThrow(), station, hand, snapshot, charge.primaryElementId(), charge.getTier())) {
+ player.sendMessage(Messages.get("gear.orbs.busy")); return;
}
- consumeOne(player, hand);
player.sendMessage(Messages.get("gear.charge.applied"));
player.playSound(player.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.2f);
}
- private static boolean confirmed(Player player, String stationKey) {
+ private static boolean confirmed(Player player, String stationKey, GearStationStore.Occupancy occupancy, net.tfminecraft.magic.gear.ownership.GearIdentity.Token token) {
Confirm confirm = CONFIRMS.get(player.getUniqueId());
if (confirm == null || System.currentTimeMillis() > confirm.expiresAt) {
return false;
}
- return confirm.stationKey.equals(stationKey);
+ return confirm.stationKey.equals(stationKey) && token.equals(confirm.token) && java.util.Objects.equals(occupancy.generation(), confirm.generation)
+ && net.tfminecraft.magic.gear.ownership.GearPropertyStore.hash(net.tfminecraft.magic.gear.ownership.GearItems.bytes(occupancy.getItem())).equals(confirm.itemHash);
}
private static List overResonance(Player player, Charge charge) {
@@ -138,12 +140,12 @@ private static List overResonance(Player player, Charge charge) {
return lines;
}
- private static void consumeOne(Player player, ItemStack hand) {
- if (hand.getAmount() <= 1) {
- player.getInventory().setItemInMainHand(null);
- } else {
- hand.setAmount(hand.getAmount() - 1);
- }
- player.updateInventory();
+ public static void clear(Player player, net.tfminecraft.magic.gear.ownership.GearIdentity.Token outgoing) {
+ Confirm confirm = CONFIRMS.get(player.getUniqueId());
+ if (confirm != null && outgoing.equals(confirm.token)) clear(player);
+ }
+ public static void clearStale(Player player) {
+ Confirm confirm = CONFIRMS.get(player.getUniqueId());
+ if (confirm != null && !GearStationStore.property().identity().current(confirm.token, player)) clear(player);
}
}
diff --git a/src/main/java/net/tfminecraft/magic/gear/GearCosts.java b/src/main/java/net/tfminecraft/magic/gear/GearCosts.java
index 33d826c..aa6904f 100644
--- a/src/main/java/net/tfminecraft/magic/gear/GearCosts.java
+++ b/src/main/java/net/tfminecraft/magic/gear/GearCosts.java
@@ -90,6 +90,24 @@ public static void take(Player player, Collection parts) {
player.updateInventory();
}
+ /** Plans removal on clones; a deposit checks the captured frame again before commit. */
+ public static ItemStack[] plan(Player player, Collection parts) {
+ ItemStack[] after = net.tfminecraft.magic.gear.ownership.GearEffects.cloneContents(player.getInventory().getContents());
+ if (bypasses(player)) return after;
+ for (var cost : total(parts).entrySet()) {
+ int remaining = cost.getValue();
+ for (int i = 0; i < after.length && remaining > 0; i++) {
+ ItemStack item = after[i]; if (item == null) continue;
+ try { if (!TLibs.getItemAPI().getChecker().checkItemWithPath(item, cost.getKey())) continue; }
+ catch (RuntimeException ex) { continue; }
+ int remove = Math.min(remaining, item.getAmount()); remaining -= remove;
+ if (remove == item.getAmount()) after[i] = null; else item.setAmount(item.getAmount() - remove);
+ }
+ if (remaining > 0) return null;
+ }
+ return after;
+ }
+
public static void refund(Player player, Map costs, Location drop) {
if (player == null || costs == null || costs.isEmpty()) {
return;
diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java b/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java
index 8c4637c..e056bf3 100644
--- a/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java
+++ b/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java
@@ -80,10 +80,12 @@ public void onFurnitureBroken(FurnitureBreakEvent event) {
if (station == null) {
return;
}
+ GearStationStore.Occupancy captured = GearStationStore.get(station);
GearOrbService.abort(station);
ItemStack weapon = GearStationStore.takeForAbort(station);
if (weapon != null) {
station.getWorld().dropItem(station.clone().add(0.5, 1.0, 0.5), weapon);
+ GearStationStore.acknowledgeRelease(station, captured.generation());
Magic.plugin.getLogger().warning("[Magic] Gear station at " + GearStationStore.key(station)
+ " lost its furniture. Dropped the weapon and removed its display.");
}
@@ -142,6 +144,7 @@ public void onInteract(PlayerInteractEvent event) {
}
event.setCancelled(true);
Player player = event.getPlayer();
+ if (GearStationStore.property().identity().current(player).isEmpty()) return;
Location location = block.getLocation();
ItemStack hand = player.getInventory().getItemInMainHand();
boolean emptyHand = hand.getType().isAir();
@@ -190,13 +193,14 @@ public void onInteract(PlayerInteractEvent event) {
private static void rest(Player player, Location location, ItemStack hand) {
ItemStack placed = hand.clone();
placed.setAmount(1);
- if (hand.getAmount() <= 1) {
- player.getInventory().setItemInMainHand(null);
- } else {
- hand.setAmount(hand.getAmount() - 1);
- }
- player.updateInventory();
- GearStationStore.rest(location, placed, player.getUniqueId());
+ var token = GearStationStore.property().identity().current(player);
+ if (token.isEmpty()) return;
+ List before = net.tfminecraft.magic.gear.ownership.GearEffects.frame(player.getInventory().getContents());
+ ItemStack[] after = net.tfminecraft.magic.gear.ownership.GearEffects.cloneContents(player.getInventory().getContents());
+ int slot = player.getInventory().getHeldItemSlot();
+ if (after[slot] == null || !java.util.Arrays.equals(net.tfminecraft.magic.gear.ownership.GearItems.bytes(hand), net.tfminecraft.magic.gear.ownership.GearItems.bytes(after[slot]))) return;
+ if (after[slot].getAmount() <= 1) after[slot] = null; else after[slot].setAmount(after[slot].getAmount() - 1);
+ if (GearStationStore.deposit(player, token.orElseThrow(), location, placed, Map.of(), true, after, before) == null) return;
player.sendMessage(Messages.get("gear.station.rested"));
player.playSound(location, Sound.BLOCK_ANVIL_USE, 1f, 1.2f);
}
@@ -258,12 +262,14 @@ private static void eject(Player player, Location location) {
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
return;
}
+ if (GearStationStore.property().identity().current(player).isEmpty()) return;
ItemStack item = GearStationStore.eject(location);
if (item == null) {
return;
}
Location drop = location.clone().add(0.5, 1.0, 0.5);
drop.getWorld().dropItem(drop, item);
+ GearStationStore.acknowledgeRelease(location, occupancy.generation());
player.playSound(location, Sound.ENTITY_ITEM_PICKUP, 1f, 1f);
}
@@ -272,24 +278,12 @@ private static void eject(Player player, Location location) {
* weapon. An idle rested weapon stays put; an empty hand picks it up.
*/
private void returnRested(Player player, Location location, GearStationStore.Occupancy occupancy) {
- UUID owner = occupancy.getOwner() != null ? occupancy.getOwner() : GearOrbService.sessionOwner(location);
- if (owner != null && !owner.equals(player.getUniqueId()) && !player.hasPermission("magic.admin")) {
- player.sendMessage(Messages.get("gear.abort.not_yours"));
- player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
- return;
- }
- GearOrbService.abort(location);
- ItemStack weapon = GearStationStore.takeForAbort(location);
- if (weapon == null) {
- return;
+ if (!net.tfminecraft.magic.gear.ownership.GearEffects.authorize(player, location, occupancy)) {
+ player.sendMessage(Messages.get("gear.abort.not_yours")); return;
}
+ if (!net.tfminecraft.magic.gear.ownership.GearEffects.abortReturn(player, location, occupancy, Map.of())) return;
recentAborts.values().removeIf(at -> System.currentTimeMillis() - at >= ABORT_BREAK_GUARD_MILLIS);
recentAborts.put(GearStationStore.key(location), System.currentTimeMillis());
- Location drop = location.clone().add(0.5, 1.0, 0.5);
- for (ItemStack leftover : player.getInventory().addItem(weapon).values()) {
- drop.getWorld().dropItem(drop, leftover);
- }
- player.updateInventory();
player.sendMessage(Messages.get("gear.abort.returned"));
player.playSound(location, Sound.ENTITY_ITEM_PICKUP, 1f, 1f);
}
@@ -329,23 +323,13 @@ private void tryAbort(PlayerInteractEvent event) {
returnRested(player, location, occupancy);
return;
}
- UUID owner = occupancy.getOwner() != null ? occupancy.getOwner() : GearOrbService.sessionOwner(location);
- if (owner != null && !owner.equals(player.getUniqueId()) && !player.hasPermission("magic.admin")) {
- player.sendMessage(Messages.get("gear.abort.not_yours"));
- player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
- return;
- }
- GearOrbService.abort(location);
- ItemStack weapon = GearStationStore.takeForAbort(location);
- if (weapon == null) {
- return;
+ if (!net.tfminecraft.magic.gear.ownership.GearEffects.authorize(player, location, occupancy)) {
+ player.sendMessage(Messages.get("gear.abort.not_yours")); return;
}
+ Map charged = occupancy.getCharged();
+ if (!net.tfminecraft.magic.gear.ownership.GearEffects.abortReturn(player, location, occupancy, charged)) return;
recentAborts.values().removeIf(at -> System.currentTimeMillis() - at >= ABORT_BREAK_GUARD_MILLIS);
recentAborts.put(GearStationStore.key(location), System.currentTimeMillis());
- Map charged = occupancy.getCharged() != null
- ? occupancy.getCharged()
- : GearCosts.total(GearProvenance.resolveParts(weapon));
- GearCosts.refund(player, charged, location);
player.sendMessage(Messages.get("gear.abort.done"));
player.playSound(location, Sound.BLOCK_ANVIL_LAND, 0.6f, 1.4f);
}
diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java b/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java
index a27f549..f60dd4c 100644
--- a/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java
+++ b/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java
@@ -38,6 +38,8 @@ public final class GearStationStore {
private static boolean loading;
private static final Map retainedRows = new LinkedHashMap<>();
private static boolean unreadableStore;
+ private static net.tfminecraft.magic.gear.ownership.GearPropertyStore property;
+ private static Magic propertyPlugin;
private GearStationStore() {}
@@ -48,6 +50,8 @@ public static final class Occupancy {
private final UUID owner;
private final Map charged;
private final boolean rested;
+ private UUID generation;
+ private net.tfminecraft.magic.gear.ownership.GearIdentity.Owner characterOwner;
Occupancy(ItemStack item, UUID displayId, UUID owner, Map charged, boolean rested) {
this.item = item;
@@ -79,9 +83,12 @@ public boolean isRested() {
}
public ItemStack getItem() {
- return item;
+ return item == null ? null : item.clone();
}
+ public UUID generation() { return generation; }
+ public net.tfminecraft.magic.gear.ownership.GearIdentity.Owner characterOwner() { return characterOwner; }
+
public void setItem(ItemStack item) {
this.item = item;
refreshDisplay();
@@ -106,34 +113,56 @@ private void refreshDisplay() {
}
}
+ public static net.tfminecraft.magic.gear.ownership.GearPropertyStore property() {
+ if (property == null || propertyPlugin != Magic.plugin) {
+ propertyPlugin = Magic.plugin;
+ property = new net.tfminecraft.magic.gear.ownership.GearPropertyStore(
+ Magic.plugin.getDataFolder().toPath().resolve("data"), net.tfminecraft.magic.gear.ownership.GearIdentity.load());
+ }
+ return property;
+ }
+
public static boolean isOccupied(Location location) {
- return location != null && OCCUPIED.containsKey(key(location));
+ if (location == null) return false;
+ try { return property().occupied(key(location)); }
+ catch (IOException ex) { warning(ex); return true; }
}
public static Occupancy get(Location location) {
return location == null ? null : OCCUPIED.get(key(location));
}
- public static Occupancy occupy(Location location, ItemStack item, UUID owner, Map charged) {
- return place(location, item, owner, charged, false);
+ /** Compatibility entry point still requires the exact current owning character. */
+ public static Occupancy occupy(Location location, ItemStack item, UUID account, Map charged) {
+ return place(location, item, account, charged, false);
+ }
+ public static Occupancy rest(Location location, ItemStack item, UUID account) {
+ return place(location, item, account, Map.of(), true);
+ }
+ private static Occupancy place(Location location, ItemStack item, UUID account, Map charged, boolean rested) {
+ org.bukkit.entity.Player player = account == null ? null : Bukkit.getPlayer(account);
+ var token = property().identity().current(player);
+ if (token.isEmpty() || location == null || item == null) return null;
+ return deposit(player, token.orElseThrow(), location, item, charged, rested, player.getInventory().getContents());
}
- /**
- * Sets an existing weapon on an empty station so it can take another charge.
- * Nothing was consumed, so the station records no material cost.
- */
- public static Occupancy rest(Location location, ItemStack item, UUID owner) {
- return place(location, item, owner, Map.of(), true);
+ public static Occupancy deposit(org.bukkit.entity.Player player, net.tfminecraft.magic.gear.ownership.GearIdentity.Token token,
+ Location location, ItemStack item, Map charged, boolean rested, ItemStack[] afterInventory) {
+ return deposit(player, token, location, item, charged, rested, afterInventory,
+ net.tfminecraft.magic.gear.ownership.GearEffects.frame(player.getInventory().getContents()));
+ }
+ public static Occupancy deposit(org.bukkit.entity.Player player, net.tfminecraft.magic.gear.ownership.GearIdentity.Token token,
+ Location location, ItemStack item, Map charged, boolean rested, ItemStack[] afterInventory, List beforeInventory) {
+ return net.tfminecraft.magic.gear.ownership.GearEffects.deposit(player, token, location, item, charged, rested, afterInventory, beforeInventory);
}
- private static Occupancy place(
- Location location, ItemStack item, UUID owner, Map charged, boolean rested) {
- if (location == null || item == null) {
- return null;
- }
- clear(location, false);
+ public static Occupancy install(Location location, ItemStack item, Map