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 init, PlayerRespawnS2CPacket packet, @Local(ordinal = 0) ClientPlayerEntity oldPlayer) { + /* + * The KEEP_ATTRIBUTES flag is not set on a death respawn, and set in all other cases + */ + AttachmentTargetImpl.copyOnRespawn(oldPlayer, newPlayer, !packet.hasFlag(PlayerRespawnS2CPacket.KEEP_ATTRIBUTES)); + init.call(newPlayer); + } +} diff --git a/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json b/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json new file mode 100644 index 000000000..9634f8d1e --- /dev/null +++ b/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json @@ -0,0 +1,13 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.mixin.attachment.client", + "compatibilityLevel": "JAVA_17", + "mixins": [ + ], + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "ClientPlayNetworkHandlerMixin" + ] +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java new file mode 100644 index 000000000..959c299ac --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java @@ -0,0 +1,150 @@ +/* + * 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.Objects; +import java.util.function.Supplier; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl; + +/** + * Class used to create and register {@link AttachmentType}s. To quickly create {@link AttachmentType}s, use one of the various + * {@code createXXX} methods: + * + * + *

For finer control over the attachment type and its properties, use {@link AttachmentRegistry#builder()} to + * get a {@link Builder} instance.

+ */ +@ApiStatus.Experimental +public final class AttachmentRegistry { + private AttachmentRegistry() { + } + + /** + * Creates and registers an attachment. The data will not be persisted. + * + * @param id the identifier of this attachment + * @param the type of attached data + * @return the registered {@link AttachmentType} instance + */ + public static AttachmentType create(Identifier id) { + Objects.requireNonNull(id, "identifier cannot be null"); + + return AttachmentRegistry.builder().buildAndRegister(id); + } + + /** + * Creates and registers an attachment, that will be automatically initialized with a default value + * when an attachment does not exist on a given target, using {@link AttachmentTarget#getAttachedOrCreate(AttachmentType)}. + * + * @param id the identifier of this attachment + * @param initializer the initializer used to provide a default value + * @param the type of attached data + * @return the registered {@link AttachmentType} instance + */ + public static AttachmentType createDefaulted(Identifier id, Supplier initializer) { + Objects.requireNonNull(id, "identifier cannot be null"); + Objects.requireNonNull(initializer, "initializer cannot be null"); + + return AttachmentRegistry.builder() + .initializer(initializer) + .buildAndRegister(id); + } + + /** + * Creates and registers an attachment, that will persist across server restarts. + * + * @param id the identifier of this attachment + * @param codec the codec used for (de)serialization + * @param the type of attached data + * @return the registered {@link AttachmentType} instance + */ + public static AttachmentType createPersistent(Identifier id, Codec codec) { + Objects.requireNonNull(id, "identifier cannot be null"); + Objects.requireNonNull(codec, "codec cannot be null"); + + return AttachmentRegistry.builder().persistent(codec).buildAndRegister(id); + } + + /** + * Creates a {@link Builder}, that gives finer control over the attachment's properties. + * + * @param the type of the attached data + * @return a {@link Builder} instance + */ + public static Builder builder() { + return AttachmentRegistryImpl.builder(); + } + + /** + * A builder for creating {@link AttachmentType}s with finer control over their properties. + * + * @param the type of the attached data + */ + @ApiStatus.NonExtendable + public interface Builder { + /** + * Declares that attachments should persist between server restarts, using the provided {@link Codec} for + * (de)serialization. + * + * @param codec the codec used for (de)serialization + * @return the builder + */ + Builder persistent(Codec codec); + + /** + * Declares that when a player dies and respawns, the attachments corresponding of this type should remain. + * + * @return the builder + */ + Builder copyOnDeath(); + + /** + * Sets the default initializer for this attachment type. The initializer will be called by + * {@link AttachmentTarget#getAttachedOrCreate(AttachmentType)} to automatically initialize attachments that + * don't yet exist. It must not return {@code null}. + * + *

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.

+ * + * @param initializer the initializer + * @return the builder + */ + Builder
initializer(Supplier initializer); + + /** + * Builds and registers the {@link AttachmentType}. + * + * @param id the attachment's identifier + * @return the built and registered {@link AttachmentType} + */ + AttachmentType buildAndRegister(Identifier id); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java new file mode 100644 index 000000000..3cdf07dfc --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java @@ -0,0 +1,230 @@ +/* + * 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.Objects; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +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. + * + *

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 MUTABLE_ATTACHMENT_TYPE = ...;
+ * BlockEntity be = ...;
+ * MutableType data = be.getAttachedOrCreate(MUTABLE_ATTACHMENT_TYPE);
+ * data.mutate();
+ * be.markDirty(); // Required because we are not using setAttached
+ * }
+ *

+ * + *

+ * 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> attachmentRegistry = new HashMap<>(); + + public static void register(Identifier id, AttachmentType attachmentType) { + AttachmentType existing = attachmentRegistry.put(id, attachmentType); + + if (existing != null) { + LOGGER.warn("Encountered duplicate type registration for id " + id); + } + } + + @Nullable + public static AttachmentType get(Identifier id) { + return attachmentRegistry.get(id); + } + + public static AttachmentRegistry.Builder builder() { + return new BuilderImpl<>(); + } + + public static class BuilderImpl implements AttachmentRegistry.Builder { + @Nullable + private Supplier defaultInitializer = null; + @Nullable + private Codec persistenceCodec = null; + private boolean copyOnDeath = false; + + @Override + public AttachmentRegistry.Builder persistent(Codec codec) { + Objects.requireNonNull(codec, "codec cannot be null"); + + this.persistenceCodec = codec; + return this; + } + + @Override + public AttachmentRegistry.Builder copyOnDeath() { + this.copyOnDeath = true; + return this; + } + + @Override + public AttachmentRegistry.Builder initializer(Supplier initializer) { + Objects.requireNonNull(initializer, "initializer cannot be null"); + + this.defaultInitializer = initializer; + return this; + } + + @Override + public AttachmentType buildAndRegister(Identifier id) { + var attachment = new AttachmentTypeImpl<>(id, defaultInitializer, persistenceCodec, copyOnDeath); + register(id, attachment); + return attachment; + } + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java new file mode 100644 index 000000000..983cbd673 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java @@ -0,0 +1,110 @@ +/* + * 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.IdentityHashMap; +import java.util.Map; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.nbt.NbtOps; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; + +public class AttachmentSerializingImpl { + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-data-attachment-api-v1"); + + @SuppressWarnings("unchecked") + public static void serializeAttachmentData(NbtCompound nbt, @Nullable IdentityHashMap, ?> attachments) { + if (attachments == null) { + return; + } + + var compound = new NbtCompound(); + + for (Map.Entry, ?> entry : attachments.entrySet()) { + AttachmentType type = entry.getKey(); + Codec codec = (Codec) type.persistenceCodec(); + + if (codec != null) { + codec.encodeStart(NbtOps.INSTANCE, entry.getValue()) + .get() + .ifRight(partial -> { + LOGGER.warn("Couldn't serialize attachment " + type.identifier() + ", skipping. Error:"); + LOGGER.warn(partial.message()); + }) + .ifLeft(serialized -> compound.put(type.identifier().toString(), serialized)); + } + } + + nbt.put(AttachmentTarget.NBT_ATTACHMENT_KEY, compound); + } + + public static IdentityHashMap, Object> deserializeAttachmentData(NbtCompound nbt) { + var attachments = new IdentityHashMap, Object>(); + + if (nbt.contains(AttachmentTarget.NBT_ATTACHMENT_KEY, NbtElement.COMPOUND_TYPE)) { + NbtCompound compound = nbt.getCompound(AttachmentTarget.NBT_ATTACHMENT_KEY); + + for (String key : compound.getKeys()) { + AttachmentType type = AttachmentRegistryImpl.get(new Identifier(key)); + + if (type == null) { + LOGGER.warn("Unknown attachment type " + key + " found when deserializing, skipping"); + continue; + } + + Codec codec = type.persistenceCodec(); + + if (codec != null) { + codec.parse(NbtOps.INSTANCE, compound.get(key)) + .get() + .ifRight(partial -> { + LOGGER.warn("Couldn't deserialize attachment " + type.identifier() + ", skipping. Error:"); + LOGGER.warn(partial.message()); + }) + .ifLeft( + deserialized -> attachments.put(type, deserialized) + ); + } + } + } + + return attachments; + } + + public static boolean hasPersistentAttachments(@Nullable IdentityHashMap, ?> map) { + if (map == null) { + return false; + } + + for (AttachmentType type : map.keySet()) { + if (type.isPersistent()) { + return true; + } + } + + return false; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java new file mode 100644 index 000000000..c46c7d7a6 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java @@ -0,0 +1,67 @@ +/* + * 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.Map; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.nbt.NbtCompound; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +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, entity conversion, return from the End or cross-world entity teleportation. + * In the first two cases, only the attachments with {@link AttachmentType#copyOnDeath()} will be transferred. + */ + @SuppressWarnings("unchecked") + static void copyOnRespawn(AttachmentTarget original, AttachmentTarget target, boolean isDeath) { + Map, ?> attachments = ((AttachmentTargetImpl) original).fabric_getAttachments(); + + if (attachments == null) { + return; + } + + for (Map.Entry, ?> entry : attachments.entrySet()) { + AttachmentType type = (AttachmentType) entry.getKey(); + + if (!isDeath || type.copyOnDeath()) { + target.setAttached(type, entry.getValue()); + } + } + } + + @Nullable + default Map, ?> fabric_getAttachments() { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + default void fabric_writeAttachmentsToNbt(NbtCompound nbt) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + default void fabric_readAttachmentsFromNbt(NbtCompound nbt) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + default boolean fabric_hasPersistentAttachments() { + throw new UnsupportedOperationException("Implemented via mixin"); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java new file mode 100644 index 000000000..7a082e14f --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java @@ -0,0 +1,33 @@ +/* + * 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.function.Supplier; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; + +public record AttachmentTypeImpl( + Identifier identifier, + @Nullable Supplier initializer, + @Nullable Codec persistenceCodec, + boolean copyOnDeath +) implements AttachmentType { } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java new file mode 100644 index 000000000..ede2717f2 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java @@ -0,0 +1,106 @@ +/* + * 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; + +import java.util.IdentityHashMap; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.WorldChunk; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin({BlockEntity.class, Entity.class, World.class, WorldChunk.class}) +abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { + @Nullable + private IdentityHashMap, Object> fabric_dataAttachments = null; + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T getAttached(AttachmentType type) { + return fabric_dataAttachments == null ? null : (T) fabric_dataAttachments.get(type); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T setAttached(AttachmentType type, @Nullable T value) { + // Extremely inelegant, but the only alternative is separating out these two mixins and duplicating code + Object thisObject = this; + + if (thisObject instanceof BlockEntity) { + ((BlockEntity) thisObject).markDirty(); + } else if (thisObject instanceof WorldChunk) { + ((Chunk) thisObject).setNeedsSaving(true); + } + + if (value == null) { + if (fabric_dataAttachments == null) { + return null; + } + + T removed = (T) fabric_dataAttachments.remove(type); + + if (fabric_dataAttachments.isEmpty()) { + fabric_dataAttachments = null; + } + + return removed; + } else { + if (fabric_dataAttachments == null) { + fabric_dataAttachments = new IdentityHashMap<>(); + } + + return (T) fabric_dataAttachments.put(type, value); + } + } + + @Override + public boolean hasAttached(AttachmentType type) { + return fabric_dataAttachments != null && fabric_dataAttachments.containsKey(type); + } + + @Override + public void fabric_writeAttachmentsToNbt(NbtCompound nbt) { + AttachmentSerializingImpl.serializeAttachmentData(nbt, fabric_dataAttachments); + } + + @Override + public void fabric_readAttachmentsFromNbt(NbtCompound nbt) { + fabric_dataAttachments = AttachmentSerializingImpl.deserializeAttachmentData(nbt); + } + + @Override + public boolean fabric_hasPersistentAttachments() { + return AttachmentSerializingImpl.hasPersistentAttachments(fabric_dataAttachments); + } + + @Override + public Map, ?> fabric_getAttachments() { + return fabric_dataAttachments; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java new file mode 100644 index 000000000..c1d42fc86 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java @@ -0,0 +1,46 @@ +/* + * 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; + +import org.spongepowered.asm.mixin.Mixin; +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.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; + +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin(BlockEntity.class) +abstract class BlockEntityMixin implements AttachmentTargetImpl { + @Inject( + method = "method_17897", // lambda body in BlockEntity#createFromNbt + at = @At(value = "INVOKE", target = "net/minecraft/block/entity/BlockEntity.readNbt(Lnet/minecraft/nbt/NbtCompound;)V") + ) + private static void readBlockEntityAttachments(NbtCompound nbt, String id, BlockEntity blockEntity, CallbackInfoReturnable cir) { + ((AttachmentTargetImpl) blockEntity).fabric_readAttachmentsFromNbt(nbt); + } + + @Inject( + method = "createNbt", + at = @At("RETURN") + ) + private void writeBlockEntityAttachments(CallbackInfoReturnable cir) { + this.fabric_writeAttachmentsToNbt(cir.getReturnValue()); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityUpdateS2CPacketMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityUpdateS2CPacketMixin.java new file mode 100644 index 000000000..7309285c2 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityUpdateS2CPacketMixin.java @@ -0,0 +1,52 @@ +/* + * 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; + +import java.util.function.Function; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; + +@Mixin(BlockEntityUpdateS2CPacket.class) +public class BlockEntityUpdateS2CPacketMixin { + /* + * Some BEs use their NBT data to sync with client. If nothing is done, that would always sync persistent attachments + * with client, which may be undesirable. To prevent this, we hook into create(BlockEntity) so it uses a getter that + * also removes attachments. Manual sync is still possible by using create(BlockEntity, Function). + */ + @ModifyArg( + method = "create(Lnet/minecraft/block/entity/BlockEntity;)Lnet/minecraft/network/packet/s2c/play/BlockEntityUpdateS2CPacket;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/packet/s2c/play/BlockEntityUpdateS2CPacket;create(Lnet/minecraft/block/entity/BlockEntity;Ljava/util/function/Function;)Lnet/minecraft/network/packet/s2c/play/BlockEntityUpdateS2CPacket;" + ) + ) + private static Function stripPersistentAttachmentData(Function getter) { + return be -> { + NbtCompound nbt = getter.apply(be); + nbt.remove(AttachmentTarget.NBT_ATTACHMENT_KEY); + return nbt; + }; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java new file mode 100644 index 000000000..b5792eb99 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkSerializerMixin.java @@ -0,0 +1,59 @@ +/* + * 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; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import org.spongepowered.asm.mixin.Mixin; +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.nbt.NbtCompound; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.ChunkSerializer; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.poi.PointOfInterestStorage; + +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin(ChunkSerializer.class) +abstract class ChunkSerializerMixin { + @ModifyExpressionValue( + at = @At( + value = "NEW", + target = "net/minecraft/world/chunk/WorldChunk" + ), + method = "deserialize" + ) + private static WorldChunk readChunkAttachments(WorldChunk chunk, ServerWorld world, PointOfInterestStorage poiStorage, ChunkPos chunkPos, NbtCompound nbt) { + ((AttachmentTargetImpl) chunk).fabric_readAttachmentsFromNbt(nbt); + return chunk; + } + + @Inject( + at = @At("RETURN"), + method = "serialize" + ) + private static void writeChunkAttachments(ServerWorld world, Chunk chunk, CallbackInfoReturnable cir) { + if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.LEVELCHUNK) { + ((AttachmentTargetImpl) chunk).fabric_writeAttachmentsToNbt(cir.getReturnValue()); + } + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java new file mode 100644 index 000000000..591aa2e6b --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java @@ -0,0 +1,47 @@ +/* + * 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; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NbtCompound; + +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin(Entity.class) +abstract class EntityMixin implements AttachmentTargetImpl { + @Inject( + at = @At(value = "INVOKE", target = "net/minecraft/entity/Entity.readCustomDataFromNbt(Lnet/minecraft/nbt/NbtCompound;)V"), + method = "readNbt" + ) + private void readEntityAttachments(NbtCompound nbt, CallbackInfo cir) { + this.fabric_readAttachmentsFromNbt(nbt); + } + + @Inject( + at = @At(value = "INVOKE", target = "net/minecraft/entity/Entity.writeCustomDataToNbt(Lnet/minecraft/nbt/NbtCompound;)V"), + method = "writeNbt" + ) + private void writeEntityAttachments(NbtCompound nbt, CallbackInfoReturnable cir) { + this.fabric_writeAttachmentsToNbt(nbt); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java new file mode 100644 index 000000000..7f020853c --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java @@ -0,0 +1,42 @@ +/* + * 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; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.PersistentState; + +import net.fabricmc.fabric.impl.attachment.AttachmentPersistentState; + +@Mixin(ServerWorld.class) +abstract class ServerWorldMixin { + @Inject(at = @At("TAIL"), method = "") + private void createAttachmentsPersistentState(CallbackInfo ci) { + // Force persistent state creation + ServerWorld world = (ServerWorld) (Object) this; + var type = new PersistentState.Type<>( + () -> new AttachmentPersistentState(world), + nbt -> AttachmentPersistentState.read(world, nbt), + null // Object builder API 12.1.0 and later makes this a no-op + ); + world.getPersistentStateManager().getOrCreate(type, AttachmentPersistentState.ID); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/icon.png b/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/icon.png new file mode 100644 index 000000000..2931efbf6 Binary files /dev/null and b/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/icon.png differ diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json new file mode 100644 index 000000000..1ce1c066a --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.mixin.attachment", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "AttachmentTargetsMixin", + "BlockEntityMixin", + "BlockEntityUpdateS2CPacketMixin", + "ChunkSerializerMixin", + "EntityMixin", + "ServerWorldMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json b/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..879ae341c --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 1, + "id": "fabric-data-attachment-api-v1", + "name": "Fabric Data Attachment API (v1)", + "version": "${version}", + "environment": "*", + "license": "Apache-2.0", + "icon": "assets/fabric-data-attachment-api-v1/icon.png", + "contact": { + "homepage": "https://fabricmc.net", + "irc": "irc://irc.esper.net:6667/fabric", + "issues": "https://github.com/FabricMC/fabric/issues", + "sources": "https://github.com/FabricMC/fabric" + }, + "authors": [ + "FabricMC" + ], + "depends": { + "fabricloader": ">=0.15.1", + "fabric-entity-events-v1": "*", + "fabric-object-builder-api-v1": "*" + }, + "description": "Allows conveniently attaching data to existing game objects", + "mixins": [ + "fabric-data-attachment-api-v1.mixins.json", + { + "config": "fabric-data-attachment-api-v1.client.mixins.json", + "environment": "client" + } + ], + "entrypoints": { + "main": [ + "net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint" + ] + }, + "custom": { + "fabric-api:module-lifecycle": "experimental", + "loom:injected_interfaces": { + "net/minecraft/class_2586": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], + "net/minecraft/class_2818": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], + "net/minecraft/class_1297": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], + "net/minecraft/class_3218": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"] + } + } +} diff --git a/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java b/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java new file mode 100644 index 000000000..eb3080a56 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java @@ -0,0 +1,226 @@ +/* + * 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.test.attachment; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.UnaryOperator; + +import com.mojang.serialization.Codec; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import net.minecraft.Bootstrap; +import net.minecraft.SharedConstants; +import net.minecraft.block.entity.BellBlockEntity; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.MarkerEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.chunk.WorldChunk; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.AttachmentPersistentState; +import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +public class CommonAttachmentTests { + private static final String MOD_ID = "example"; + private static final AttachmentType PERSISTENT = AttachmentRegistry.createPersistent( + new Identifier(MOD_ID, "persistent"), + Codec.INT + ); + + @BeforeAll + static void beforeAll() { + SharedConstants.createGameVersion(); + Bootstrap.initialize(); + } + + @Test + void testTargets() { + AttachmentType basic = AttachmentRegistry.create(new Identifier(MOD_ID, "basic_attachment")); + // Attachment targets + /* + * CALLS_REAL_METHODS makes sense here because AttachmentTarget does not refer to anything in the underlying + * class, and it saves us a lot of pain trying to get the regular constructors for ServerWorld and WorldChunk to work. + */ + ServerWorld serverWorld = mock(ServerWorld.class, CALLS_REAL_METHODS); + Entity entity = mock(Entity.class, CALLS_REAL_METHODS); + BlockEntity blockEntity = mock(BlockEntity.class, CALLS_REAL_METHODS); + WorldChunk worldChunk = mock(WorldChunk.class, CALLS_REAL_METHODS); + + for (AttachmentTarget target : new AttachmentTarget[]{serverWorld, entity, blockEntity, worldChunk}) { + testForTarget(target, basic); + } + } + + private void testForTarget(AttachmentTarget target, AttachmentType basic) { + assertFalse(target.hasAttached(basic)); + assertEquals("", target.getAttachedOrElse(basic, "")); + assertNull(target.getAttached(basic)); + + String value = "attached"; + assertEquals(value, target.getAttachedOrSet(basic, value)); + assertTrue(target.hasAttached(basic)); + assertEquals(value, target.getAttached(basic)); + assertDoesNotThrow(() -> target.getAttachedOrThrow(basic)); + + UnaryOperator modifier = s -> s + '_'; + String modified = modifier.apply(value); + target.modifyAttached(basic, modifier); + assertEquals(modified, target.getAttached(basic)); + assertEquals(modified, target.removeAttached(basic)); + assertFalse(target.hasAttached(basic)); + assertThrows(NullPointerException.class, () -> target.getAttachedOrThrow(basic)); + } + + @Test + void testDefaulted() { + AttachmentType defaulted = AttachmentRegistry.createDefaulted( + new Identifier(MOD_ID, "defaulted_attachment"), + () -> 0 + ); + Entity target = mock(Entity.class, CALLS_REAL_METHODS); + + assertFalse(target.hasAttached(defaulted)); + assertEquals(0, target.getAttachedOrCreate(defaulted)); + target.removeAttached(defaulted); + assertFalse(target.hasAttached(defaulted)); + } + + @Test + void testStaticReadWrite() { + AttachmentType dummy = AttachmentRegistry.createPersistent( + new Identifier(MOD_ID, "dummy"), + Codec.DOUBLE + ); + var map = new IdentityHashMap, Object>(); + map.put(dummy, 0.5d); + var fakeSave = new NbtCompound(); + + AttachmentSerializingImpl.serializeAttachmentData(fakeSave, map); + assertTrue(fakeSave.contains(AttachmentTarget.NBT_ATTACHMENT_KEY, NbtElement.COMPOUND_TYPE)); + assertTrue(fakeSave.getCompound(AttachmentTarget.NBT_ATTACHMENT_KEY).contains(dummy.identifier().toString())); + + map = AttachmentSerializingImpl.deserializeAttachmentData(fakeSave); + assertEquals(1, map.size()); + Map.Entry, Object> entry = map.entrySet().stream().findFirst().orElseThrow(); + // in this case the key should be the exact same object + // but in practice this is meaningless because on a dedicated server the JVM restarted + assertEquals(dummy.identifier(), entry.getKey().identifier()); + assertEquals(0.5d, entry.getValue()); + } + + @Test + void testEntityCopy() { + AttachmentType notCopiedOnRespawn = AttachmentRegistry.create( + new Identifier(MOD_ID, "not_copied_on_respawn") + ); + AttachmentType copiedOnRespawn = AttachmentRegistry.builder() + .copyOnDeath() + .buildAndRegister(new Identifier(MOD_ID, "copied_on_respawn")); + + Entity original = mock(Entity.class, CALLS_REAL_METHODS); + original.setAttached(notCopiedOnRespawn, true); + original.setAttached(copiedOnRespawn, true); + + Entity respawnTarget = mock(Entity.class, CALLS_REAL_METHODS); + Entity nonRespawnTarget = mock(Entity.class, CALLS_REAL_METHODS); + + AttachmentTargetImpl.copyOnRespawn(original, respawnTarget, true); + AttachmentTargetImpl.copyOnRespawn(original, nonRespawnTarget, false); + assertTrue(respawnTarget.hasAttached(copiedOnRespawn)); + assertFalse(respawnTarget.hasAttached(notCopiedOnRespawn)); + assertTrue(nonRespawnTarget.hasAttached(copiedOnRespawn)); + assertTrue(nonRespawnTarget.hasAttached(notCopiedOnRespawn)); + } + + @Test + void testEntityPersistence() { + Entity entity = new MarkerEntity(EntityType.MARKER, mock()); + assertFalse(entity.hasAttached(PERSISTENT)); + + int expected = 1; + entity.setAttached(PERSISTENT, expected); + NbtCompound fakeSave = new NbtCompound(); + entity.writeNbt(fakeSave); + + entity = spy(new MarkerEntity(EntityType.MARKER, mock())); // fresh object, like on restart + entity.setChangeListener(mock()); + doNothing().when(entity).calculateDimensions(); + entity.readNbt(fakeSave); + assertTrue(entity.hasAttached(PERSISTENT)); + assertEquals(expected, entity.getAttached(PERSISTENT)); + } + + @Test + void testBlockEntityPersistence() { + BlockEntity blockEntity = new BellBlockEntity(BlockPos.ORIGIN, mock()); + assertFalse(blockEntity.hasAttached(PERSISTENT)); + + int expected = 1; + blockEntity.setAttached(PERSISTENT, expected); + NbtCompound fakeSave = blockEntity.createNbtWithId(); + + blockEntity = BlockEntity.createFromNbt(BlockPos.ORIGIN, mock(), fakeSave); + assertNotNull(blockEntity); + assertTrue(blockEntity.hasAttached(PERSISTENT)); + assertEquals(expected, blockEntity.getAttached(PERSISTENT)); + } + + @Test + void testWorldPersistentState() { + // Trying to simulate actual saving and loading for the world is too hard + ServerWorld world = mock(ServerWorld.class, CALLS_REAL_METHODS); + AttachmentPersistentState state = new AttachmentPersistentState(world); + assertFalse(world.hasAttached(PERSISTENT)); + + int expected = 1; + world.setAttached(PERSISTENT, expected); + NbtCompound fakeSave = state.writeNbt(new NbtCompound()); + + world = mock(ServerWorld.class, CALLS_REAL_METHODS); + AttachmentPersistentState.read(world, fakeSave); + assertTrue(world.hasAttached(PERSISTENT)); + assertEquals(expected, world.getAttached(PERSISTENT)); + } + + /* + * Chunk serializing is coupled with world saving in ChunkSerializer which is too much of a pain to mock, + * so testing is handled by the testmod instead. + */ +} diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java new file mode 100644 index 000000000..5c7e4dbbf --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java @@ -0,0 +1,68 @@ +/* + * 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.test.attachment; + +import com.mojang.serialization.Codec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.world.chunk.WorldChunk; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; + +public class AttachmentTestMod implements ModInitializer { + public static final String MOD_ID = "fabric-data-attachment-api-v1-testmod"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static final AttachmentType PERSISTENT = AttachmentRegistry.createPersistent( + new Identifier(MOD_ID, "persistent"), + Codec.STRING + ); + + private boolean firstLaunch = true; + + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + ServerWorld overworld; + WorldChunk chunk; + + if (firstLaunch) { + LOGGER.info("First launch, setting up"); + + overworld = server.getOverworld(); + overworld.setAttached(PERSISTENT, "world_data"); + + chunk = overworld.getChunk(0, 0); + chunk.setAttached(PERSISTENT, "chunk_data"); + } else { + LOGGER.info("Second launch, testing"); + + overworld = server.getOverworld(); + if (!"world_data".equals(overworld.getAttached(PERSISTENT))) throw new AssertionError(); + + chunk = overworld.getChunk(0, 0); + if (!"chunk_data".equals(chunk.getAttached(PERSISTENT))) throw new AssertionError(); + } + }); + ServerLifecycleEvents.SERVER_STOPPING.register(server -> firstLaunch = false); + } +} diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/gametest/AttachmentCopyTests.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/gametest/AttachmentCopyTests.java new file mode 100644 index 000000000..def57d8c7 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/gametest/AttachmentCopyTests.java @@ -0,0 +1,94 @@ +/* + * 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.test.attachment.gametest; + +import java.util.Objects; +import java.util.function.IntSupplier; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.test.GameTest; +import net.minecraft.test.GameTestException; +import net.minecraft.test.TestContext; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.fabricmc.fabric.test.attachment.AttachmentTestMod; + +public class AttachmentCopyTests implements FabricGameTest { + // using a lambda type because serialization shouldn't play a role in this + public static AttachmentType DUMMY = AttachmentRegistry.create( + new Identifier(AttachmentTestMod.MOD_ID, "dummy") + ); + public static AttachmentType COPY_ON_DEATH = AttachmentRegistry.builder() + .copyOnDeath() + .buildAndRegister(new Identifier(AttachmentTestMod.MOD_ID, "copy_test")); + + @GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE) + public void testCrossWorldTeleport(TestContext context) { + MinecraftServer server = context.getWorld().getServer(); + ServerWorld overworld = server.getOverworld(); + ServerWorld end = server.getWorld(World.END); + // using overworld and end to avoid portal code related to the nether + + Entity entity = EntityType.PIG.create(overworld); + Objects.requireNonNull(entity, "entity was null"); + entity.setAttached(DUMMY, () -> 10); + entity.setAttached(COPY_ON_DEATH, () -> 10); + + Entity moved = entity.moveToWorld(end); + if (moved == null) throw new GameTestException("Cross-world teleportation failed"); + + IntSupplier attached1 = moved.getAttached(DUMMY); + IntSupplier attached2 = moved.getAttached(COPY_ON_DEATH); + + if (attached1 == null || attached1.getAsInt() != 10 || attached2 == null || attached2.getAsInt() != 10) { + throw new GameTestException("Attachment copying failed during cross-world teleportation"); + } + + moved.discard(); + context.complete(); + } + + @GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE) + public void testMobConversion(TestContext context) { + MobEntity mob = Objects.requireNonNull(EntityType.ZOMBIE.create(context.getWorld())); + mob.setAttached(DUMMY, () -> 42); + mob.setAttached(COPY_ON_DEATH, () -> 42); + MobEntity converted = mob.convertTo(EntityType.DROWNED, false); + if (converted == null) throw new GameTestException("Conversion failed"); + + if (converted.hasAttached(DUMMY)) { + throw new GameTestException("Attachment shouldn't have been copied on mob conversion"); + } + + IntSupplier attached = converted.getAttached(COPY_ON_DEATH); + + if (attached == null || attached.getAsInt() != 42) { + throw new GameTestException("Attachment copying failed during mob conversion"); + } + + converted.discard(); + context.complete(); + } +} diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/gametest/BlockEntityTests.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/gametest/BlockEntityTests.java new file mode 100644 index 000000000..243295e97 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/gametest/BlockEntityTests.java @@ -0,0 +1,85 @@ +/* + * 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.test.attachment.gametest; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import net.minecraft.block.Block; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.listener.ClientPlayPacketListener; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.test.GameTest; +import net.minecraft.test.GameTestException; +import net.minecraft.test.TestContext; +import net.minecraft.util.math.BlockPos; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.fabricmc.fabric.test.attachment.AttachmentTestMod; +import net.fabricmc.fabric.test.attachment.mixin.BlockEntityTypeAccessor; + +public class BlockEntityTests implements FabricGameTest { + private static final Logger LOGGER = LogUtils.getLogger(); + + @GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE) + public void testBlockEntitySync(TestContext context) { + BlockPos pos = BlockPos.ORIGIN.up(); + + for (RegistryEntry> entry : Registries.BLOCK_ENTITY_TYPE.getIndexedEntries()) { + Block supportBlock = ((BlockEntityTypeAccessor) entry.value()).getBlocks().iterator().next(); + + if (!supportBlock.isEnabled(context.getWorld().getEnabledFeatures())) { + LOGGER.info("Skipped disabled feature {}", entry); + continue; + } + + BlockEntity be = entry.value().instantiate(pos, supportBlock.getDefaultState()); + + if (be == null) { + LOGGER.info("Couldn't get a block entity for type " + entry); + continue; + } + + be.setAttached(AttachmentTestMod.PERSISTENT, "test"); + Packet packet = be.toUpdatePacket(); + + if (packet == null) { + // Doesn't send update packets, fine + continue; + } + + if (!(packet instanceof BlockEntityUpdateS2CPacket)) { + LOGGER.warn("Not a BE packet for {}, instead {}", entry, packet); + continue; + } + + NbtCompound nbt = ((BlockEntityUpdateS2CPacket) packet).getNbt(); + + if (nbt != null && nbt.contains(AttachmentTarget.NBT_ATTACHMENT_KEY)) { + throw new GameTestException("Packet NBT for " + entry + " had persistent data: " + nbt.asString()); + } + } + + context.complete(); + } +} diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/mixin/BlockEntityTypeAccessor.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/mixin/BlockEntityTypeAccessor.java new file mode 100644 index 000000000..605c744e8 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/mixin/BlockEntityTypeAccessor.java @@ -0,0 +1,31 @@ +/* + * 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.test.attachment.mixin; + +import java.util.Set; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.block.Block; +import net.minecraft.block.entity.BlockEntityType; + +@Mixin(BlockEntityType.class) +public interface BlockEntityTypeAccessor { + @Accessor + Set getBlocks(); +} diff --git a/fabric-data-attachment-api-v1/src/testmod/resources/fabric-data-attachment-api-v1-testmod.mixins.json b/fabric-data-attachment-api-v1/src/testmod/resources/fabric-data-attachment-api-v1-testmod.mixins.json new file mode 100644 index 000000000..5a74edbaa --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/resources/fabric-data-attachment-api-v1-testmod.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.test.attachment.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "BlockEntityTypeAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json b/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json new file mode 100644 index 000000000..9c369f4f8 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "id": "fabric-data-attachment-api-v1-testmod", + "name": "Fabric Data Attachment API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-data-attachment-api-v1": "*", + "fabric-lifecycle-events-v1": "*" + }, + "entrypoints": { + "main": [ + "net.fabricmc.fabric.test.attachment.AttachmentTestMod" + ], + "fabric-gametest": [ + "net.fabricmc.fabric.test.attachment.gametest.AttachmentCopyTests", + "net.fabricmc.fabric.test.attachment.gametest.BlockEntityTests" + ] + }, + "mixins": [ + "fabric-data-attachment-api-v1-testmod.mixins.json" + ] +} diff --git a/gradle.properties b/gradle.properties index 7991c0c49..886698d46 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,6 +24,7 @@ fabric-commands-v0-version=0.2.51 fabric-containers-v0-version=0.1.64 fabric-content-registries-v0-version=4.0.11 fabric-crash-report-info-v1-version=0.2.19 +fabric-data-attachment-api-v1-version=1.0.0 fabric-data-generation-api-v1-version=12.3.4 fabric-dimensions-v1-version=2.1.54 fabric-entity-events-v1-version=1.5.23 diff --git a/settings.gradle b/settings.gradle index 7bb01f21f..29f8cd416 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include 'fabric-command-api-v2' include 'fabric-content-registries-v0' include 'fabric-convention-tags-v1' include 'fabric-crash-report-info-v1' +include 'fabric-data-attachment-api-v1' include 'fabric-data-generation-api-v1' include 'fabric-dimensions-v1' include 'fabric-entity-events-v1'