Skip to content
Closed
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ Concurrent Chunk Management Engine, Fabric API, FerriteCore, Lithium, ScalableLu
- `/async stats entity` — Shows the number of entities processed by Async in various worlds.
- `/async stats entity [number] [ticks]` — Shows the top [number] entity types by count in descending order. For example, `/async stats entity 10` displays the top 10 most numerous entity types. /async stats entity 5 20 displays the top 5 most numerous entity types with their average mspt usage.

## 📚 API for Mod Developers
Making your custom entities or AI compatible with Async? See the full API reference:
**[docs/API.md](docs/API.md)** — annotations, concurrent collections, sensor helpers, and fastutil concurrent wrappers, with usage examples.

## 📥 Download
The mod is available on [Modrinth](https://modrinth.com/mod/async)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.axalotl.async.api.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks an entity method that consumes an {@code ItemEntity} (typically the override
* of {@code pickUpItem(ServerLevel, ItemEntity)}) as needing automatic synchronization
* when Async is processing entities on multiple threads.
*
* <p>At class-load time, AsyncAPI rewrites the annotated method to:</p>
* <ol>
* <li>Acquire a per-declaring-class lock before executing the body.</li>
* <li>Skip the body entirely if the {@code ItemEntity} parameter has already been
* removed (i.e. {@code itemEntity.isRemoved()} returns {@code true}).</li>
* </ol>
*
* <p>Without these two guards, two parallel ticks may race on the same item entity,
* both pass their internal liveness check, and both consume it — producing the classic
* item-duplication bug observed on Pandas, Foxes, Allays, Villagers, etc.</p>
*
* <h2>Contract</h2>
* <ul>
* <li>The annotated method <b>must</b> declare an {@code ItemEntity} (or subtype) parameter.</li>
* <li>Annotate only the entry point that consumes the item — not internal helpers.</li>
* <li>Do <b>not</b> add a manual {@code synchronized} keyword or your own
* {@code isRemoved()} check; both are injected for you.</li>
* </ul>
*
* <h2>Example</h2>
* <pre>{@code
* @AsyncCompatible
* public class TruffleHog extends Animal {
*
* @Override
* @SyncItemPickup
* protected void pickUpItem(ServerLevel level, ItemEntity item) {
* super.pickUpItem(level, item);
* getBrain().setMemory(MemoryModuleType.LIKED_PLAYER, item.getOwner());
* }
* }
* }</pre>
*
* @see com.axalotl.async.api.utils.AsyncCompatible
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SyncItemPickup {
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.animal.allay.Allay;
import net.minecraft.world.entity.item.ItemEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(Allay.class)
public abstract class AllayMixin {

@Unique
private static final Object lock = new Object();

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
Expand All @@ -9,24 +10,17 @@
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(Dolphin.class)
public abstract class DolphinMixin extends AgeableWaterCreature {

@Unique
private static final Object lock = new Object();

protected DolphinMixin(EntityType<? extends AgeableWaterCreature> entityType, Level level) {
super(entityType, level);
}

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.animal.fox.Fox;
import net.minecraft.world.entity.item.ItemEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(Fox.class)
public class FoxMixin {

@Unique
private static final Object lock = new Object();

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
Expand All @@ -24,10 +25,9 @@ private ItemStack tryEquip(ServerLevel level, ItemStack itemStack, Operation<Ite
}

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
original.call(level, entity);
}
original.call(level, entity);
}

@WrapMethod(method = "setItemSlotAndDropWhenKilled")
Expand All @@ -43,4 +43,4 @@ private void equipLootStack(ItemStack item, Operation<Void> original) {
original.call(item);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.animal.panda.Panda;
import net.minecraft.world.entity.item.ItemEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(Panda.class)
public class PandaMixin {

@Unique
private static final Object lock = new Object();

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.monster.piglin.Piglin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(Piglin.class)
public class PiglinMixin {

@Unique
private static final Object lock = new Object();

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.raid.Raider;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(Raider.class)
public class RaiderMixin {

@Unique
private static final Object lock = new Object();

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.axalotl.async.common.mixin.entity;

import com.axalotl.async.api.annotation.SyncItemPickup;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.server.level.ServerLevel;
Expand All @@ -15,12 +16,9 @@ public class VillagerMixin {
private static final Object lock = new Object();

@WrapMethod(method = "pickUpItem")
@SyncItemPickup
private void pickUpItem(ServerLevel level, ItemEntity entity, Operation<Void> original) {
synchronized (lock) {
if (!entity.isRemoved()) {
original.call(level, entity);
}
}
original.call(level, entity);
}

@WrapMethod(method = "spawnGolemIfNeeded")
Expand All @@ -29,4 +27,4 @@ private void spawnGolemIfNeeded(ServerLevel level, long timestamp, int villagers
original.call(level, timestamp, villagersNeededToAgree);
}
}
}
}
Loading
Loading