From 19c9fbd42ac22f5cc3ae5191ec066d5205867883 Mon Sep 17 00:00:00 2001 From: Mark Friedman Date: Thu, 19 Mar 2026 18:09:57 -0400 Subject: [PATCH 1/6] fix: move client-only mixins to client array in mixin config ClientBlockBreakMixin targets MultiPlayerGameMode (client-only class) and PlayerCollidesWithEntityMixin imports LocalPlayer (client-only class). Both were incorrectly placed in the common 'mixins' array, causing dedicated servers to crash with ClassMetadataNotFoundException for net.minecraft.client.player.LocalPlayer. Moved both to the 'client' array so they only load on the client side. ClientConnectionAccessor remains in 'mixins' as it targets Connection which exists on both client and server. Fixes #28 --- common/src/main/resources/playerengine.mixins.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/resources/playerengine.mixins.json b/common/src/main/resources/playerengine.mixins.json index 57ba7e0f..ff82a4fb 100644 --- a/common/src/main/resources/playerengine.mixins.json +++ b/common/src/main/resources/playerengine.mixins.json @@ -8,15 +8,15 @@ "defaultRequire": 1 }, "client": [ + "ClientBlockBreakMixin", + "PlayerCollidesWithEntityMixin" ], "mixins": [ - "ClientBlockBreakMixin", "ClientConnectionAccessor", "EntityAnimationSwungMixin", "LivingEntityMixin", "MixinAbstractFurnaceBlockEntity", "PersistentProjectileEntityAccessor", - "PlayerCollidesWithEntityMixin", "PlayerDamageMixin", "WorldBlockModifiedMixin", "baritone.MixinBucketItem", From 2fed4dc0392bbac836b5ade9c96f2987c24a0fda Mon Sep 17 00:00:00 2001 From: Mark Friedman Date: Thu, 19 Mar 2026 20:11:54 -0400 Subject: [PATCH 2/6] fix: wrap recalculateHash in try-catch for config-not-loaded crash Some mods (e.g. ConstructionStick) access NeoForge config values in getMaxDamage() which may not be loaded yet during recipe deserialization. This causes IllegalStateException: Cannot get config value before config is loaded, crashing the server in a loop. Fall back to item.hashCode() without damage value when config is unavailable. --- .../mixins/baritone/MixinItemStack.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java index 3c157765..95bf67d8 100644 --- a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java +++ b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java @@ -23,7 +23,18 @@ public abstract class MixinItemStack implements IItemStack { public abstract int getDamageValue(); private void recalculateHash() { - this.baritoneHash = this.item == null ? -1 : this.item.hashCode() + this.getDamageValue(); + if (this.item == null) { + this.baritoneHash = -1; + return; + } + try { + this.baritoneHash = this.item.hashCode() + this.getDamageValue(); + } catch (IllegalStateException e) { + // Some mods (e.g. ConstructionStick) access NeoForge config values in + // getMaxDamage() which may not be loaded yet during recipe deserialization. + // Fall back to hash without damage value to avoid crashing the server. + this.baritoneHash = this.item.hashCode(); + } } @Inject( From c69818bbd6f02fd3e282ffc1e7bd011bf0b4a057 Mon Sep 17 00:00:00 2001 From: Mark Friedman Date: Fri, 20 Mar 2026 20:17:35 -0400 Subject: [PATCH 3/6] fix: widen catch to Exception for RuntimeDistCleaner crash The previous fix only caught IllegalStateException, but Undergarden's slingshot item triggers loading client-only SoundInstance class during getDamageValue() -> getMaxDamage() on DEDICATED_SERVER, which throws RuntimeException from NeoForge's RuntimeDistCleaner. Widening to catch(Exception) handles both failure modes safely. --- .../playerengine/mixins/baritone/MixinItemStack.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java index 95bf67d8..b8faa450 100644 --- a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java +++ b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java @@ -29,9 +29,10 @@ private void recalculateHash() { } try { this.baritoneHash = this.item.hashCode() + this.getDamageValue(); - } catch (IllegalStateException e) { - // Some mods (e.g. ConstructionStick) access NeoForge config values in - // getMaxDamage() which may not be loaded yet during recipe deserialization. + } catch (Exception e) { + // Catches multiple failure modes during ItemStack init: + // - IllegalStateException: NeoForge config not loaded (e.g. ConstructionStick) + // - RuntimeException: client-only class loaded on DEDICATED_SERVER (e.g. Undergarden slingshot -> SoundInstance) // Fall back to hash without damage value to avoid crashing the server. this.baritoneHash = this.item.hashCode(); } From e3af5ae1f775f2491bd8de3744e66dbc34fb2a99 Mon Sep 17 00:00:00 2001 From: Mark Friedman Date: Fri, 20 Mar 2026 20:31:21 -0400 Subject: [PATCH 4/6] fix: catch Throwable (not Exception) to handle StackOverflowError from Silent Gear recursive loop getMaxDamage -> PartInstance -> ItemStack.copy -> ItemStack. -> recalculateHash -> getDamageValue -> getMaxDamage causes StackOverflowError which extends Error, not Exception. --- .../player2/playerengine/mixins/baritone/MixinItemStack.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java index b8faa450..eab47f63 100644 --- a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java +++ b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java @@ -29,10 +29,11 @@ private void recalculateHash() { } try { this.baritoneHash = this.item.hashCode() + this.getDamageValue(); - } catch (Exception e) { - // Catches multiple failure modes during ItemStack init: + } catch (Throwable t) { + // Catches ALL failure modes during ItemStack init (Throwable to include Errors): // - IllegalStateException: NeoForge config not loaded (e.g. ConstructionStick) // - RuntimeException: client-only class loaded on DEDICATED_SERVER (e.g. Undergarden slingshot -> SoundInstance) + // - StackOverflowError: recursive loop in Silent Gear getMaxDamage -> PartInstance -> ItemStack.copy -> recalculateHash // Fall back to hash without damage value to avoid crashing the server. this.baritoneHash = this.item.hashCode(); } From da07a24d07b34d416a6217574d80878872647b60 Mon Sep 17 00:00:00 2001 From: Mark Friedman Date: Fri, 20 Mar 2026 20:40:09 -0400 Subject: [PATCH 5/6] fix: add ThreadLocal recursion guard to prevent Silent Gear infinite loop The recursive cycle getMaxDamage -> PartInstance -> ItemStack.copy -> ItemStack. -> recalculateHash -> getDamageValue -> getMaxDamage was spinning for 60+ seconds before StackOverflowError, triggering the Server Watchdog to kill the process. ThreadLocal guard detects re-entry and falls back to item.hashCode() without damage value, breaking the cycle immediately. --- .../mixins/baritone/MixinItemStack.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java index eab47f63..443a1802 100644 --- a/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java +++ b/common/src/main/java/com/player2/playerengine/mixins/baritone/MixinItemStack.java @@ -19,6 +19,12 @@ public abstract class MixinItemStack implements IItemStack { @Unique private int baritoneHash; + // Thread-local recursion guard: prevents infinite loop when getDamageValue() + // triggers ItemStack.copy -> ItemStack. -> recalculateHash -> getDamageValue + // (e.g. Silent Gear MainPartItem.getMaxDamage -> PartInstance -> ItemStack.copy) + @Unique + private static final ThreadLocal playerengine$inRecalc = ThreadLocal.withInitial(() -> Boolean.FALSE); + @Shadow public abstract int getDamageValue(); @@ -27,15 +33,24 @@ private void recalculateHash() { this.baritoneHash = -1; return; } + // If we're already inside recalculateHash on this thread, skip to avoid recursion. + // This breaks the cycle: recalculateHash -> getDamageValue -> getMaxDamage -> + // PartInstance -> ItemStack.copy -> ItemStack. -> recalculateHash + if (playerengine$inRecalc.get()) { + this.baritoneHash = this.item.hashCode(); + return; + } + playerengine$inRecalc.set(Boolean.TRUE); try { this.baritoneHash = this.item.hashCode() + this.getDamageValue(); } catch (Throwable t) { - // Catches ALL failure modes during ItemStack init (Throwable to include Errors): + // Catches failure modes during ItemStack init: // - IllegalStateException: NeoForge config not loaded (e.g. ConstructionStick) - // - RuntimeException: client-only class loaded on DEDICATED_SERVER (e.g. Undergarden slingshot -> SoundInstance) - // - StackOverflowError: recursive loop in Silent Gear getMaxDamage -> PartInstance -> ItemStack.copy -> recalculateHash - // Fall back to hash without damage value to avoid crashing the server. + // - RuntimeException: client-only class loaded on DEDICATED_SERVER + // - StackOverflowError: any remaining deep recursion this.baritoneHash = this.item.hashCode(); + } finally { + playerengine$inRecalc.set(Boolean.FALSE); } } From 19c22fad14ab5e3f46b6abb0de636a84198dbda3 Mon Sep 17 00:00:00 2001 From: Mark Friedman Date: Fri, 20 Mar 2026 22:35:54 -0400 Subject: [PATCH 6/6] fix6: Patch Architectury EventFactory ConcurrentModificationException Architectury API issue #653: EventFactory$EventImpl uses a plain ArrayList for its listeners field, which is not thread-safe. When a mod registers a listener during event dispatch (e.g., during the first server tick), the ArrayList iterator throws ConcurrentModificationException, crashing the server. This mixin injects at the end of EventImpl's constructor and replaces the ArrayList with a SnapshotArrayList subclass whose iterator() returns an iterator over a snapshot copy. This makes concurrent registration during iteration safe without changing the field type (which must remain ArrayList). Fixes deterministic crash loop on server startup with ATM10 modpack. --- .../mixins/MixinEventFactoryEventImpl.java | 63 +++++++++++++++++++ .../main/resources/playerengine.mixins.json | 3 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/com/player2/playerengine/mixins/MixinEventFactoryEventImpl.java diff --git a/common/src/main/java/com/player2/playerengine/mixins/MixinEventFactoryEventImpl.java b/common/src/main/java/com/player2/playerengine/mixins/MixinEventFactoryEventImpl.java new file mode 100644 index 00000000..c4f5ed6f --- /dev/null +++ b/common/src/main/java/com/player2/playerengine/mixins/MixinEventFactoryEventImpl.java @@ -0,0 +1,63 @@ +package com.player2.playerengine.mixins; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.function.Function; + +/** + * Fixes ConcurrentModificationException in Architectury EventFactory. + * + * Architectury API issue #653: EventFactory uses a plain ArrayList for event + * listeners, which is not thread-safe. When a mod registers a listener during + * event dispatch (e.g., during the first server tick), the iterator throws + * ConcurrentModificationException. + * + * This mixin replaces the ArrayList with a SnapshotArrayList subclass that + * returns snapshot-based iterators, making concurrent registration during + * iteration safe. We must extend ArrayList because the field type is ArrayList, + * not List. + */ +@Mixin(targets = "dev.architectury.event.EventFactory$EventImpl", remap = false) +public class MixinEventFactoryEventImpl { + + @Shadow + private ArrayList listeners; + + /** + * An ArrayList subclass whose iterators operate on a snapshot of the backing + * array at the time of iterator creation. This means concurrent add/remove + * during iteration won't throw ConcurrentModificationException. + */ + public static class SnapshotArrayList extends ArrayList { + @Override + public Iterator iterator() { + // Return an iterator over a snapshot copy + return new ArrayList<>(this).iterator(); + } + + @Override + public ListIterator listIterator() { + return new ArrayList<>(this).listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return new ArrayList<>(this).listIterator(index); + } + } + + @Inject(method = "", at = @At("RETURN")) + private void playerengine$replaceListenersWithSnapshotList(Function function, CallbackInfo ci) { + // Replace the plain ArrayList with our SnapshotArrayList. + // The snapshot iterator pattern means that iteration (event dispatch) sees + // a frozen copy, while registration can safely modify the real list. + this.listeners = new SnapshotArrayList<>(); + } +} diff --git a/common/src/main/resources/playerengine.mixins.json b/common/src/main/resources/playerengine.mixins.json index ff82a4fb..51d69bb5 100644 --- a/common/src/main/resources/playerengine.mixins.json +++ b/common/src/main/resources/playerengine.mixins.json @@ -16,6 +16,7 @@ "EntityAnimationSwungMixin", "LivingEntityMixin", "MixinAbstractFurnaceBlockEntity", + "MixinEventFactoryEventImpl", "PersistentProjectileEntityAccessor", "PlayerDamageMixin", "WorldBlockModifiedMixin", @@ -28,4 +29,4 @@ "baritone.MixinServerCommandSource", "baritone.MixinUtil" ] -} \ No newline at end of file +}