事务

事务

事务是 NeoForge 添加的系统,用于管理不同物品栏或容器之间转移内容时的交互。每次转移涉及三个基本概念:转移的 Resource、表示容器的 ResourceHandler,以及协调操作的 Transaction。

资源

Resource 表示事务操作的底层对象。每种资源都应不可变,只说明对象种类,不包含转移数量。例如“五个苹果换一个绿宝石”的事务中,资源是“苹果”和“绿宝石”,而不是“五个苹果”和“一个绿宝石”。

因此,每种资源都应满足三项特征:

  • 不可变:资源对象中保存的任何内容都不应改变。
  • 与数量无关:不保存对象数量。
  • 相等性:无论如何构造,只要表示相同对象,就必须相等。

NeoForge 通过 ItemResource 和 FluidResource 分别提供物品、流体资源,包含底层对象以及其独有的数据组件。

// Create the resource from its backing object
ItemResource item = ItemResource.of(Items.EMERALD);

ItemStack stack = new ItemStack(Items.APPLE);
stack.set(DataComponents.CUSTOM_NAME, Component.literal("Apple?"));
ItemResource itemWithComponents = ItemResource.of(stack);

FluidResource fluid = FluidResource.of(Fluids.WATER);

也可以创建自己的资源:

// Let's assume we are trying to represent the following object:
public class ExampleObject {
    public static final ExampleObject EMPTY = new ExampleObject(-1, 0, Map.of());

    public static final Codec<ExampleObject> CODEC = RecordCodecBuilder.of(instance ->
        instance.group(
            ExtraCodecs.NON_NEGATIVE_INT.fieldOf("id").forGetter(ExampleObject::id),
            ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("count", 1).forGetter(ExampleObject::count),
            Codec.unboundedMap(Codec.STRING, Codec.BOOL).optionalFieldOf("flags", Map::of).forGetter(ExampleObject::flags)
        ).apply(instance, ExampleObject::new)
    );

    private final int id;
    private final Map<String, Boolean> flags;
    private int count;

    public ExampleObject(int id, int count, Map<String, Boolean> flags) {
        // ...
    }

    public int id() {
        return this.id;
    }

    public int count() {
        return this.count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public Map<String, Boolean> flags() {
        return this.flags;
    }
}

// Create our resource.
public final class ExampleResource implements Resource {

    private final ExampleObject object;

    public ExampleResource(ExampleObject object) {
        // Enforce immutability and ignore count.
        this.object = new ExampleObject(
            object.id(), 1, ImmutableMap.copyOf(object.flags())
        );
    }

    public int id() {
        return this.object.id();
    }

    public Map<String, Boolean> flags() {
        return this.object.flags();
    }

    // Defines when the backing object is considered empty.
    // This is the only method that `Resource` defines.
    @Override
    public boolean isEmpty() {
        return this.object.id() == -1;
    }

    // Equality for classes is defined by implementing `hashCode`
    // and `equals`. Records already do this for you.
    @Override
    public int hashCode() {
        // Since our backing object is not unique by itself, we
        // extract the components that make it unique and construct
        // the hash.
        return Objects.hash(this.object.id(), this.object.flags());
    }

    @Override
    public boolean equals(Object obj) {
        // Check identity equality.
        if (this == obj) return true;
        // Check if same class.
        if (obj == null || this.getClass() != obj.getClass()) return false;
        // Check the individual components of the resource.
        ExampleResource other = (ExampleResource) obj;
        return this.object.id() == other.object.id()
            && this.object.flags().equals(other.object.flags());
    }

    // Just an ease of convenience to more easily understand what
    // the resource is representing.
    @Override
    public String toString() {
        return Integer.toString(this.object.id()) + "[" 
            + this.object.flags().size() + "]";
    }
}

提示 基本类型也可以包装为资源,但并非必需,例如能量直接使用 long,不定义 Resource。不过,下文的资源处理器要求使用 Resource,因此不使用资源时,需要自行重新实现相应的部分行为。

资源处理器

ResourceHandler<T> 表示事务中的底层容器,T 是资源类型。每个处理器通过索引对应内容,例如索引 0 为第一个槽位,1 为第二个。对每个索引,可通过 isValid 检查能否存入某种资源,通过 getResource 查询已有资源;通过 getCapacityAsLong/getCapacityAsInt 查询容量,通过 getAmountAsLong/getAmountAsInt 查询当前数量。可访问索引的总数就是 size。

修改内容使用 insert 和 extract,分别插入和取出资源。两者接受资源、int 数量,以及表示当前事务的 TransactionContext,返回实际插入或取出的数量。它们会依次寻找可操作的位置。如果只操作某个索引,可使用额外接受 int 索引的重载。

// For some ResourceHandler<ItemResource> handler

// Get the resource stored in the handler.
ItemResource item = handler.getResource(0);
int count = handler.getAmountAsInt(0);

// Get information about the handler itself.
int handlerSize = handler.size();
int indexCapacity = handler.getCapacityAsInt(0);
boolean canAcceptApples = handler.isValid(0, ItemResource.of(Items.APPLE));

根据底层容器不同,有多种处理器。一些包装现有原版容器,例如 VanillaContainerWrapper 包装 Container,PlayerInventoryWrapper 包装玩家物品栏,LivingEntityEquipmentWrapper 包装生物实体装备槽。

// Wrapping around an existing container.
Container container = new SimpleContainer(5);
ResourceHandler<ItemResource> containerWrapper = VanillaContainerWrapper.of(container);

// Wrapping around a `Player` player inventory.
ResourceHandler<ItemResource> playerInv = PlayerInventoryWrapper.of(player);

// Wrapping around a specific equipment slot for some LivingEntity entity.
ResourceHandler<ItemResource> head = LivingEntityEquipmentWrapper.of(entity, EquipmentSlot.HEAD);

另一些本身就是容器,方便直接使用,无需大量实现。例如,ItemStacksResourceHandler 保存 ItemStack 列表,FluidStacksResourceHandler 保存 FluidStack 列表。

// Creating an `ItemStack` storage.
ItemStacksResourceHandler itemStorage = new ItemStacksResourceHandler(5);

// Creating a `FluidStack` storage.
FluidStacksResourceHandler fluidStorage = new FluidStacksResourceHandler(
    // The size of the handler
    5,
    // The maximum capacity of every index
    1000
);

提示 将 StacksResourceHandler 用作容器时,强烈建议重写 onContentsChanged,处理磁盘保存或网络同步。

// Example for block entities
public class ExampleBlockEntity extends BlockEntity {

    private final ItemStacksResourceHandler storage = new ItemStacksResourceHandler(5) {
        @Override
        protected void onContentsChanged(int index, ItemStack previousContents) {
            // Schedule the block entity for saving
            BlockEntity.this.setChanged();
        }
    };

    // ...
}

也可以创建自己的处理器:

public class ExampleResourceHandler implements ResourceHandler<ExampleResource> {

    private ExampleObject object;

    public ExampleResourceHandler(ExampleObject object) {
        this.object = object;
    }
    
    @Override
    public int size() {
        // The size of the handler.
        return 1;
    }

    @Override
    public ExampleResource getResource(int index) {
        // Gets the resource at the desired index.

        // Check the bounds.
        Objects.checkIndex(index, this.size());
        // Then get the resource.
        return new ExampleResource(this.object);
    }

    @Override
    public long getAmountAsLong(int index) {
        // Gets the amount from the content.
        Objects.checkIndex(index, this.size());
        return this.object.count();
    }

    @Override
    public long getCapacityAsLong(int index, ExampleResource resource) {
        // The capacity at a given index for the stored resource.
        Objects.checkIndex(index, this.size());
        return Integer.MAX_VALUE;
    }

    @Override
    public boolean isValid(int index, ExampleResource resource) {
        // Whether the resource can be set at the index, regardless of its
        // current contents.
        Objects.checkIndex(index, this.size());
        // Make sure the resource isn't empty.
        TransferPreconditions.checkNonEmpty(resource);
        return true;
    }

    @Override
    public int insert(int index, ExampleResource resource, int amount, TransactionContext transaction) {
        // Inserts the resource into the given index, returning the amount put in.

        // Validate arguments.
        Objects.checkIndex(index, size());
        TransferPreconditions.checkNonEmptyNonNegative(resource, amount);

        // Check whether the resource can be inserted from this location.
        ExampleObject current = this.object;
        if (current.count() == 0 || (current.id() == resource.id() && current.flags().equals(resource.flags()) && this.isValid(index, resource))) {
            // Compute the amount to insert.
            int insertedAmount = Math.min(amount, this.getCapacityAsInt(index, resource) - current.count());

            if (insertedAmount > 0) {
                // Update the content.
                if (current.count() == 0) {
                    this.object = new ExampleObject(
                        resource.id(), insertedAmount, new HashMap<>(resource.flags())
                    );
                } else {
                    this.object.setCount(current.count() + insertedAmount);
                }

                // Return the amount inserted.
                return insertedAmount;
            }
        }

        // If not matching, insert nothing.
        return 0;
    }

    @Override
    public int extract(int index, ExampleResource resource, int amount, TransactionContext transaction) {
        // Extracts the contents from the given index, returning the amount taken out.

        // Validate arguments.
        Objects.checkIndex(index, size());
        TransferPreconditions.checkNonEmptyNonNegative(resource, amount);

        // Check whether the resource can be extracted from this location.
        ExampleObject current = this.object;
        if (current.id() == resource.id() && current.flags().equals(resource.flags())) {
            // Compute the amount to extract.
            int extracted = Math.min(current.count(), amount);

            if (extracted > 0) {
                // Update the content.
                this.object.setCount(current.count() - extracted);

                // Return the amount extracted.
                return extracted;
            }
        }

        // If not matching, extract nothing.
        return 0;
    }
}

或创建 StacksResourceHandler:

public class ExampleStacksResourceHandler extends StacksResourceHandler<ExampleObject, ExampleResource> {

    public ExampleStacksResourceHandler(int size) {
        super(size, ExampleObject.EMPTY, ExampleObject.CODEC);
    }

    public ExampleStacksResourceHandler(NonNullList<ExampleObject> objects) {
        super(objects, ExampleObject.EMPTY, ExampleObject.CODEC);
    }

    @Override
    public ExampleResource getResourceFrom(ExampleObject object) {
        // Constructs the resource from the content.
        return new ExampleResource(object);
    }

    @Override
    public int getAmountFrom(ExampleObject object) {
        // Gets the amount from the content.
        return object.count();
    }

    @Override
    protected ExampleObject getStackFrom(ExampleResource resource, int amount) {
        // Create the content from its resource.
        return new ExampleObject(resource.id(), amount, new HashMap<>(resource.flags()));
    }

    @Override
    protected int getCapacity(int index, ExampleResource resource) {
        // The capacity at a given index for the stored resource.
        return Integer.MAX_VALUE;
    }

    @Override
    protected ExampleObject copyOf(ExampleObject object) {
        // Constructs a copy of the content.
        return new ExampleObject(object.id(), object.count(), new HashMap<>(object.flags()));
    }

    @Override
    public boolean matches(ExampleObject object, ExampleResource resource) {
        // Check if an object matches the stored resource.
        return object.id() == resource.id() && object.flags().equals(resource.flags());
    }
}

建议 NeoForge 还提供 ResourceStacksResourceHandler,以 ResourceStack 保存内容,适用于资源实现本身就是容器中实际对象的情况。

能量处理器

EnergyHandler 是精简的资源处理器,只包含一个保存 long 的位置。因此只提供容量和已有数量查询,分别使用 getCapacityAsLong/getCapacityAsInt、getAmountAsLong/getAmountAsInt。insert 和 extract 不接受索引,也不需要 Resource,因为只有一个位置,底层数据也是基本类型。

能量处理器同样有不同实现。最常用的是 SimpleEnergyHandler,提供基础实现和插入、取出速率限制。

// Create an energy handler.
EnergyHandler energy = new SimpleEnergyHandler(1000);

物品访问

ItemAccess 也是精简的资源处理器,访问特定存储位置中的单种物品。通常用于物品能力,修改能力所在的物品。它只提供 getResource 和 getAmount 查询,insert、extract 也不接受索引。物品还可能保存数据,因此它提供 getCapability,访问以 ItemAccess 为上下文的 ItemCapability。

ItemAccess 也有不同实现。最常见的是包装玩家某个物品栏槽位的 PlayerItemAccess,以及包装资源处理器某个索引的 HandlerItemAccess。

// Create an item access for some location.
// Assume we have some `Player` player.
ItemAccess access = ItemAccess.forPlayerInteraction(player, InteractionHand.MAIN_HAND);

// Get the data about the referenced item
ItemResource item = access.getResource();
int count = access.getAmount();

// Gets the item capability on the stack.
// For example, if the item is a fluid container:
ResourceHandler<FluidResource> fluidContainer = access.getCapability(Capabilities.Fluid.ITEM);

在处理器之间转移

Transaction 协调资源在处理器之间的转移,包括插入和取出。操作完毕并调用 Transaction#commit 后,才将转移视为有效或完成。

Transaction 实现 AutoCloseable,因此标准做法是在 try-with-resources 块中调用 Transaction#openRoot:

// Let's assume we have two `ResourceHandler<ItemResource>`s apples, emeralds.

// Open the transaction.
try (Transaction tx = Transaction.openRoot()) {
    // Insert and extract from resource handlers.
    ItemResource appleResource = ItemResource.of(Items.APPLE);
    ItemResource emeraldResource = ItemResource.of(Items.EMERALD);

    int numOfApples = apples.extract(appleResource, 5, tx);
    int numOfEmeralds = emeralds.extract(emeraldResource, 1, tx);

    // Perform any validation necessary.
    if (numOfApples == 5 && numOfEmeralds == 1) {
        numOfEmeralds = apples.insert(emeraldResource, numOfEmeralds, tx);
        numOfApples = emeralds.insert(appleResource, numOfApples, tx);

        if (numOfApples == 5 && numOfEmeralds == 1) {
            // Mark the transaction as complete.
            tx.commit();
        }
    }
}

建议

ResourceHandlerUtil 提供多个便捷方法,用于检查处理器状态或在处理器间转移。例如,上面的绿宝石换苹果可以简化为:

// Let's assume we have two `ResourceHandler<ItemResource>`s apples, emeralds.

// Open the transaction.
try (Transaction tx = Transaction.openRoot()) {
    // Insert and extract from resource handlers.
    ItemResource appleResource = ItemResource.of(Items.APPLE);
    ItemResource emeraldResource = ItemResource.of(Items.EMERALD);

    int applesMoved = ResourceHandlerUtil.moveStacking(
        // Moving from apples -> emeralds.
        apples, emeralds,
        // Checks what resource(s) to move.
        appleResource::equals,
        // The number of the resource to move.
        5,
        // The transaction context.
        tx
    );
    int emeraldsMoved = ResourceHandlerUtil.moveStacking(
        emeralds, apples, emeraldResource::equals, 1, tx
    );;

    // Perform any validation necessary.
    if (applesMoved == 5 && emeraldsMoved == 1) {
        // Mark the transaction as complete.
        tx.commit();
    }
}

需要同时处理多个事务时,可以通过 Transaction#open 在现有事务中嵌套新事务。

// Open the transaction.
try (Transaction tx = Transaction.openRoot()) {
    // Transaction A
    try (Transaction atx = Transaction.open(tx)) {
        // Insert and extract from resource handlers.

        // ...

        // Mark as complete.
        atx.commit();
    }

    // Transaction B
    try (Transaction btx = Transaction.open(tx)) {
        // Insert and extract from resource handlers.

        // ...

        // Maybe this one was invalid, so don't mark as complete.
    }

    // Mark the root transaction as successful such that the successful
    // inner transactions are completed.
    tx.commit();
}

创建快照

单靠 Transaction#commit 不会自动完成回滚。如果处理器不支持快照,插入和取出即使未成功完成整个转移,也会永久生效。所需行为应是:只有提交才保留修改,否则撤销。

SnapshotJournal<T> 用于实现这一点。修改处理器内容前,它保存当前状态的 T 快照;事务成功时释放快照,否则恢复原状态。至少要实现 createSnapshot,创建保存状态,以及 revertToSnapshot,恢复指定状态。如果处理器变化后需要通知或更新底层对象,还可以重写 onRootCommit。

NeoForge 所有资源处理器实现都以某种方式使用快照日志,可能直接在处理器上实现,也可能保存为字段。只有创建新处理器时,才需要自行实现。

// We can use the stored object as the snapshot value since we only ever
// need to keep track of one index.
public class ExampleResourceHandler extends SnapshotJournal<ExampleObject> implements ResourceHandler<ExampleResource> {

    private ExampleObject object;

    public ExampleResourceHandler(ExampleObject object) {
        // ...
    }
    
    // ...

    @Override
    protected ExampleObject createSnapshot() {
        // Create a snapshot of the object.
        // This should be immutable.
        ExampleObject original = this.object;
        this.object = new ExampleObject(
            original.id(), original.count(), ImmutableMap.copyOf(original.flags())
        );
        return original;
    }

    @Override
    protected void revertToSnapshot(ExampleObject snapshot) {
        // Reverts the state of the handler to the snapshot.
        this.object = snapshot;
    }

    // We need to update the insert and extract methods to make snapshots before
    // every modification.

    @Override
    public int insert(int index, ExampleResource resource, int amount, TransactionContext transaction) {
        // Inserts the resource into the given index, returning the amount put in.

        // Validate arguments.
        Objects.checkIndex(index, size());
        TransferPreconditions.checkNonEmptyNonNegative(resource, amount);

        // Check whether the resource can be inserted from this location.
        ExampleObject current = this.object;
        if (current.count() == 0 || (current.id() == resource.id() && current.flags().equals(resource.flags()) && this.isValid(index, resource))) {
            // Compute the amount to insert.
            int insertedAmount = Math.min(amount, this.getCapacityAsInt(index, resource) - current.count());

            if (insertedAmount > 0) {
                // Snapshot the handler before modifying the contents.
                this.updateSnapshots(transaction);

                // Update the content.
                if (current.count() == 0) {
                    this.object = new ExampleObject(
                        resource.id(), insertedAmount, new HashMap<>(resource.flags())
                    );
                } else {
                    this.object.setCount(current.count() + insertedAmount);
                }

                // Return the amount inserted.
                return insertedAmount;
            }
        }

        // If not matching, insert nothing.
        return 0;
    }

    @Override
    public int extract(int index, ExampleResource resource, int amount, TransactionContext transaction) {
        // Extracts the contents from the given index, returning the amount taken out.

        // Validate arguments.
        Objects.checkIndex(index, size());
        TransferPreconditions.checkNonEmptyNonNegative(resource, amount);

        // Check whether the resource can be extracted from this location.
        ExampleObject current = this.object;
        if (current.id() == resource.id() && current.flags().equals(resource.flags())) {
            // Compute the amount to extract.
            int extracted = Math.min(current.count(), amount);

            if (extracted > 0) {
                // Snapshot the handler before modifying the contents.
                this.updateSnapshots(transaction);

                // Update the content.
                this.object.setCount(current.count() - extracted);

                // Return the amount extracted.
                return extracted;
            }
        }

        // If not matching, extract nothing.
        return 0;
    }
}

加入快照后,事务才能正确维护容器状态:

// Let's assume we have two `ResourceHandler<ExampleResource>`s exampleA, exampleB.

// Open the transaction.
try (Transaction tx = Transaction.openRoot()) {
    // Insert and extract from resource handlers
    ExampleResource resource = new ExampleResource(new ExampleObject(0, 1, Map.of()));

    // Try to extract and insert the desired resource
    if (exampleA.extract(resource, 1, tx) == 1 && exampleB.insert(resource, 1, tx) == 1) {
        // If successful, commit the transaction to make the change permanent.
        tx.commit();
    }

    // Otherwise, the transaction is aborted and the two handlers will revert their
    // contents to before the transaction occurred.
}

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。