mirror of
https://github.com/FabricMC/fabric.git
synced 2024-11-27 18:15:44 -05:00
Data Attachment API (#3476)
This commit is contained in:
parent
1c78457f5d
commit
efd4a353d0
30 changed files with 1892 additions and 0 deletions
11
fabric-data-attachment-api-v1/build.gradle
Normal file
11
fabric-data-attachment-api-v1/build.gradle
Normal 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'
|
||||||
|
])
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"package": "net.fabricmc.fabric.mixin.attachment.client",
|
||||||
|
"compatibilityLevel": "JAVA_17",
|
||||||
|
"mixins": [
|
||||||
|
],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
"ClientPlayNetworkHandlerMixin"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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> { }
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"package": "net.fabricmc.fabric.test.attachment.mixin",
|
||||||
|
"compatibilityLevel": "JAVA_17",
|
||||||
|
"mixins": [
|
||||||
|
"BlockEntityTypeAccessor"
|
||||||
|
],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ fabric-commands-v0-version=0.2.51
|
||||||
fabric-containers-v0-version=0.1.64
|
fabric-containers-v0-version=0.1.64
|
||||||
fabric-content-registries-v0-version=4.0.11
|
fabric-content-registries-v0-version=4.0.11
|
||||||
fabric-crash-report-info-v1-version=0.2.19
|
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-data-generation-api-v1-version=12.3.4
|
||||||
fabric-dimensions-v1-version=2.1.54
|
fabric-dimensions-v1-version=2.1.54
|
||||||
fabric-entity-events-v1-version=1.5.23
|
fabric-entity-events-v1-version=1.5.23
|
||||||
|
|
|
@ -23,6 +23,7 @@ include 'fabric-command-api-v2'
|
||||||
include 'fabric-content-registries-v0'
|
include 'fabric-content-registries-v0'
|
||||||
include 'fabric-convention-tags-v1'
|
include 'fabric-convention-tags-v1'
|
||||||
include 'fabric-crash-report-info-v1'
|
include 'fabric-crash-report-info-v1'
|
||||||
|
include 'fabric-data-attachment-api-v1'
|
||||||
include 'fabric-data-generation-api-v1'
|
include 'fabric-data-generation-api-v1'
|
||||||
include 'fabric-dimensions-v1'
|
include 'fabric-dimensions-v1'
|
||||||
include 'fabric-entity-events-v1'
|
include 'fabric-entity-events-v1'
|
||||||
|
|
Loading…
Reference in a new issue