more advanced copy mechanics

- attachments require an EntityCopyHandler to be copied across entities
- a copy handler is automatically derived if there's a codec
- updated javadoc for chunk and BE targets
This commit is contained in:
Syst3ms 2024-01-03 00:42:34 +01:00
parent 40ba2a4b44
commit 3f53b554fb
10 changed files with 233 additions and 63 deletions

View file

@ -102,24 +102,90 @@ public final class AttachmentRegistry {
/**
* A builder for creating {@link AttachmentType}s with finer control over their properties.
*
* <p>Note on entity attachments: sometimes, the game needs to copy data between two different entity instances.
* This happens for example on player respawn, when a mob converts to another type, or when the player returns from the End.
* Since one entity instance is discarded, it is imperative that the attached data doesn't hold a reference to the old instance.</p>
* <ul>
* <li>If a {@link Codec codec} is provided using {@link #codec(Codec)}, it will automatically be used for copying data.</li>
* <li>If finer control is desired, a {@link AttachmentType.EntityCopyHandler custom copy handler} can be specified using {@link #entityCopyHandler(AttachmentType.EntityCopyHandler)}.</li>
* <li>If neither are provided, <i>no attempt at copy will be made</i>, and attached data <b>will be lost</b>
* in the situations outlined above. This can sometimes be useful for even finer control over copying, but
* is generally undesirable for attachment types used on entities.</li>
* </ul>
*
* @param <A> the type of the attached data
* @see #copyOnDeath()
* @see #copyOnDeath(Codec)
* @see #entityCopyHandler(AttachmentType.EntityCopyHandler)
*/
public interface Builder<A> {
/**
* Declares that attachments should persist between server restarts, using the provided {@link Codec} for
* (de)serialization.
* Declares that attachments corresponding to this type should persist between server restarts,
* using the provided {@link Codec} for (de)serialization.
*
* <p>A shorthand for {@code persistent().codec(codec)}, cannot be used in conjunction with {@link #copyOnDeath(Codec)},
* as {@link #codec(Codec)} can only be called once.</p>
*
* @param codec the codec used for (de)serialization
* @return the builder
* @see #codec(Codec)
*/
default Builder<A> persistent(Codec<A> codec) {
return persistent().codec(codec);
}
/**
* Declares that attachments corresponding to this type should persist between server restarts. A codec must be
* declared using {@link #codec(Codec)} at some point, or {@link #buildAndRegister(Identifier)} will fail.
*
* @return the builder
*/
Builder<A> persistent();
/**
* Declares that when an entity dies and respawns in some way, the attachments corresponding to this type
* should be copied, using the provided {@link Codec} to copy data
* between entity instances. This is used either when a player dies and respawns, or when a mob converts to another
* (for example, zombie drowned, or zombie villager villager).
*
* <p>A shorthand for {@code copyEntityAttachments().codec(codec)}, and cannot be used in conjunction with {@link #persistent(Codec)},
* as {@link #codec(Codec)} can only be called once.</p>
*
* @param codec a codec
* @return the builder
* @see #codec(Codec)
*/
default Builder<A> copyOnDeath(Codec<A> codec) {
return copyOnDeath().codec(codec);
}
/**
* Declares that when a player dies and respawns, the attachments corresponding to this type should remain.
* When using this method, some method for attachment copying must be specified as explained in the description
* of {@link Builder}, otherwise {@link #buildAndRegister(Identifier)} will fail.
*
* @return the builder
* @see Builder
*/
Builder<A> copyOnDeath();
/**
* Sets the {@link AttachmentType.EntityCopyHandler} for this attachment type, used when copying attachments between
* entity instances.
*
* @param copyHandler the copy handler
* @return the builder
*/
Builder<A> entityCopyHandler(AttachmentType.EntityCopyHandler<A> copyHandler);
/**
* Sets the codec used for (de)serialization of this attachment type. Must only be called once during the
* builder's existence.
*
* @param codec the codec used for (de)serialization
* @return the builder
*/
Builder<A> persistent(Codec<A> codec);
/**
* Declares that when a player dies and respawns, the attachments corresponding of this type should remain.
*
* @return the builder
*/
Builder<A> copyOnPlayerRespawn();
Builder<A> codec(Codec<A> codec);
/**
* Sets the default initializer for this attachment type. The initializer will be called by

View file

@ -26,12 +26,26 @@ import org.jetbrains.annotations.Nullable;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.entity.Entity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.WorldChunk;
/**
* Marks all objects on which data can be attached using {@link AttachmentType}s.
*
* <p>Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerWorld} and {@link WorldChunk} via mixin.</p>
*
* <p>Note about {@link BlockEntity} and {@link WorldChunk} targets: these objects need to be notified of changes to their
* state (using {@link BlockEntity#markDirty()} and {@link Chunk#setNeedsSaving(boolean)} respectively), otherwise the modifications will not take effect properly.
* The {@link #setAttached(AttachmentType, Object)} method handles this automatically, but this needs to be done manually
* when attached data is mutable, for example:
* <pre>{@code
* AttachmentType<MutableInt> MUTABLE_ATTACHMENT_TYPE = ...;
* BlockEntity be = ...;
* MutableInt data = be.getAttachedOrCreate(MUTABLE_ATTACHMENT_TYPE);
* data.setValue(10);
* be.markDirty(); // Required because we are not using setAttached
* }</pre>
* </p>
*/
@ApiStatus.Experimental
@ApiStatus.NonExtendable

View file

@ -22,6 +22,7 @@ import com.mojang.serialization.Codec;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.entity.Entity;
import net.minecraft.util.Identifier;
/**
@ -41,19 +42,17 @@ public interface AttachmentType<A> {
Identifier identifier();
/**
* An optional {@link Codec} used for reading and writing attachments to NBT for persistence.
* An optional {@link Codec} used for reading and writing attachments to NBT for persistence and copying.
*
* @return the persistence codec, may be null
* @return the persistence codec, may be {@code null}
*/
@Nullable
Codec<A> persistenceCodec();
Codec<A> codec();
/**
* @return whether the attachments persist across server restarts
*/
default boolean isPersistent() {
return persistenceCodec() != null;
}
boolean persistent();
/**
* If an object has no value associated to an attachment,
@ -66,13 +65,40 @@ public interface AttachmentType<A> {
* As an example, for a (mutable) list/array attachment type,
* the initializer should create a new independent instance each time it is called.</p>
*
* @return the initializer for this attachment
* @return the initializer for this attachment, may be {@code null}
*/
@Nullable
Supplier<A> initializer();
/**
* @return the {@link EntityCopyHandler} of this attachment type, may be {@code null}
*/
@Nullable
AttachmentType.EntityCopyHandler<A> entityCopyHandler();
/**
* @return whether the attachments should persist after a player's death and respawn
*/
boolean copyOnPlayerRespawn();
boolean copyOnDeath();
/**
* A functional interface to handle copying attachment data from an old entity instance to a new one, for example
* during player respawn, entity conversion, or when an entity is teleported between worlds.
*
* <p>It is <i>imperative</i> that the data attached to the new entity doesn't hold a reference to the old entity,
* as that can and will break in unexpected ways.</p>
*/
@FunctionalInterface
interface EntityCopyHandler<T> {
/**
* Copies an attachment's data from an old entity instance (which will be discarded) to a new one. The data returned
* will be attached to the new entity and <i>must not</i> hold a reference to the old one.
*
* @param original the previously attached data
* @param oldEntity the entity to copy the attachment from
* @param newEntity the entity to copy the attachment to
* @return the new data for this attachment. <i>Must not</i> hold a reference to {@code oldEntity}.
*/
T copyAttachment(T original, Entity oldEntity, Entity newEntity);
}
}

View file

@ -22,8 +22,6 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
public class AttachmentEntrypoint implements ModInitializer {
@Override
public void onInitialize() {
ServerPlayerEvents.COPY_FROM.register((oldPlayer, newPlayer, alive) ->
AttachmentTargetImpl.copyOnRespawn((AttachmentTargetImpl) oldPlayer, (AttachmentTargetImpl) newPlayer, alive)
);
ServerPlayerEvents.COPY_FROM.register(AttachmentTargetImpl::copyEntityAttachments);
}
}

View file

@ -27,6 +27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.util.Identifier;
import net.minecraft.util.dynamic.RuntimeOps;
import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry;
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
@ -43,6 +44,14 @@ public final class AttachmentRegistryImpl {
}
}
private static <T> AttachmentType.EntityCopyHandler<T> copyHandlerFromCodec(Codec<T> codec) {
return (original, oldEntity, newEntity) -> codec.encodeStart(RuntimeOps.INSTANCE, original)
.flatMap(s -> codec.decode(RuntimeOps.INSTANCE, s))
.result()
.orElseThrow()
.getFirst();
}
@Nullable
public static AttachmentType<?> get(Identifier id) {
return attachmentRegistry.get(id);
@ -56,20 +65,43 @@ public final class AttachmentRegistryImpl {
@Nullable
private Supplier<A> defaultInitializer = null;
@Nullable
private Codec<A> persistenceCodec = null;
private boolean copyOnPlayerRespawn = false;
private Codec<A> codec = null;
@Nullable
private AttachmentType.EntityCopyHandler<A> copyHandler = null;
private boolean persistent = false;
private boolean copyOnDeath = false;
@Override
public AttachmentRegistry.Builder<A> persistent(Codec<A> codec) {
Objects.requireNonNull(codec, "codec cannot be null");
this.persistenceCodec = codec;
public AttachmentRegistry.Builder<A> persistent() {
this.persistent = true;
return this;
}
@Override
public AttachmentRegistry.Builder<A> copyOnPlayerRespawn() {
this.copyOnPlayerRespawn = true;
public AttachmentRegistry.Builder<A> copyOnDeath() {
this.copyOnDeath = true;
return this;
}
@Override
public AttachmentRegistry.Builder<A> entityCopyHandler(AttachmentType.EntityCopyHandler<A> copyHandler) {
Objects.requireNonNull(copyHandler, "entity copy handler cannot be null");
this.copyHandler = copyHandler;
return this;
}
@Override
public AttachmentRegistry.Builder<A> codec(Codec<A> codec) {
Objects.requireNonNull(codec, "codec cannot be null");
if (this.codec != null) {
throw new IllegalArgumentException(
"A codec was already set for this attachment type. Declare it once using the Builder#codec() method instead"
);
}
this.codec = codec;
return this;
}
@ -83,7 +115,19 @@ public final class AttachmentRegistryImpl {
@Override
public AttachmentType<A> buildAndRegister(Identifier id) {
var attachment = new AttachmentTypeImpl<>(id, defaultInitializer, persistenceCodec, copyOnPlayerRespawn);
if (codec == null && persistent) {
throw new IllegalArgumentException("Persistence was enabled, but no codec was provided");
}
if (codec != null && copyHandler == null) {
this.copyHandler = copyHandlerFromCodec(codec);
}
if (copyOnDeath && copyHandler == null) {
throw new IllegalArgumentException("Copy on death was enabled, but no way of copying attachments was provided");
}
var attachment = new AttachmentTypeImpl<>(id, defaultInitializer, codec, copyHandler, persistent, copyOnDeath);
register(id, attachment);
return attachment;
}

View file

@ -45,9 +45,10 @@ public class AttachmentSerializingImpl {
for (Map.Entry<AttachmentType<?>, ?> entry : attachments.entrySet()) {
AttachmentType<?> type = entry.getKey();
Codec<Object> codec = (Codec<Object>) type.persistenceCodec();
if (codec != null) {
if (type.persistent()) {
Codec<Object> codec = (Codec<Object>) type.codec();
// non-nullity enforced by builder API
codec.encodeStart(NbtOps.INSTANCE, entry.getValue())
.get()
.ifRight(partial -> {
@ -75,10 +76,10 @@ public class AttachmentSerializingImpl {
continue;
}
Codec<?> codec = type.persistenceCodec();
if (codec != null) {
codec.parse(NbtOps.INSTANCE, compound.get(key))
if (type.persistent()) {
// non-nullity enforced by builder API
type.codec()
.parse(NbtOps.INSTANCE, compound.get(key))
.get()
.ifRight(partial -> {
LOGGER.warn("Couldn't deserialize attachment " + type.identifier() + ", skipping. Error:");
@ -100,7 +101,7 @@ public class AttachmentSerializingImpl {
}
for (AttachmentType<?> type : map.keySet()) {
if (type.isPersistent()) {
if (type.persistent()) {
return true;
}
}

View file

@ -18,6 +18,7 @@ package net.fabricmc.fabric.impl.attachment;
import java.util.Map;
import net.minecraft.entity.Entity;
import net.minecraft.nbt.NbtCompound;
import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget;
@ -25,19 +26,20 @@ import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
public interface AttachmentTargetImpl extends AttachmentTarget {
/**
* Copies entity attachments when it is respawned and a new instance is created.
* Is triggered on player respawn, return from the End, or entity conversion.
* In the first case, only the attachments with {@link AttachmentType#copyOnPlayerRespawn()} will be transferred.
* Copies entity attachments a new instance is created.
* Is triggered on player respawn, entity conversion, or return from the End.
* In the first two cases, only the attachments with {@link AttachmentType#copyOnDeath()} will be copied.
*/
@SuppressWarnings("unchecked")
static void copyOnRespawn(AttachmentTargetImpl original, AttachmentTargetImpl target, boolean alive) {
Map<AttachmentType<?>, ?> attachments = original.fabric_getAttachments();
static void copyEntityAttachments(Entity original, Entity target, boolean alive) {
Map<AttachmentType<?>, ?> attachments = ((AttachmentTargetImpl) original).fabric_getAttachments();
for (Map.Entry<AttachmentType<?>, ?> entry : attachments.entrySet()) {
AttachmentType<Object> type = (AttachmentType<Object>) entry.getKey();
AttachmentType.EntityCopyHandler<Object> copyHandler = type.entityCopyHandler();
if (alive || type.copyOnPlayerRespawn()) {
target.setAttached(type, entry.getValue());
if (copyHandler != null && (alive || type.copyOnDeath())) {
target.setAttached(type, copyHandler.copyAttachment(entry.getValue(), original, target));
}
}
}

View file

@ -28,6 +28,9 @@ import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
public record AttachmentTypeImpl<A>(
Identifier identifier,
@Nullable Supplier<A> initializer,
@Nullable Codec<A> persistenceCodec,
boolean copyOnPlayerRespawn
) implements AttachmentType<A> { }
@Nullable Codec<A> codec,
@Nullable EntityCopyHandler<A> entityCopyHandler,
boolean persistent,
boolean copyOnDeath
) implements AttachmentType<A> {
}

View file

@ -22,6 +22,7 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.mob.MobEntity;
@ -39,6 +40,6 @@ abstract class MobEntityMixin implements AttachmentTargetImpl {
CallbackInfoReturnable<T> cir,
@Local MobEntity converted
) {
AttachmentTargetImpl.copyOnRespawn(this, (AttachmentTargetImpl) converted, true);
AttachmentTargetImpl.copyEntityAttachments((Entity) (Object) this, converted, true);
}
}

View file

@ -147,26 +147,41 @@ public class CommonAttachmentTests {
@Test
void testEntityCopy() {
AttachmentType<Boolean> notCopiedOnRespawn = AttachmentRegistry.create(
new Identifier(MOD_ID, "not_copied_on_respawn")
AttachmentType<Boolean> notCopiedAtAll = AttachmentRegistry.create(
new Identifier(MOD_ID, "not_copied_at_all")
);
AttachmentType<Boolean> copiedOnRespawn = AttachmentRegistry.<Boolean>builder()
.copyOnPlayerRespawn()
.buildAndRegister(new Identifier(MOD_ID, "copied_on_respawn"));
AttachmentType<Boolean> copiedNotOnDeath = AttachmentRegistry.<Boolean>builder()
.codec(Codec.BOOL)
.buildAndRegister(new Identifier(MOD_ID, "copied_not_on_death"));
AttachmentType<Boolean> copiedOnDeath = AttachmentRegistry.<Boolean>builder()
.copyOnDeath(Codec.BOOL)
.buildAndRegister(new Identifier(MOD_ID, "copied_on_death"));
AttachmentType<Boolean> customCopy = AttachmentRegistry.<Boolean>builder()
.copyOnDeath()
.entityCopyHandler((original, oldEntity, newEntity) -> !original) // very bad, only for testing
.buildAndRegister(new Identifier(MOD_ID, "custom_copy"));
Entity original = mock(Entity.class, CALLS_REAL_METHODS);
original.setAttached(notCopiedOnRespawn, true);
original.setAttached(copiedOnRespawn, true);
original.setAttached(notCopiedAtAll, true);
original.setAttached(copiedNotOnDeath, true);
original.setAttached(copiedOnDeath, true);
original.setAttached(customCopy, true);
Entity respawnTarget = mock(Entity.class, CALLS_REAL_METHODS);
Entity nonRespawnTarget = mock(Entity.class, CALLS_REAL_METHODS);
Entity nonDeathTarget = mock(Entity.class, CALLS_REAL_METHODS);
AttachmentTargetImpl.copyEntityAttachments(original, nonDeathTarget, true);
AttachmentTargetImpl.copyOnRespawn((AttachmentTargetImpl) original, (AttachmentTargetImpl) respawnTarget, false);
AttachmentTargetImpl.copyOnRespawn((AttachmentTargetImpl) original, (AttachmentTargetImpl) nonRespawnTarget, true);
assertTrue(respawnTarget.hasAttached(copiedOnRespawn));
assertFalse(respawnTarget.hasAttached(notCopiedOnRespawn));
assertTrue(nonRespawnTarget.hasAttached(copiedOnRespawn));
assertTrue(nonRespawnTarget.hasAttached(notCopiedOnRespawn));
assertFalse(nonDeathTarget.hasAttached(notCopiedAtAll));
assertEquals(true, nonDeathTarget.getAttached(copiedNotOnDeath));
assertEquals(true, nonDeathTarget.getAttached(copiedOnDeath));
assertEquals(false, nonDeathTarget.getAttached(customCopy));
Entity deathTarget = mock(Entity.class, CALLS_REAL_METHODS);
AttachmentTargetImpl.copyEntityAttachments(nonDeathTarget, deathTarget, false);
assertFalse(deathTarget.hasAttached(notCopiedAtAll));
assertFalse(deathTarget.hasAttached(copiedNotOnDeath));
assertEquals(true, deathTarget.getAttached(copiedOnDeath));
assertEquals(true, deathTarget.getAttached(customCopy));
}
@Test