diff --git a/README.md b/README.md
index 054b477..924d742 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,13 @@ finished item.
it, never the count); a natural 20 always succeeds and a natural 1 always
fails. The roll never changes the piece: 100% hits is always a Masterwork.
+## Requirements
+
+Paper 1.21.10 with MMOCore, MMOItems, MythicLib and **TLibs 2.3.0 or newer**
+(builds pin 2.6.1). The item refresh listens for TLibs' character session
+event, so on an older TLibs its listener cannot register. `plugin.yml` cannot
+declare a minimum version, so update TLibs before or together with this plugin.
+
## Credits
Originally authored by **Drefvelin**.
diff --git a/pom.xml b/pom.xml
index 36d1c83..bec1f95 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
UTF-8
full
2026-09-22T00:00:00Z
- 2.0.0
+ 2.6.1
diff --git a/src/main/java/net/tfminecraft/advancedcrafting/managers/CraftRefreshListener.java b/src/main/java/net/tfminecraft/advancedcrafting/managers/CraftRefreshListener.java
index fa8c2a1..1c20151 100644
--- a/src/main/java/net/tfminecraft/advancedcrafting/managers/CraftRefreshListener.java
+++ b/src/main/java/net/tfminecraft/advancedcrafting/managers/CraftRefreshListener.java
@@ -21,6 +21,7 @@
import net.tfminecraft.tlibs.armour.ArmorEquipEvent;
import net.tfminecraft.tlibs.armour.ArmorType;
+import net.tfminecraft.tlibs.character.CharacterSessionStartEvent;
import net.tfminecraft.advancedcrafting.AdvancedCrafting;
import net.tfminecraft.advancedcrafting.utils.AcItemRefresher;
@@ -88,7 +89,16 @@ public void onArmorEquip(ArmorEquipEvent event) {
/** After a restart every player rejoins, so this brings their carried items up to date without anyone acting. */
@EventHandler(priority = EventPriority.MONITOR)
public void onJoin(PlayerJoinEvent event) {
- Player player = event.getPlayer();
+ sweepLater(event.getPlayer());
+ }
+
+ /** A character's inventory is only put on when it is played, after the join sweep has already run. */
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onSessionStart(CharacterSessionStartEvent event) {
+ sweepLater(event.getPlayer());
+ }
+
+ private void sweepLater(Player player) {
Bukkit.getScheduler().runTask(AdvancedCrafting.plugin, () -> {
if (!player.isOnline()) {
return;
diff --git a/src/test/java/net/tfminecraft/advancedcrafting/RefreshListenerCoverageTest.java b/src/test/java/net/tfminecraft/advancedcrafting/RefreshListenerCoverageTest.java
index 69a5072..8e790ba 100644
--- a/src/test/java/net/tfminecraft/advancedcrafting/RefreshListenerCoverageTest.java
+++ b/src/test/java/net/tfminecraft/advancedcrafting/RefreshListenerCoverageTest.java
@@ -7,6 +7,7 @@
import net.tfminecraft.advancedcrafting.managers.*;
import net.tfminecraft.advancedcrafting.utils.*;
import net.tfminecraft.tlibs.armour.*;
+import net.tfminecraft.tlibs.character.CharacterSessionStartEvent;
import net.tfminecraft.tlibs.event.MMOItemRebuildEvent;
import org.bukkit.Material;
import org.bukkit.block.DoubleChest;
@@ -158,6 +159,33 @@ void joinSweepRefreshesInventoryArmourOffhandAndEnderChestOneTickLater() throws
verify(gone, never()).getInventory();
}
+ @Test
+ void characterSessionStartSweepsTheCharacterInventoryOneTickLater() throws Exception {
+ when(plugin.isEnabled()).thenReturn(true);
+ var p = server.addPlayer();
+ var iron = ingredient("iron", "");
+ iron.setRevision(1);
+ var stale = iron.build();
+ iron.setRevision(2);
+ // The character's body is put on after the join sweep, so the join tick finds nothing to refresh.
+ var join = mock(PlayerJoinEvent.class);
+ when(join.getPlayer()).thenReturn(p);
+ var listener = new CraftRefreshListener();
+ listener.onJoin(join);
+ server.getScheduler().performOneTick();
+ p.getInventory().setChestplate(stale.clone());
+ p.getInventory().setItem(5, stale.clone());
+ p.getEnderChest().setItem(1, stale.clone());
+ var start = mock(CharacterSessionStartEvent.class);
+ when(start.getPlayer()).thenReturn(p);
+ listener.onSessionStart(start);
+ assertEquals(1, AcItemTags.getStoredRevision(p.getInventory().getChestplate()));
+ server.getScheduler().performOneTick();
+ assertEquals(2, AcItemTags.getStoredRevision(p.getInventory().getChestplate()));
+ assertEquals(2, AcItemTags.getStoredRevision(p.getInventory().getItem(5)));
+ assertEquals(2, AcItemTags.getStoredRevision(p.getEnderChest().getItem(1)));
+ }
+
@Test
void aBrokenItemIsLoggedAndTheSweepCarriesOn() {
when(plugin.isEnabled()).thenReturn(true);