事务
事务是 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。