diff --git a/fabric-data-attachment-api-v1/build.gradle b/fabric-data-attachment-api-v1/build.gradle
new file mode 100644
index 000000000..10d208c1f
--- /dev/null
+++ b/fabric-data-attachment-api-v1/build.gradle
@@ -0,0 +1,11 @@
+version = getSubprojectVersion(project)
+
+moduleDependencies(project, [
+ 'fabric-api-base',
+ ':fabric-entity-events-v1',
+ ':fabric-object-builder-api-v1'
+])
+
+testDependencies(project, [
+ ':fabric-lifecycle-events-v1'
+])
diff --git a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java
new file mode 100644
index 000000000..093082c14
--- /dev/null
+++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPlayNetworkHandlerMixin.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.attachment.client;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket;
+
+import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
+
+@Mixin(ClientPlayNetworkHandler.class)
+abstract class ClientPlayNetworkHandlerMixin {
+ @WrapOperation(
+ method = "onPlayerRespawn",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;init()V")
+ )
+ private void copyAttachmentsOnClientRespawn(ClientPlayerEntity newPlayer, Operation For finer control over the attachment type and its properties, use {@link AttachmentRegistry#builder()} to
+ * get a {@link Builder} instance. It is encouraged for {@link A} to be an immutable data type, such as a primitive type
+ * or an immutable record. Otherwise, one must be very careful, as attachments must not share any mutable state.
+ * As an example, for a (mutable) list/array attachment type,
+ * the initializer should create a new independent instance each time it is called. Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerWorld} and {@link WorldChunk} via mixin. 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:
+ *
+ *
+ *
+ * {@code
+ * AttachmentType
+ *
+ * Note about {@link BlockEntity} targets: by default, many block entities use their NBT to synchronize with the client. + * That would mean persistent attachments are automatically synced with the client for those block entities. As this is + * undesirable behavior, the API completely removes attachments from the result of {@link BlockEntity#toInitialChunkDataNbt()}, + * which takes care of all vanilla types. However, modded block entities may be coded differently, so be wary of this + * when attaching data to modded block entities. + *
+ */ +@ApiStatus.Experimental +@ApiStatus.NonExtendable +public interface AttachmentTarget { + String NBT_ATTACHMENT_KEY = "fabric:attachments"; + + /** + * Gets the data associated with the given {@link AttachmentType}, or {@code null} if it doesn't yet exist. + * + * @param type the attachment type + * @param the type of the data + * @return the attached data + */ + @Nullable + default A getAttached(AttachmentType type) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + /** + * Gets the data associated with the given {@link AttachmentType}, throwing a {@link NullPointerException} if it doesn't yet exist. + * + * @param type the attachment type + * @param the type of the data + * @return the attached data + */ + default A getAttachedOrThrow(AttachmentType type) { + return Objects.requireNonNull(getAttached(type), "No value was attached"); + } + + /** + * Gets the data associated with the given {@link AttachmentType}, or initializes it using the provided non-{@code null} + * default value. + * + * @param type the attachment type + * @param defaultValue the fallback default value + * @param the type of the data + * @return the attached data, initialized if originally absent + */ + default A getAttachedOrSet(AttachmentType type, A defaultValue) { + Objects.requireNonNull(defaultValue, "default value cannot be null"); + A attached = getAttached(type); + + if (attached != null) { + return attached; + } else { + setAttached(type, defaultValue); + return defaultValue; + } + } + + /** + * Gets the data associated with the given {@link AttachmentType}, or initializes it using the non-{@code null} result + * of the provided {@link Supplier}. + * + * @param type the attachment type + * @param initializer the fallback initializer + * @param the type of the data + * @return the attached data, initialized if originally absent + */ + default A getAttachedOrCreate(AttachmentType type, Supplier initializer) { + A attached = getAttached(type); + + if (attached != null) { + return attached; + } else { + A initialized = Objects.requireNonNull(initializer.get(), "initializer result cannot be null"); + setAttached(type, initialized); + return initialized; + } + } + + /** + * Specialization of {@link #getAttachedOrCreate(AttachmentType, Supplier)}, but only for attachment types with + * {@link AttachmentType#initializer() initializers}. It will throw an exception if one is not present. + * + * @param type the attachment type + * @param the type of the data + * @return the attached data, initialized if originally absent + */ + default A getAttachedOrCreate(AttachmentType type) { + Supplier init = type.initializer(); + + if (init == null) { + throw new IllegalArgumentException("Single-argument getAttachedOrCreate is reserved for attachment types with default initializers"); + } + + return getAttachedOrCreate(type, init); + } + + /** + * Gets the data associated with the given {@link AttachmentType}, or returns the provided default value if it doesn't exist. + * Unlike {@link #getAttachedOrCreate(AttachmentType, Supplier)}, this doesn't initialize the attachment with the default value. + * + * @param type the attachment type + * @param defaultValue the default value to use as fallback + * @param the type of the attached data + * @return the attached data, or the default value + */ + @Contract("_, !null -> !null") + default A getAttachedOrElse(AttachmentType type, @Nullable A defaultValue) { + A attached = getAttached(type); + return attached == null ? defaultValue : attached; + } + + /** + * Gets the data associated with the given {@link AttachmentType}, or gets the provided default value from the + * provided non-{@code null} supplier if it doesn't exist. The supplier may return {@code null}. + * Unlike {@link #getAttachedOrCreate(AttachmentType, Supplier)}, this doesn't initialize the attachment with the default value. + * + * @param type the attachment type + * @param defaultValue the default value supplier to use as fallback + * @param the type of the attached data + * @return the attached data, or the default value + */ + default A getAttachedOrGet(AttachmentType type, Supplier defaultValue) { + Objects.requireNonNull(defaultValue, "default value supplier cannot be null"); + + A attached = getAttached(type); + return attached == null ? defaultValue.get() : attached; + } + + /** + * Sets the data associated with the given {@link AttachmentType}. Passing {@code null} removes the data. + * + * @param type the attachment type + * @param value the new value + * @param the type of the data + * @return the previous data + */ + @Nullable + default A setAttached(AttachmentType type, @Nullable A value) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + /** + * Tests whether the given {@link AttachmentType} has any associated data. This doesn't create any data, and may return + * {@code false} even for attachment types with an automatic initializer. + * + * @param type the attachment type + * @return whether there is associated data + */ + default boolean hasAttached(AttachmentType> type) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + /** + * Removes any data associated with the given {@link AttachmentType}. Equivalent to calling {@link #setAttached(AttachmentType, Object)} + * with {@code null}. + * + * @param type the attachment type + * @param the type of the data + * @return the previous data + */ + @Nullable + default A removeAttached(AttachmentType type) { + return setAttached(type, null); + } + + /** + * Modifies the data associated with the given {@link AttachmentType}. Functionally the same as calling {@link #getAttached(AttachmentType)}, + * applying the modifier, then calling {@link #setAttached(AttachmentType, Object)} with the result. The modifier + * takes in the currently attached value, or {@code null} if no attachment is present. + * + * @param type the attachment type + * @param modifier the operation to apply to the current data, or to {@code null} if it doesn't exist yet + * @param the type of the data + * @return the previous data + */ + @Nullable + default A modifyAttached(AttachmentType type, UnaryOperator modifier) { + return setAttached(type, modifier.apply(getAttached(type))); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java new file mode 100644 index 000000000..e8d07e028 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.attachment.v1; + +import java.util.function.Supplier; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; + +/** + * An attachment allows "attaching" arbitrary data to various game objects (entities, block entities, worlds and chunks at the moment). + * Use the methods provided in {@link AttachmentRegistry} to create and register attachments. Attachments can + * optionally be made to persist between restarts using a provided {@link Codec}. + * + *While the API places no restrictions on the types of data that can be attached, it is generally encouraged to use + * immutable types. More generally, different attachments must not share mutable state, and it is strongly advised + * for attachments not to hold internal references to their target. See the following note on entity targets.
+ * + *Note on {@link Entity} targets: in several instances, the name needs to copy data from one {@link Entity} to another. + * These are player respawning, mob conversion, return from the End and cross-world entity teleportation. By default, + * attachments are simply copied wholesale, up to {@link #copyOnDeath()}. Since one entity instance is discarded, + * an attachment that keeps a reference to an {@link Entity} instance can and will break unexpectedly. If, + * for whatever reason, keeping to reference to the target entity is absolutely necessary, be sure to use + * {@link ServerPlayerEvents#COPY_FROM}, {@link ServerEntityWorldChangeEvents#AFTER_ENTITY_CHANGE_WORLD} + * and a mixin into {@link MobEntity#convertTo(EntityType, boolean)} to implement custom copying logic.
+ * + * @param type of the attached data. It is encouraged for this to be an immutable type. + */ +@ApiStatus.NonExtendable +@ApiStatus.Experimental +public interface AttachmentType { + /** + * @return the identifier that uniquely identifies this attachment + */ + Identifier identifier(); + + /** + * An optional {@link Codec} used for reading and writing attachments to NBT for persistence. + * + * @return the persistence codec, may be null + */ + @Nullable + Codec persistenceCodec(); + + /** + * @return whether the attachments persist across server restarts + */ + default boolean isPersistent() { + return persistenceCodec() != null; + } + + /** + * If an object has no value associated to an attachment, + * this initializer is used to create a non-{@code null} starting value. + * + *It is encouraged for {@link A} to be an immutable data type, such as a primitive type + * or an immutable record.
+ * + *Otherwise, one must be very careful, as attachments must not share any mutable state. + * As an example, for a (mutable) list/array attachment type, + * the initializer should create a new independent instance each time it is called.
+ * + * @return the initializer for this attachment + */ + @Nullable + Supplier initializer(); + + /** + * @return whether the attachments should persist after an entity dies, for example when a player respawns or + * when a mob is converted (e.g. zombie → drowned) + */ + boolean copyOnDeath(); +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java new file mode 100644 index 000000000..92e848d6a --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentEntrypoint.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.attachment; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +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(oldPlayer, newPlayer, !alive) + ); + ServerEntityWorldChangeEvents.AFTER_ENTITY_CHANGE_WORLD.register(((originalEntity, newEntity, origin, destination) -> + AttachmentTargetImpl.copyOnRespawn(originalEntity, newEntity, false)) + ); + // using the corresponding player event is unnecessary as no new instance is created + ServerLivingEntityEvents.MOB_CONVERSION.register((previous, converted, keepEquipment) -> + AttachmentTargetImpl.copyOnRespawn(previous, converted, true) + ); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentPersistentState.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentPersistentState.java new file mode 100644 index 000000000..badfc9546 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentPersistentState.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.attachment; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.PersistentState; + +/** + * Backing storage for server-side world attachments. + * Thanks to custom {@link #isDirty()} logic, the file is only written if something needs to be persisted. + */ +public class AttachmentPersistentState extends PersistentState { + public static final String ID = "fabric_attachments"; + private final AttachmentTargetImpl worldTarget; + private final boolean wasSerialized; + + public AttachmentPersistentState(ServerWorld world) { + this.worldTarget = (AttachmentTargetImpl) world; + this.wasSerialized = worldTarget.fabric_hasPersistentAttachments(); + } + + public static AttachmentPersistentState read(ServerWorld world, @Nullable NbtCompound nbt) { + ((AttachmentTargetImpl) world).fabric_readAttachmentsFromNbt(nbt); + return new AttachmentPersistentState(world); + } + + @Override + public boolean isDirty() { + // Only write data if there are attachments, or if we previously wrote data. + return wasSerialized || worldTarget.fabric_hasPersistentAttachments(); + } + + @Override + public NbtCompound writeNbt(NbtCompound nbt) { + worldTarget.fabric_writeAttachmentsToNbt(nbt); + return nbt; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java new file mode 100644 index 000000000..d2be65d7c --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.attachment; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; + +public final class AttachmentRegistryImpl { + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-data-attachment-api-v1"); + private static final Map