Data Attachment API (#3476)

This commit is contained in:
Syst3ms 2024-01-19 12:14:33 +01:00 committed by modmuss
parent 1c78457f5d
commit efd4a353d0
30 changed files with 1892 additions and 0 deletions

View file

@ -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'
])

View file

@ -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<Void> 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);
}
}

View file

@ -0,0 +1,13 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.attachment.client",
"compatibilityLevel": "JAVA_17",
"mixins": [
],
"injectors": {
"defaultRequire": 1
},
"client": [
"ClientPlayNetworkHandlerMixin"
]
}

View file

@ -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:
* <ul>
* <li>{@link #create(Identifier)}: attachments will be neither persistent nor auto-initialized.</li>
* <li>{@link #createDefaulted(Identifier, Supplier)}: attachments will be auto-initialized, but not persistent.</li>
* <li>{@link #createPersistent(Identifier, Codec)}: attachments will be persistent, but not auto-initialized.</li>
* </ul>
*
* <p>For finer control over the attachment type and its properties, use {@link AttachmentRegistry#builder()} to
* get a {@link Builder} instance.</p>
*/
@ApiStatus.Experimental
public final class AttachmentRegistry {
private AttachmentRegistry() {
}
/**
* Creates <i>and registers</i> an attachment. The data will not be persisted.
*
* @param id the identifier of this attachment
* @param <A> the type of attached data
* @return the registered {@link AttachmentType} instance
*/
public static <A> AttachmentType<A> create(Identifier id) {
Objects.requireNonNull(id, "identifier cannot be null");
return AttachmentRegistry.<A>builder().buildAndRegister(id);
}
/**
* Creates <i>and registers</i> 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 <A> the type of attached data
* @return the registered {@link AttachmentType} instance
*/
public static <A> AttachmentType<A> createDefaulted(Identifier id, Supplier<A> initializer) {
Objects.requireNonNull(id, "identifier cannot be null");
Objects.requireNonNull(initializer, "initializer cannot be null");
return AttachmentRegistry.<A>builder()
.initializer(initializer)
.buildAndRegister(id);
}
/**
* Creates <i>and registers</i> an attachment, that will persist across server restarts.
*
* @param id the identifier of this attachment
* @param codec the codec used for (de)serialization
* @param <A> the type of attached data
* @return the registered {@link AttachmentType} instance
*/
public static <A> AttachmentType<A> createPersistent(Identifier id, Codec<A> codec) {
Objects.requireNonNull(id, "identifier cannot be null");
Objects.requireNonNull(codec, "codec cannot be null");
return AttachmentRegistry.<A>builder().persistent(codec).buildAndRegister(id);
}
/**
* Creates a {@link Builder}, that gives finer control over the attachment's properties.
*
* @param <A> the type of the attached data
* @return a {@link Builder} instance
*/
public static <A> Builder<A> builder() {
return AttachmentRegistryImpl.builder();
}
/**
* A builder for creating {@link AttachmentType}s with finer control over their properties.
*
* @param <A> the type of the attached data
*/
@ApiStatus.NonExtendable
public interface Builder<A> {
/**
* 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<A> persistent(Codec<A> codec);
/**
* Declares that when a player dies and respawns, the attachments corresponding of this type should remain.
*
* @return the builder
*/
Builder<A> 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}.
*
* <p>It is <i>encouraged</i> for {@link A} to be an immutable data type, such as a primitive type
* or an immutable record.</p>
*
* <p>Otherwise, one must be very careful, as attachments <i>must not share any mutable state</i>.
* As an example, for a (mutable) list/array attachment type,
* the initializer should create a new independent instance each time it is called.</p>
*
* @param initializer the initializer
* @return the builder
*/
Builder<A> initializer(Supplier<A> initializer);
/**
* Builds and registers the {@link AttachmentType}.
*
* @param id the attachment's identifier
* @return the built and registered {@link AttachmentType}
*/
AttachmentType<A> buildAndRegister(Identifier id);
}
}

View file

@ -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.
*
* <p>Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerWorld} and {@link WorldChunk} via mixin.</p>
*
* <p>Note about {@link BlockEntity} and {@link WorldChunk} targets: these objects need to be notified of changes to their
* state (using {@link BlockEntity#markDirty()} and {@link Chunk#setNeedsSaving(boolean)} respectively), otherwise the modifications will not take effect properly.
* The {@link #setAttached(AttachmentType, Object)} method handles this automatically, but this needs to be done manually
* when attached data is mutable, for example:
* <pre>{@code
* AttachmentType<MutableType> MUTABLE_ATTACHMENT_TYPE = ...;
* BlockEntity be = ...;
* MutableType data = be.getAttachedOrCreate(MUTABLE_ATTACHMENT_TYPE);
* data.mutate();
* be.markDirty(); // Required because we are not using setAttached
* }</pre>
* </p>
*
* <p>
* 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.
* </p>
*/
@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 <A> the type of the data
* @return the attached data
*/
@Nullable
default <A> A getAttached(AttachmentType<A> 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 <A> the type of the data
* @return the attached data
*/
default <A> A getAttachedOrThrow(AttachmentType<A> 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 <A> the type of the data
* @return the attached data, initialized if originally absent
*/
default <A> A getAttachedOrSet(AttachmentType<A> 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 <A> the type of the data
* @return the attached data, initialized if originally absent
*/
default <A> A getAttachedOrCreate(AttachmentType<A> type, Supplier<A> 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 <i>only for attachment types with
* {@link AttachmentType#initializer() initializers}.</i> It will throw an exception if one is not present.
*
* @param type the attachment type
* @param <A> the type of the data
* @return the attached data, initialized if originally absent
*/
default <A> A getAttachedOrCreate(AttachmentType<A> type) {
Supplier<A> 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 <A> the type of the attached data
* @return the attached data, or the default value
*/
@Contract("_, !null -> !null")
default <A> A getAttachedOrElse(AttachmentType<A> 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 <A> the type of the attached data
* @return the attached data, or the default value
*/
default <A> A getAttachedOrGet(AttachmentType<A> type, Supplier<A> 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 <A> the type of the data
* @return the previous data
*/
@Nullable
default <A> A setAttached(AttachmentType<A> 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 <A> the type of the data
* @return the previous data
*/
@Nullable
default <A> A removeAttached(AttachmentType<A> 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 <A> the type of the data
* @return the previous data
*/
@Nullable
default <A> A modifyAttached(AttachmentType<A> type, UnaryOperator<A> modifier) {
return setAttached(type, modifier.apply(getAttached(type)));
}
}

View file

@ -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}.
*
* <p>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 <i>must not</i> share mutable state, and it is <i>strongly advised</i>
* for attachments not to hold internal references to their target. See the following note on entity targets.</p>
*
* <p>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.</p>
*
* @param <A> type of the attached data. It is encouraged for this to be an immutable type.
*/
@ApiStatus.NonExtendable
@ApiStatus.Experimental
public interface AttachmentType<A> {
/**
* @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<A> 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.
*
* <p>It is <i>encouraged</i> for {@link A} to be an immutable data type, such as a primitive type
* or an immutable record.</p>
*
* <p>Otherwise, one must be very careful, as attachments <i>must not share any mutable state</i>.
* As an example, for a (mutable) list/array attachment type,
* the initializer should create a new independent instance each time it is called.</p>
*
* @return the initializer for this attachment
*/
@Nullable
Supplier<A> 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();
}

View file

@ -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)
);
}
}

View file

@ -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;
}
}

View file

@ -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<Identifier, AttachmentType<?>> attachmentRegistry = new HashMap<>();
public static <A> void register(Identifier id, AttachmentType<A> 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 <A> AttachmentRegistry.Builder<A> builder() {
return new BuilderImpl<>();
}
public static class BuilderImpl<A> implements AttachmentRegistry.Builder<A> {
@Nullable
private Supplier<A> defaultInitializer = null;
@Nullable
private Codec<A> persistenceCodec = null;
private boolean copyOnDeath = false;
@Override
public AttachmentRegistry.Builder<A> persistent(Codec<A> codec) {
Objects.requireNonNull(codec, "codec cannot be null");
this.persistenceCodec = codec;
return this;
}
@Override
public AttachmentRegistry.Builder<A> copyOnDeath() {
this.copyOnDeath = true;
return this;
}
@Override
public AttachmentRegistry.Builder<A> initializer(Supplier<A> initializer) {
Objects.requireNonNull(initializer, "initializer cannot be null");
this.defaultInitializer = initializer;
return this;
}
@Override
public AttachmentType<A> buildAndRegister(Identifier id) {
var attachment = new AttachmentTypeImpl<>(id, defaultInitializer, persistenceCodec, copyOnDeath);
register(id, attachment);
return attachment;
}
}
}

View file

@ -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<AttachmentType<?>, ?> attachments) {
if (attachments == null) {
return;
}
var compound = new NbtCompound();
for (Map.Entry<AttachmentType<?>, ?> entry : attachments.entrySet()) {
AttachmentType<?> type = entry.getKey();
Codec<Object> codec = (Codec<Object>) 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<AttachmentType<?>, Object> deserializeAttachmentData(NbtCompound nbt) {
var attachments = new IdentityHashMap<AttachmentType<?>, 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<AttachmentType<?>, ?> map) {
if (map == null) {
return false;
}
for (AttachmentType<?> type : map.keySet()) {
if (type.isPersistent()) {
return true;
}
}
return false;
}
}

View file

@ -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<AttachmentType<?>, ?> attachments = ((AttachmentTargetImpl) original).fabric_getAttachments();
if (attachments == null) {
return;
}
for (Map.Entry<AttachmentType<?>, ?> entry : attachments.entrySet()) {
AttachmentType<Object> type = (AttachmentType<Object>) entry.getKey();
if (!isDeath || type.copyOnDeath()) {
target.setAttached(type, entry.getValue());
}
}
}
@Nullable
default Map<AttachmentType<?>, ?> 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");
}
}

View file

@ -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<A>(
Identifier identifier,
@Nullable Supplier<A> initializer,
@Nullable Codec<A> persistenceCodec,
boolean copyOnDeath
) implements AttachmentType<A> { }

View file

@ -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<AttachmentType<?>, Object> fabric_dataAttachments = null;
@SuppressWarnings("unchecked")
@Override
@Nullable
public <T> T getAttached(AttachmentType<T> type) {
return fabric_dataAttachments == null ? null : (T) fabric_dataAttachments.get(type);
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public <T> T setAttached(AttachmentType<T> 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<AttachmentType<?>, ?> fabric_getAttachments() {
return fabric_dataAttachments;
}
}

View file

@ -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<BlockEntity> cir) {
((AttachmentTargetImpl) blockEntity).fabric_readAttachmentsFromNbt(nbt);
}
@Inject(
method = "createNbt",
at = @At("RETURN")
)
private void writeBlockEntityAttachments(CallbackInfoReturnable<NbtCompound> cir) {
this.fabric_writeAttachmentsToNbt(cir.getReturnValue());
}
}

View file

@ -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<BlockEntity, NbtCompound> stripPersistentAttachmentData(Function<BlockEntity, NbtCompound> getter) {
return be -> {
NbtCompound nbt = getter.apply(be);
nbt.remove(AttachmentTarget.NBT_ATTACHMENT_KEY);
return nbt;
};
}
}

View file

@ -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<NbtCompound> cir) {
if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.LEVELCHUNK) {
((AttachmentTargetImpl) chunk).fabric_writeAttachmentsToNbt(cir.getReturnValue());
}
}
}

View file

@ -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<NbtCompound> cir) {
this.fabric_writeAttachmentsToNbt(nbt);
}
}

View file

@ -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 = "<init>")
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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -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
}
}

View file

@ -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"]
}
}
}

View file

@ -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<Integer> PERSISTENT = AttachmentRegistry.createPersistent(
new Identifier(MOD_ID, "persistent"),
Codec.INT
);
@BeforeAll
static void beforeAll() {
SharedConstants.createGameVersion();
Bootstrap.initialize();
}
@Test
void testTargets() {
AttachmentType<String> 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<String> 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<String> 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<Integer> 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<Double> dummy = AttachmentRegistry.createPersistent(
new Identifier(MOD_ID, "dummy"),
Codec.DOUBLE
);
var map = new IdentityHashMap<AttachmentType<?>, 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<AttachmentType<?>, 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<Boolean> notCopiedOnRespawn = AttachmentRegistry.create(
new Identifier(MOD_ID, "not_copied_on_respawn")
);
AttachmentType<Boolean> copiedOnRespawn = AttachmentRegistry.<Boolean>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.
*/
}

View file

@ -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<String> 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);
}
}

View file

@ -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<IntSupplier> DUMMY = AttachmentRegistry.create(
new Identifier(AttachmentTestMod.MOD_ID, "dummy")
);
public static AttachmentType<IntSupplier> COPY_ON_DEATH = AttachmentRegistry.<IntSupplier>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();
}
}

View file

@ -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<BlockEntityType<?>> 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<ClientPlayPacketListener> 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();
}
}

View file

@ -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<Block> getBlocks();
}

View file

@ -0,0 +1,11 @@
{
"required": true,
"package": "net.fabricmc.fabric.test.attachment.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": [
"BlockEntityTypeAccessor"
],
"injectors": {
"defaultRequire": 1
}
}

View file

@ -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"
]
}

View file

@ -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

View file

@ -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'