diff --git a/fabric-data-attachment-api-v1/build.gradle b/fabric-data-attachment-api-v1/build.gradle index cd51ebaf9..dc51470d2 100644 --- a/fabric-data-attachment-api-v1/build.gradle +++ b/fabric-data-attachment-api-v1/build.gradle @@ -3,10 +3,13 @@ version = getSubprojectVersion(project) moduleDependencies(project, [ 'fabric-api-base', ':fabric-entity-events-v1', - ':fabric-object-builder-api-v1' + ':fabric-object-builder-api-v1', + ':fabric-networking-api-v1' ]) testDependencies(project, [ ':fabric-lifecycle-events-v1', - ':fabric-biome-api-v1' + ':fabric-biome-api-v1', + ':fabric-command-api-v2', + ':fabric-rendering-v1' ]) diff --git a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/impl/attachment/client/AttachmentSyncClient.java b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/impl/attachment/client/AttachmentSyncClient.java new file mode 100644 index 000000000..ceb1bcef8 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/impl/attachment/client/AttachmentSyncClient.java @@ -0,0 +1,41 @@ +/* + * 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.client; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; +import net.fabricmc.fabric.impl.attachment.sync.s2c.RequestAcceptedAttachmentsPayloadS2C; + +public class AttachmentSyncClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + // config + ClientConfigurationNetworking.registerGlobalReceiver( + RequestAcceptedAttachmentsPayloadS2C.ID, + (payload, context) -> context.responseSender().sendPacket(AttachmentSync.createResponsePayload()) + ); + + // play + ClientPlayNetworking.registerGlobalReceiver( + AttachmentSyncPayloadS2C.ID, + (payload, context) -> payload.attachments().forEach(attachmentChange -> attachmentChange.apply(context.client().world)) + ); + } +} diff --git a/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json b/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json index fdf9b0f4c..0b250b91e 100644 --- a/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json +++ b/fabric-data-attachment-api-v1/src/client/resources/fabric-data-attachment-api-v1.client.mixins.json @@ -2,8 +2,6 @@ "required": true, "package": "net.fabricmc.fabric.mixin.attachment.client", "compatibilityLevel": "JAVA_21", - "mixins": [ - ], "injectors": { "defaultRequire": 1 }, diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java index e325bfaa9..f54934252 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentRegistry.java @@ -22,6 +22,8 @@ import java.util.function.Supplier; import com.mojang.serialization.Codec; import org.jetbrains.annotations.ApiStatus; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; import net.minecraft.util.Identifier; import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl; @@ -45,9 +47,10 @@ public final class AttachmentRegistry { } /** - * Creates and registers an attachment, configuring the builder used underneath. + * Creates and registers an attachment using a {@linkplain Builder builder}. * * @param id the identifier of this attachment + * @param consumer a lambda that configures a {@link Builder} for this attachment type * @param the type of attached data * @return the registered {@link AttachmentType} instance */ @@ -60,7 +63,7 @@ public final class AttachmentRegistry { } /** - * Creates and registers an attachment. The data will not be persisted. + * Creates and registers an attachment. The data will not be persisted or synchronized. * * @param id the identifier of this attachment * @param the type of attached data @@ -124,7 +127,7 @@ public final class AttachmentRegistry { Builder persistent(Codec codec); /** - * Declares that when a player dies and respawns, the attachments corresponding of this type should remain. + * Declares that when a player dies and respawns, the attachments of this type should remain. * * @return the builder */ @@ -138,7 +141,7 @@ public final class AttachmentRegistry { *

It is encouraged for {@link A} to be an immutable data type, such as a primitive type * or an immutable record.

* - *

Otherwise, one must be very careful, as attachments must not share any mutable state. + *

Otherwise, it is important to ensure that attachments do not share any mutable state. * As an example, for a (mutable) list/array attachment type, * the initializer should create a new independent instance each time it is called.

* @@ -147,6 +150,15 @@ public final class AttachmentRegistry { */ Builder
initializer(Supplier initializer); + /** + * Declares that this attachment type may be automatically synchronized with some clients, as determined by {@code syncPredicate}. + * + * @param packetCodec the codec used to serialize the attachment data over the network + * @param syncPredicate an {@link AttachmentSyncPredicate} determining with which clients to synchronize data + * @return the builder + */ + AttachmentRegistry.Builder syncWith(PacketCodec packetCodec, AttachmentSyncPredicate syncPredicate); + /** * Builds and registers the {@link AttachmentType}. * diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentSyncPredicate.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentSyncPredicate.java new file mode 100644 index 000000000..2bd85b055 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentSyncPredicate.java @@ -0,0 +1,56 @@ +/* + * 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.BiPredicate; + +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.server.network.ServerPlayerEntity; + +/** + * A predicate that determines, for a specific attachment type, whether the data should be synchronized with a + * player's client, given the player's {@link ServerPlayerEntity} and the {@linkplain AttachmentTarget} the data is linked to. + * + *

The class extends {@link BiPredicate} to allow for custom predicates, outside the ones provided by methods.

+ */ +@ApiStatus.NonExtendable +@FunctionalInterface +public interface AttachmentSyncPredicate extends BiPredicate { + /** + * @return a predicate that syncs an attachment with all clients + */ + static AttachmentSyncPredicate all() { + return (t, p) -> true; + } + + /** + * @return a predicate that syncs an attachment only with the target it is attached to, when that is a player. If the + * target isn't a player, the attachment will be synced with no clients. + */ + static AttachmentSyncPredicate targetOnly() { + return (target, player) -> target == player; + } + + /** + * @return a predicate that syncs an attachment with every client except the target it is attached to, when that is a player. + * When the target isn't a player, the attachment will be synced with all clients. + */ + static AttachmentSyncPredicate allButTarget() { + return (target, player) -> target != player; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java index cfae90949..e189d65ce 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentType.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import net.minecraft.entity.Entity; +import net.minecraft.network.codec.PacketCodec; import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; import net.minecraft.world.chunk.Chunk; @@ -36,7 +37,7 @@ 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}. + * optionally be made to persist between restarts using a provided {@link Codec}, and to synchronize with player clients. * *

While the API places no restrictions on the types of data that can be attached, it is generally encouraged to use * immutable types. More generally, different attachments must not share mutable state, and it is strongly advised @@ -53,6 +54,9 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; *

* * @param
type of the attached data. It is encouraged for this to be an immutable type. + * @see AttachmentRegistry + * @see AttachmentRegistry.Builder#persistent(Codec) + * @see AttachmentRegistry.Builder#syncWith(PacketCodec, AttachmentSyncPredicate) */ @ApiStatus.NonExtendable @ApiStatus.Experimental @@ -93,6 +97,15 @@ public interface AttachmentType { @Nullable Supplier initializer(); + /** + * Whether this attachment type can be synchronized with clients. This method returning {@code true} does not in any way + * indicate that the attachment type will synchronize data with any given client, only that it is able to, as per its + * {@link AttachmentSyncPredicate}. + * + * @return whether this attachment type is synced + */ + boolean isSynced(); + /** * @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) diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java index f1ce87465..ae6076478 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentRegistryImpl.java @@ -16,9 +16,12 @@ package net.fabricmc.fabric.impl.attachment; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import com.mojang.serialization.Codec; @@ -26,20 +29,35 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; import net.minecraft.util.Identifier; import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; public final class AttachmentRegistryImpl { private static final Logger LOGGER = LoggerFactory.getLogger("fabric-data-attachment-api-v1"); private static final Map> attachmentRegistry = new HashMap<>(); + private static final Set syncableAttachments = new HashSet<>(); + private static final Set syncableView = Collections.unmodifiableSet(syncableAttachments); public static void register(Identifier id, AttachmentType attachmentType) { AttachmentType existing = attachmentRegistry.put(id, attachmentType); if (existing != null) { LOGGER.warn("Encountered duplicate type registration for id {}", id); + + // Prevent duplicate registration from incorrectly overriding a synced type with a non-synced one or vice-versa + if (existing.isSynced() && !attachmentType.isSynced()) { + syncableAttachments.remove(id); + } else if (!existing.isSynced() && attachmentType.isSynced()) { + syncableAttachments.add(id); + } + } else if (attachmentType.isSynced()) { + syncableAttachments.add(id); } } @@ -48,6 +66,10 @@ public final class AttachmentRegistryImpl { return attachmentRegistry.get(id); } + public static Set getSyncableAttachments() { + return syncableView; + } + public static AttachmentRegistry.Builder builder() { return new BuilderImpl<>(); } @@ -57,6 +79,10 @@ public final class AttachmentRegistryImpl { private Supplier defaultInitializer = null; @Nullable private Codec persistenceCodec = null; + @Nullable + private PacketCodec packetCodec = null; + @Nullable + private AttachmentSyncPredicate syncPredicate = null; private boolean copyOnDeath = false; @Override @@ -81,11 +107,36 @@ public final class AttachmentRegistryImpl { return this; } + public AttachmentRegistry.Builder syncWith(PacketCodec packetCodec, AttachmentSyncPredicate syncPredicate) { + Objects.requireNonNull(packetCodec, "packet codec cannot be null"); + Objects.requireNonNull(syncPredicate, "sync predicate cannot be null"); + + this.packetCodec = packetCodec; + this.syncPredicate = syncPredicate; + return this; + } + @Override public AttachmentType buildAndRegister(Identifier id) { Objects.requireNonNull(id, "identifier cannot be null"); - var attachment = new AttachmentTypeImpl<>(id, defaultInitializer, persistenceCodec, copyOnDeath); + if (syncPredicate != null && id.toString().length() > AttachmentSync.MAX_IDENTIFIER_SIZE) { + throw new IllegalArgumentException( + "Identifier length is too long for a synced attachment type (was %d, maximum is %d)".formatted( + id.toString().length(), + AttachmentSync.MAX_IDENTIFIER_SIZE + ) + ); + } + + var attachment = new AttachmentTypeImpl<>( + id, + defaultInitializer, + persistenceCodec, + packetCodec, + syncPredicate, + copyOnDeath + ); register(id, attachment); return attachment; } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java index 87b79afd2..860aba0c4 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSerializingImpl.java @@ -53,7 +53,7 @@ public class AttachmentSerializingImpl { RegistryOps registryOps = wrapperLookup.getOps(NbtOps.INSTANCE); codec.encodeStart(registryOps, entry.getValue()) .ifError(partial -> { - LOGGER.warn("Couldn't serialize attachment " + type.identifier() + ", skipping. Error:"); + LOGGER.warn("Couldn't serialize attachment {}, skipping. Error:", type.identifier()); LOGGER.warn(partial.message()); }) .ifSuccess(serialized -> compound.put(type.identifier().toString(), serialized)); @@ -73,7 +73,7 @@ public class AttachmentSerializingImpl { AttachmentType type = AttachmentRegistryImpl.get(Identifier.of(key)); if (type == null) { - LOGGER.warn("Unknown attachment type " + key + " found when deserializing, skipping"); + LOGGER.warn("Unknown attachment type {} found when deserializing, skipping", key); continue; } @@ -83,7 +83,7 @@ public class AttachmentSerializingImpl { RegistryOps registryOps = wrapperLookup.getOps(NbtOps.INSTANCE); codec.parse(registryOps, compound.get(key)) .ifError(partial -> { - LOGGER.warn("Couldn't deserialize attachment " + type.identifier() + ", skipping. Error:"); + LOGGER.warn("Couldn't deserialize attachment {}, skipping. Error:", type.identifier()); LOGGER.warn(partial.message()); }) .ifSuccess( diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java index 6cf81b0be..0794002fa 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTargetImpl.java @@ -17,14 +17,19 @@ package net.fabricmc.fabric.impl.attachment; import java.util.Map; +import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import net.minecraft.nbt.NbtCompound; import net.minecraft.registry.RegistryWrapper; +import net.minecraft.server.network.ServerPlayerEntity; import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; public interface AttachmentTargetImpl extends AttachmentTarget { /** @@ -32,7 +37,7 @@ public interface AttachmentTargetImpl extends AttachmentTarget { * WorldChunk, and when an entity is respawned and a new instance is created. For entity respawns, it 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 transfer(AttachmentTarget original, AttachmentTarget target, boolean isDeath) { Map, ?> attachments = ((AttachmentTargetImpl) original).fabric_getAttachments(); @@ -66,4 +71,26 @@ public interface AttachmentTargetImpl extends AttachmentTarget { default boolean fabric_hasPersistentAttachments() { throw new UnsupportedOperationException("Implemented via mixin"); } + + default AttachmentTargetInfo fabric_getSyncTargetInfo() { + // this only makes sense for server objects + throw new UnsupportedOperationException("Sync target info was not retrieved on server!"); + } + + /* + * Computes changes that should be communicated to newcomers (i.e. clients that start tracking this target) + */ + default void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer changeOutput) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + default void fabric_syncChange(AttachmentType type, AttachmentSyncPayloadS2C payload) { + } + + default void fabric_markChanged(AttachmentType type) { + } + + default boolean fabric_shouldTryToSync() { + throw new UnsupportedOperationException("Implemented via mixin"); + } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java index 7a082e14f..a316442e2 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentTypeImpl.java @@ -21,13 +21,23 @@ import java.util.function.Supplier; import com.mojang.serialization.Codec; import org.jetbrains.annotations.Nullable; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; import net.minecraft.util.Identifier; +import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; public record AttachmentTypeImpl( Identifier identifier, @Nullable Supplier initializer, @Nullable Codec persistenceCodec, + @Nullable PacketCodec packetCodec, + @Nullable AttachmentSyncPredicate syncPredicate, boolean copyOnDeath -) implements AttachmentType { } +) implements AttachmentType { + @Override + public boolean isSynced() { + return syncPredicate != null; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentChange.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentChange.java new file mode 100644 index 000000000..ca4eab840 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentChange.java @@ -0,0 +1,123 @@ +/* + * 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.sync; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.netty.buffer.Unpooled; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; +import net.fabricmc.fabric.mixin.attachment.CustomPayloadS2CPacketAccessor; +import net.fabricmc.fabric.mixin.attachment.VarIntsAccessor; +import net.fabricmc.fabric.mixin.networking.accessor.ServerCommonNetworkHandlerAccessor; + +public record AttachmentChange(AttachmentTargetInfo targetInfo, AttachmentType type, byte[] data) { + public static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + AttachmentTargetInfo.PACKET_CODEC, AttachmentChange::targetInfo, + Identifier.PACKET_CODEC.xmap( + id -> Objects.requireNonNull(AttachmentRegistryImpl.get(id)), + AttachmentType::identifier + ), AttachmentChange::type, + PacketCodecs.BYTE_ARRAY, AttachmentChange::data, + AttachmentChange::new + ); + private static final int MAX_PADDING_SIZE_IN_BYTES = AttachmentTargetInfo.MAX_SIZE_IN_BYTES + AttachmentSync.MAX_IDENTIFIER_SIZE; + private static final int MAX_DATA_SIZE_IN_BYTES = CustomPayloadS2CPacketAccessor.getMaxPayloadSize() - MAX_PADDING_SIZE_IN_BYTES; + + @SuppressWarnings("unchecked") + public static AttachmentChange create(AttachmentTargetInfo targetInfo, AttachmentType type, @Nullable Object value) { + PacketCodec codec = (PacketCodec) ((AttachmentTypeImpl) type).packetCodec(); + Objects.requireNonNull(codec, "attachment packet codec cannot be null"); + + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeOptional(Optional.ofNullable(value), codec); + byte[] encoded = buf.array(); + + if (encoded.length > MAX_DATA_SIZE_IN_BYTES) { + throw new IllegalArgumentException("Data for attachment '%s' was too big (%d bytes, over maximum %d)".formatted( + type.identifier(), + encoded.length, + MAX_DATA_SIZE_IN_BYTES + )); + } + + return new AttachmentChange(targetInfo, type, encoded); + } + + public static void partitionAndSendPackets(List changes, ServerPlayerEntity player) { + Set supported = ((SupportedAttachmentsClientConnection) ((ServerCommonNetworkHandlerAccessor) player.networkHandler).getConnection()) + .fabric_getSupportedAttachments(); + // sort by size to better partition packets + changes.sort(Comparator.comparingInt(c -> c.data().length)); + List packetChanges = new ArrayList<>(); + int maxVarIntSize = VarIntsAccessor.getMaxByteSize(); + int byteSize = maxVarIntSize; + + for (AttachmentChange change : changes) { + if (!supported.contains(change.type.identifier())) { + continue; + } + + int size = MAX_PADDING_SIZE_IN_BYTES + change.data.length; + + if (byteSize + size > MAX_DATA_SIZE_IN_BYTES) { + ServerPlayNetworking.send(player, new AttachmentSyncPayloadS2C(packetChanges)); + packetChanges.clear(); + byteSize = maxVarIntSize; + } + + packetChanges.add(change); + byteSize += size; + } + + if (!packetChanges.isEmpty()) { + ServerPlayNetworking.send(player, new AttachmentSyncPayloadS2C(packetChanges)); + } + } + + @SuppressWarnings("unchecked") + @Nullable + public Object decodeValue() { + PacketCodec codec = (PacketCodec) ((AttachmentTypeImpl) type).packetCodec(); + Objects.requireNonNull(codec, "codec was null"); + + PacketByteBuf buf = new PacketByteBuf(Unpooled.copiedBuffer(data)); + return buf.readOptional(codec).orElse(null); + } + + public void apply(World world) { + targetInfo.getTarget(world).setAttached((AttachmentType) type, decodeValue()); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentSync.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentSync.java new file mode 100644 index 000000000..e8153a144 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentSync.java @@ -0,0 +1,132 @@ +/* + * 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.sync; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.Packet; +import net.minecraft.server.network.ServerPlayerConfigurationTask; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerConfigurationConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint; +import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.sync.c2s.AcceptedAttachmentsPayloadC2S; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; +import net.fabricmc.fabric.impl.attachment.sync.s2c.RequestAcceptedAttachmentsPayloadS2C; +import net.fabricmc.fabric.mixin.networking.accessor.ServerCommonNetworkHandlerAccessor; + +public class AttachmentSync implements ModInitializer { + public static final int MAX_IDENTIFIER_SIZE = 256; + + public static AcceptedAttachmentsPayloadC2S createResponsePayload() { + return new AcceptedAttachmentsPayloadC2S(AttachmentRegistryImpl.getSyncableAttachments()); + } + + public static void trySync(AttachmentSyncPayloadS2C payload, ServerPlayerEntity player) { + if (!payload.attachments().isEmpty()) { + ServerPlayNetworking.send(player, payload); + } + } + + private static Set decodeResponsePayload(AcceptedAttachmentsPayloadC2S payload) { + Set atts = payload.acceptedAttachments(); + Set syncable = AttachmentRegistryImpl.getSyncableAttachments(); + atts.retainAll(syncable); + + if (atts.size() < syncable.size()) { + // Client doesn't support all + AttachmentEntrypoint.LOGGER.warn( + "Client does not support the syncable attachments {}", + syncable.stream().filter(id -> !atts.contains(id)).map(Identifier::toString).collect(Collectors.joining(", ")) + ); + } + + return atts; + } + + @Override + public void onInitialize() { + // Config + PayloadTypeRegistry.configurationC2S() + .register(AcceptedAttachmentsPayloadC2S.ID, AcceptedAttachmentsPayloadC2S.CODEC); + PayloadTypeRegistry.configurationS2C() + .register(RequestAcceptedAttachmentsPayloadS2C.ID, RequestAcceptedAttachmentsPayloadS2C.CODEC); + + ServerConfigurationConnectionEvents.CONFIGURE.register((handler, server) -> { + if (ServerConfigurationNetworking.canSend(handler, RequestAcceptedAttachmentsPayloadS2C.PACKET_ID)) { + handler.addTask(new AttachmentSyncTask()); + } else { + AttachmentEntrypoint.LOGGER.debug( + "Couldn't send attachment configuration packet to client, as the client cannot receive the payload." + ); + } + }); + + ServerConfigurationNetworking.registerGlobalReceiver(AcceptedAttachmentsPayloadC2S.ID, (payload, context) -> { + Set supportedAttachments = decodeResponsePayload(payload); + ClientConnection connection = ((ServerCommonNetworkHandlerAccessor) context.networkHandler()).getConnection(); + ((SupportedAttachmentsClientConnection) connection).fabric_setSupportedAttachments(supportedAttachments); + + context.networkHandler().completeTask(AttachmentSyncTask.KEY); + }); + + // Play + PayloadTypeRegistry.playS2C().register(AttachmentSyncPayloadS2C.ID, AttachmentSyncPayloadS2C.CODEC); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + ServerPlayerEntity player = handler.player; + List changes = new ArrayList<>(); + // sync world attachments + ((AttachmentTargetImpl) player.getServerWorld()).fabric_computeInitialSyncChanges(player, changes::add); + // sync player's own persistent attachments that couldn't be synced earlier + ((AttachmentTargetImpl) player).fabric_computeInitialSyncChanges(player, changes::add); + + if (!changes.isEmpty()) { + AttachmentChange.partitionAndSendPackets(changes, player); + } + }); + + // entity tracking handled in EntityTrackerEntryMixin instead, see comment + } + + private record AttachmentSyncTask() implements ServerPlayerConfigurationTask { + public static final Key KEY = new Key(RequestAcceptedAttachmentsPayloadS2C.PACKET_ID.toString()); + + @Override + public void sendPacket(Consumer> sender) { + sender.accept(ServerConfigurationNetworking.createS2CPacket(RequestAcceptedAttachmentsPayloadS2C.INSTANCE)); + } + + @Override + public Key getKey() { + return KEY; + } + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentTargetInfo.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentTargetInfo.java new file mode 100644 index 000000000..531e2f062 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/AttachmentTargetInfo.java @@ -0,0 +1,131 @@ +/* + * 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.sync; + +import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap; +import it.unimi.dsi.fastutil.bytes.Byte2ObjectMap; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; + +public sealed interface AttachmentTargetInfo { + int MAX_SIZE_IN_BYTES = Byte.BYTES + Long.BYTES; + PacketCodec> PACKET_CODEC = PacketCodecs.BYTE.dispatch( + AttachmentTargetInfo::getId, Type::packetCodecFromId + ); + + Type getType(); + + default byte getId() { + return getType().id; + } + + AttachmentTarget getTarget(World world); + + record Type(byte id, PacketCodec> packetCodec) { + static Byte2ObjectMap> TYPES = new Byte2ObjectArrayMap<>(); + static Type BLOCK_ENTITY = new Type<>((byte) 0, BlockEntityTarget.PACKET_CODEC); + static Type ENTITY = new Type<>((byte) 1, EntityTarget.PACKET_CODEC); + static Type CHUNK = new Type<>((byte) 2, ChunkTarget.PACKET_CODEC); + static Type WORLD = new Type<>((byte) 3, WorldTarget.PACKET_CODEC); + + public Type { + TYPES.put(id, this); + } + + static PacketCodec> packetCodecFromId(byte id) { + return TYPES.get(id).packetCodec; + } + } + + record BlockEntityTarget(BlockPos pos) implements AttachmentTargetInfo { + static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + BlockPos.PACKET_CODEC, BlockEntityTarget::pos, + BlockEntityTarget::new + ); + + @Override + public Type getType() { + return Type.BLOCK_ENTITY; + } + + @Override + public AttachmentTarget getTarget(World world) { + return world.getBlockEntity(pos); + } + } + + record EntityTarget(int networkId) implements AttachmentTargetInfo { + static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + PacketCodecs.VAR_INT, EntityTarget::networkId, + EntityTarget::new + ); + + @Override + public Type getType() { + return Type.ENTITY; + } + + @Override + public AttachmentTarget getTarget(World world) { + return world.getEntityById(networkId); + } + } + + record ChunkTarget(ChunkPos pos) implements AttachmentTargetInfo { + static final PacketCodec PACKET_CODEC = PacketCodecs.VAR_LONG + .xmap(ChunkPos::new, ChunkPos::toLong) + .xmap(ChunkTarget::new, ChunkTarget::pos); + + @Override + public Type getType() { + return Type.CHUNK; + } + + @Override + public AttachmentTarget getTarget(World world) { + return world.getChunk(pos.x, pos.z); + } + } + + final class WorldTarget implements AttachmentTargetInfo { + public static final WorldTarget INSTANCE = new WorldTarget(); + static final PacketCodec PACKET_CODEC = PacketCodec.unit(INSTANCE); + + private WorldTarget() { + } + + @Override + public Type getType() { + return Type.WORLD; + } + + @Override + public AttachmentTarget getTarget(World world) { + return world; + } + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/SupportedAttachmentsClientConnection.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/SupportedAttachmentsClientConnection.java new file mode 100644 index 000000000..1893988aa --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/SupportedAttachmentsClientConnection.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.attachment.sync; + +import java.util.Set; + +import net.minecraft.network.ClientConnection; +import net.minecraft.util.Identifier; + +/** + * Implemented on {@link ClientConnection} to store which attachments the client supports. + */ +public interface SupportedAttachmentsClientConnection { + void fabric_setSupportedAttachments(Set supportedAttachments); + + Set fabric_getSupportedAttachments(); +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/c2s/AcceptedAttachmentsPayloadC2S.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/c2s/AcceptedAttachmentsPayloadC2S.java new file mode 100644 index 000000000..d2c780005 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/c2s/AcceptedAttachmentsPayloadC2S.java @@ -0,0 +1,40 @@ +/* + * 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.sync.c2s; + +import java.util.HashSet; +import java.util.Set; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public record AcceptedAttachmentsPayloadC2S(Set acceptedAttachments) implements CustomPayload { + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.collection(HashSet::new, Identifier.PACKET_CODEC), AcceptedAttachmentsPayloadC2S::acceptedAttachments, + AcceptedAttachmentsPayloadC2S::new + ); + public static final Identifier PACKET_ID = Identifier.of("fabric", "accepted_attachments_v1"); + public static final Id ID = new Id<>(PACKET_ID); + + @Override + public Id getId() { + return ID; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/s2c/AttachmentSyncPayloadS2C.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/s2c/AttachmentSyncPayloadS2C.java new file mode 100644 index 000000000..0d776ec1b --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/s2c/AttachmentSyncPayloadS2C.java @@ -0,0 +1,41 @@ +/* + * 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.sync.s2c; + +import java.util.List; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; + +public record AttachmentSyncPayloadS2C(List attachments) implements CustomPayload { + public static final PacketCodec CODEC = PacketCodec.tuple( + AttachmentChange.PACKET_CODEC.collect(PacketCodecs.toList()), AttachmentSyncPayloadS2C::attachments, + AttachmentSyncPayloadS2C::new + ); + public static final Identifier PACKET_ID = Identifier.of("fabric", "attachment_sync_v1"); + public static final Id ID = new Id<>(PACKET_ID); + + @Override + public Id getId() { + return ID; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/s2c/RequestAcceptedAttachmentsPayloadS2C.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/s2c/RequestAcceptedAttachmentsPayloadS2C.java new file mode 100644 index 000000000..d6d16ba3e --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/sync/s2c/RequestAcceptedAttachmentsPayloadS2C.java @@ -0,0 +1,37 @@ +/* + * 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.sync.s2c; + +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public class RequestAcceptedAttachmentsPayloadS2C implements CustomPayload { + public static final RequestAcceptedAttachmentsPayloadS2C INSTANCE = new RequestAcceptedAttachmentsPayloadS2C(); + public static final Identifier PACKET_ID = Identifier.of("fabric", "accepted_attachments_v1"); + public static final Id ID = new Id<>(PACKET_ID); + public static final PacketCodec CODEC = PacketCodec.unit(INSTANCE); + + private RequestAcceptedAttachmentsPayloadS2C() { + } + + @Override + public Id getId() { + return ID; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java index b385a0170..2ff585c7e 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/AttachmentTargetsMixin.java @@ -17,28 +17,35 @@ package net.fabricmc.fabric.mixin.attachment; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import net.minecraft.block.entity.BlockEntity; import net.minecraft.entity.Entity; import net.minecraft.nbt.NbtCompound; import net.minecraft.registry.RegistryWrapper; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.ChunkStatus; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; -import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint; import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; @Mixin({BlockEntity.class, Entity.class, World.class, Chunk.class}) abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { @Nullable private IdentityHashMap, Object> fabric_dataAttachments = null; + @Nullable + private IdentityHashMap, AttachmentChange> fabric_syncedAttachments = null; @SuppressWarnings("unchecked") @Override @@ -51,17 +58,12 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { @Override @Nullable public T setAttached(AttachmentType type, @Nullable T value) { - // Extremely inelegant, but the only alternative is separating out these two mixins and duplicating code - Object thisObject = this; + this.fabric_markChanged(type); - if (thisObject instanceof BlockEntity) { - ((BlockEntity) thisObject).markDirty(); - } else if (thisObject instanceof Chunk) { - ((Chunk) thisObject).markNeedsSaving(); - - if (type.isPersistent() && ((Chunk) thisObject).getStatus().equals(ChunkStatus.EMPTY)) { - AttachmentEntrypoint.LOGGER.warn("Attaching persistent attachment {} to chunk with chunk status EMPTY. Attachment might be discarded.", type.identifier()); - } + if (this.fabric_shouldTryToSync() && type.isSynced()) { + AttachmentChange change = AttachmentChange.create(fabric_getSyncTargetInfo(), type, value); + acknowledgeSyncedEntry(type, change); + this.fabric_syncChange(type, new AttachmentSyncPayloadS2C(List.of(change))); } if (value == null) { @@ -69,13 +71,7 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { return null; } - T removed = (T) fabric_dataAttachments.remove(type); - - if (fabric_dataAttachments.isEmpty()) { - fabric_dataAttachments = null; - } - - return removed; + return (T) fabric_dataAttachments.remove(type); } else { if (fabric_dataAttachments == null) { fabric_dataAttachments = new IdentityHashMap<>(); @@ -97,7 +93,17 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { @Override public void fabric_readAttachmentsFromNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapperLookup) { - fabric_dataAttachments = AttachmentSerializingImpl.deserializeAttachmentData(nbt, wrapperLookup); + // Note on player targets: no syncing can happen here as the networkHandler is still null + // Instead it is done on player join (see AttachmentSync) + this.fabric_dataAttachments = AttachmentSerializingImpl.deserializeAttachmentData(nbt, wrapperLookup); + + if (this.fabric_shouldTryToSync() && this.fabric_dataAttachments != null) { + this.fabric_dataAttachments.forEach((type, value) -> { + if (type.isSynced()) { + acknowledgeSynced(type, value); + } + }); + } } @Override @@ -109,4 +115,39 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { public Map, ?> fabric_getAttachments() { return fabric_dataAttachments; } + + @Unique + private void acknowledgeSynced(AttachmentType type, Object value) { + acknowledgeSyncedEntry(type, AttachmentChange.create(fabric_getSyncTargetInfo(), type, value)); + } + + @Unique + private void acknowledgeSyncedEntry(AttachmentType type, @Nullable AttachmentChange change) { + if (change == null) { + if (fabric_syncedAttachments == null) { + return; + } + + fabric_syncedAttachments.remove(type); + } else { + if (fabric_syncedAttachments == null) { + fabric_syncedAttachments = new IdentityHashMap<>(); + } + + fabric_syncedAttachments.put(type, change); + } + } + + @Override + public void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer changeOutput) { + if (fabric_syncedAttachments == null) { + return; + } + + for (Map.Entry, AttachmentChange> entry : fabric_syncedAttachments.entrySet()) { + if (((AttachmentTypeImpl) entry.getKey()).syncPredicate().test(this, player)) { + changeOutput.accept(entry.getValue()); + } + } + } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java index d617eb481..415544239 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/BlockEntityMixin.java @@ -16,7 +16,10 @@ package net.fabricmc.fabric.mixin.attachment; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -25,11 +28,32 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import net.minecraft.block.entity.BlockEntity; import net.minecraft.nbt.NbtCompound; import net.minecraft.registry.RegistryWrapper; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; @Mixin(BlockEntity.class) abstract class BlockEntityMixin implements AttachmentTargetImpl { + @Shadow + @Final + protected BlockPos pos; + @Shadow + @Nullable + protected World world; + + @Shadow + public abstract void markDirty(); + + @Shadow + public abstract boolean hasWorld(); + @Inject( method = "read", at = @At("RETURN") @@ -45,4 +69,30 @@ abstract class BlockEntityMixin implements AttachmentTargetImpl { private void writeBlockEntityAttachments(RegistryWrapper.WrapperLookup wrapperLookup, CallbackInfoReturnable cir) { this.fabric_writeAttachmentsToNbt(cir.getReturnValue(), wrapperLookup); } + + @Override + public void fabric_markChanged(AttachmentType type) { + this.markDirty(); + } + + @Override + public AttachmentTargetInfo fabric_getSyncTargetInfo() { + return new AttachmentTargetInfo.BlockEntityTarget(this.pos); + } + + @Override + public void fabric_syncChange(AttachmentType type, AttachmentSyncPayloadS2C payload) { + PlayerLookup.tracking((BlockEntity) (Object) this) + .forEach(player -> { + if (((AttachmentTypeImpl) type).syncPredicate().test(this, player)) { + AttachmentSync.trySync(payload, player); + } + }); + } + + @Override + public boolean fabric_shouldTryToSync() { + // Persistent attachments are read at a time with no world + return !this.hasWorld() || !this.world.isClient(); + } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkDataSenderMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkDataSenderMixin.java new file mode 100644 index 000000000..84f9873b5 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkDataSenderMixin.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.attachment; + +import java.util.ArrayList; +import java.util.List; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import net.minecraft.server.network.ChunkDataSender; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.chunk.WorldChunk; + +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; + +@Mixin(ChunkDataSender.class) +abstract class ChunkDataSenderMixin { + @WrapOperation( + method = "sendChunkBatches", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ChunkDataSender;sendChunkData(Lnet/minecraft/server/network/ServerPlayNetworkHandler;Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/WorldChunk;)V" + ) + ) + private void sendInitialAttachmentData(ServerPlayNetworkHandler handler, ServerWorld world, WorldChunk chunk, Operation original, ServerPlayerEntity player) { + original.call(handler, world, chunk); + // do a wrap operation so this packet is sent *after* the chunk ones + List changes = new ArrayList<>(); + ((AttachmentTargetImpl) chunk).fabric_computeInitialSyncChanges(player, changes::add); + + if (!changes.isEmpty()) { + AttachmentChange.partitionAndSendPackets(changes, player); + } + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkMixin.java new file mode 100644 index 000000000..e819d8be5 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ChunkMixin.java @@ -0,0 +1,70 @@ +/* + * 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.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkStatus; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; + +@Mixin(Chunk.class) +abstract class ChunkMixin implements AttachmentTargetImpl { + @Shadow + @Final + protected ChunkPos pos; + + @Shadow + public abstract ChunkStatus getStatus(); + + @Shadow + public abstract ChunkPos getPos(); + + @Shadow + public abstract boolean needsSaving(); + + @Override + public AttachmentTargetInfo fabric_getSyncTargetInfo() { + return new AttachmentTargetInfo.ChunkTarget(this.pos); + } + + @Override + public void fabric_markChanged(AttachmentType type) { + needsSaving(); + + if (type.isPersistent() && this.getStatus().equals(ChunkStatus.EMPTY)) { + AttachmentEntrypoint.LOGGER.warn( + "Attaching persistent attachment {} to chunk {} with chunk status EMPTY. Attachment might be discarded.", + type.identifier(), + this.getPos() + ); + } + } + + @Override + public boolean fabric_shouldTryToSync() { + // ProtoChunk or EmptyChunk + return false; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ClientConnectionMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ClientConnectionMixin.java new file mode 100644 index 000000000..187ca2921 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ClientConnectionMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.attachment; + +import java.util.HashSet; +import java.util.Set; + +import org.spongepowered.asm.mixin.Mixin; + +import net.minecraft.network.ClientConnection; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.impl.attachment.sync.SupportedAttachmentsClientConnection; + +@Mixin(ClientConnection.class) +public class ClientConnectionMixin implements SupportedAttachmentsClientConnection { + private Set fabric_supportedAttachments = new HashSet<>(); + + @Override + public void fabric_setSupportedAttachments(Set supportedAttachments) { + fabric_supportedAttachments = supportedAttachments; + } + + @Override + public Set fabric_getSupportedAttachments() { + return fabric_supportedAttachments; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/CustomPayloadS2CPacketAccessor.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/CustomPayloadS2CPacketAccessor.java new file mode 100644 index 000000000..d85df8ceb --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/CustomPayloadS2CPacketAccessor.java @@ -0,0 +1,30 @@ +/* + * 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.gen.Accessor; + +import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket; + +@Mixin(CustomPayloadC2SPacket.class) +public interface CustomPayloadS2CPacketAccessor { + @Accessor("MAX_PAYLOAD_SIZE") + static int getMaxPayloadSize() { + throw new UnsupportedOperationException("Implemented via mixin"); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java index d7b6bddd4..1bfffcccf 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityMixin.java @@ -25,12 +25,23 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import net.minecraft.entity.Entity; import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.world.World; +import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; @Mixin(Entity.class) abstract class EntityMixin implements AttachmentTargetImpl { + @Shadow + private int id; + @Shadow public abstract World getWorld(); @@ -49,4 +60,33 @@ abstract class EntityMixin implements AttachmentTargetImpl { private void writeEntityAttachments(NbtCompound nbt, CallbackInfoReturnable cir) { this.fabric_writeAttachmentsToNbt(nbt, getWorld().getRegistryManager()); } + + @Override + public AttachmentTargetInfo fabric_getSyncTargetInfo() { + return new AttachmentTargetInfo.EntityTarget(this.id); + } + + @Override + public void fabric_syncChange(AttachmentType type, AttachmentSyncPayloadS2C payload) { + if (!this.getWorld().isClient()) { + AttachmentSyncPredicate predicate = ((AttachmentTypeImpl) type).syncPredicate(); + + if ((Object) this instanceof ServerPlayerEntity self && predicate.test(this, self)) { + // Players do not track themselves + AttachmentSync.trySync(payload, self); + } + + PlayerLookup.tracking((Entity) (Object) this) + .forEach(player -> { + if (predicate.test(this, player)) { + AttachmentSync.trySync(payload, player); + } + }); + } + } + + @Override + public boolean fabric_shouldTryToSync() { + return !this.getWorld().isClient(); + } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityTrackerEntryMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityTrackerEntryMixin.java new file mode 100644 index 000000000..500734de6 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/EntityTrackerEntryMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.attachment; + +import java.util.ArrayList; +import java.util.List; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +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.entity.Entity; +import net.minecraft.server.network.EntityTrackerEntry; +import net.minecraft.server.network.ServerPlayerEntity; + +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; + +@Mixin(EntityTrackerEntry.class) +abstract class EntityTrackerEntryMixin { + @Shadow + @Final + private Entity entity; + + @Inject( + method = "startTracking", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/entity/Entity;onStartedTrackingBy(Lnet/minecraft/server/network/ServerPlayerEntity;)V" + ) + ) + private void syncAttachmentsAfterSpawn(ServerPlayerEntity player, CallbackInfo ci) { + // mixin because the START_TRACKING event triggers before the spawn packet is sent to the client, + // whereas we want to modify the entity on the client + List changes = new ArrayList<>(); + ((AttachmentTargetImpl) this.entity).fabric_computeInitialSyncChanges(player, changes::add); + + if (!changes.isEmpty()) { + AttachmentChange.partitionAndSendPackets(changes, player); + } + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java index 8a6e8db2e..84c6849c8 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerWorldMixin.java @@ -23,18 +23,44 @@ 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.registry.DynamicRegistryManager; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.server.MinecraftServer; import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.MutableWorldProperties; import net.minecraft.world.PersistentState; +import net.minecraft.world.World; +import net.minecraft.world.dimension.DimensionType; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.impl.attachment.AttachmentPersistentState; +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; @Mixin(ServerWorld.class) -abstract class ServerWorldMixin { +abstract class ServerWorldMixin extends World implements AttachmentTargetImpl { @Shadow @Final private MinecraftServer server; + protected ServerWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, DynamicRegistryManager registryManager, RegistryEntry dimensionEntry, boolean isClient, boolean debugWorld, long seed, int maxChainedNeighborUpdates) { + super( + properties, + registryRef, + registryManager, + dimensionEntry, + isClient, + debugWorld, + seed, + maxChainedNeighborUpdates + ); + } + @Inject(at = @At("TAIL"), method = "") private void createAttachmentsPersistentState(CallbackInfo ci) { // Force persistent state creation @@ -46,4 +72,21 @@ abstract class ServerWorldMixin { ); world.getPersistentStateManager().getOrCreate(type, AttachmentPersistentState.ID); } + + @Override + public void fabric_syncChange(AttachmentType type, AttachmentSyncPayloadS2C payload) { + if ((Object) this instanceof ServerWorld serverWorld) { + PlayerLookup.world(serverWorld) + .forEach(player -> { + if (((AttachmentTypeImpl) type).syncPredicate().test(this, player)) { + AttachmentSync.trySync(payload, player); + } + }); + } + } + + @Override + public AttachmentTargetInfo fabric_getSyncTargetInfo() { + return AttachmentTargetInfo.WorldTarget.INSTANCE; + } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/VarIntsAccessor.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/VarIntsAccessor.java new file mode 100644 index 000000000..cea30c413 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/VarIntsAccessor.java @@ -0,0 +1,30 @@ +/* + * 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.gen.Accessor; + +import net.minecraft.network.encoding.VarInts; + +@Mixin(VarInts.class) +public interface VarIntsAccessor { + @Accessor("MAX_BYTES") + static int getMaxByteSize() { + throw new UnsupportedOperationException("implemented via mixin"); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java index f7c538698..4fae99a96 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldChunkMixin.java @@ -16,25 +16,71 @@ package net.fabricmc.fabric.mixin.attachment; +import java.util.Map; +import java.util.function.Consumer; + +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; 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.block.entity.BlockEntity; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.ProtoChunk; import net.minecraft.world.chunk.WorldChunk; -import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; @Mixin(WorldChunk.class) -public class WorldChunkMixin { - @Inject( - method = "(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/ProtoChunk;Lnet/minecraft/world/chunk/WorldChunk$EntityLoader;)V", - at = @At("TAIL") - ) - public void transferProtoChunkAttachement(ServerWorld world, ProtoChunk protoChunk, WorldChunk.EntityLoader entityLoader, CallbackInfo ci) { - AttachmentTargetImpl.transfer(protoChunk, (AttachmentTarget) this, false); +abstract class WorldChunkMixin extends AttachmentTargetsMixin implements AttachmentTargetImpl { + @Shadow + @Final + World world; + + @Shadow + public abstract Map getBlockEntities(); + + @Inject(method = "(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/ProtoChunk;Lnet/minecraft/world/chunk/WorldChunk$EntityLoader;)V", at = @At("TAIL")) + private void transferProtoChunkAttachement(ServerWorld world, ProtoChunk protoChunk, WorldChunk.EntityLoader entityLoader, CallbackInfo ci) { + AttachmentTargetImpl.transfer(protoChunk, this, false); + } + + @Override + public void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer changeOutput) { + super.fabric_computeInitialSyncChanges(player, changeOutput); + + for (BlockEntity be : this.getBlockEntities().values()) { + ((AttachmentTargetImpl) be).fabric_computeInitialSyncChanges(player, changeOutput); + } + } + + @Override + public void fabric_syncChange(AttachmentType type, AttachmentSyncPayloadS2C payload) { + if (this.world instanceof ServerWorld serverWorld) { + // can't shadow from Chunk because this already extends a supermixin + PlayerLookup.tracking(serverWorld, ((Chunk) (Object) this).getPos()) + .forEach(player -> { + if (((AttachmentTypeImpl) type).syncPredicate().test(this, player)) { + AttachmentSync.trySync(payload, player); + } + }); + } + } + + @Override + public boolean fabric_shouldTryToSync() { + return !this.world.isClient(); } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldMixin.java new file mode 100644 index 000000000..bc8a89c69 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WorldMixin.java @@ -0,0 +1,35 @@ +/* + * 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.Shadow; + +import net.minecraft.world.World; + +import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; + +@Mixin(World.class) +abstract class WorldMixin implements AttachmentTargetImpl { + @Shadow + public abstract boolean isClient(); + + @Override + public boolean fabric_shouldTryToSync() { + return !this.isClient(); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java index 6412fe2d5..870dd5521 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/WrapperProtoChunkMixin.java @@ -17,6 +17,7 @@ package net.fabricmc.fabric.mixin.attachment; import java.util.Map; +import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; @@ -25,14 +26,18 @@ import org.spongepowered.asm.mixin.Shadow; import net.minecraft.nbt.NbtCompound; import net.minecraft.registry.RegistryWrapper; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.world.chunk.WorldChunk; import net.minecraft.world.chunk.WrapperProtoChunk; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; +import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C; @Mixin(WrapperProtoChunk.class) -public class WrapperProtoChunkMixin implements AttachmentTargetImpl { +abstract class WrapperProtoChunkMixin extends AttachmentTargetsMixin { @Shadow @Final private WorldChunk wrapped; @@ -73,4 +78,29 @@ public class WrapperProtoChunkMixin implements AttachmentTargetImpl { public Map, ?> fabric_getAttachments() { return ((AttachmentTargetImpl) this.wrapped).fabric_getAttachments(); } + + @Override + public boolean fabric_shouldTryToSync() { + return ((AttachmentTargetImpl) wrapped).fabric_shouldTryToSync(); + } + + @Override + public void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer changeOutput) { + ((AttachmentTargetImpl) wrapped).fabric_computeInitialSyncChanges(player, changeOutput); + } + + @Override + public AttachmentTargetInfo fabric_getSyncTargetInfo() { + return ((AttachmentTargetImpl) wrapped).fabric_getSyncTargetInfo(); + } + + @Override + public void fabric_syncChange(AttachmentType type, AttachmentSyncPayloadS2C payload) { + ((AttachmentTargetImpl) wrapped).fabric_syncChange(type, payload); + } + + @Override + public void fabric_markChanged(AttachmentType type) { + ((AttachmentTargetImpl) wrapped).fabric_markChanged(type); + } } diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json index 788fef65f..4ac56820a 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.mixins.json @@ -6,10 +6,17 @@ "AttachmentTargetsMixin", "BannerBlockEntityMixin", "BlockEntityMixin", - "SerializedChunkMixin", + "ChunkDataSenderMixin", + "ChunkMixin", + "ClientConnectionMixin", + "CustomPayloadS2CPacketAccessor", "EntityMixin", + "EntityTrackerEntryMixin", + "SerializedChunkMixin", "ServerWorldMixin", + "VarIntsAccessor", "WorldChunkMixin", + "WorldMixin", "WrapperProtoChunkMixin" ], "injectors": { diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json b/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json index 8a92af1ae..7c04ab122 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric.mod.json @@ -18,7 +18,8 @@ "depends": { "fabricloader": ">=0.16.8", "fabric-entity-events-v1": "*", - "fabric-object-builder-api-v1": "*" + "fabric-object-builder-api-v1": "*", + "fabric-networking-api-v1": "*" }, "description": "Allows conveniently attaching data to existing game objects", "mixins": [ @@ -30,7 +31,11 @@ ], "entrypoints": { "main": [ - "net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint" + "net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint", + "net.fabricmc.fabric.impl.attachment.sync.AttachmentSync" + ], + "client": [ + "net.fabricmc.fabric.impl.attachment.client.AttachmentSyncClient" ] }, "custom": { @@ -39,7 +44,7 @@ "net/minecraft/class_2586": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"], "net/minecraft/class_2791": ["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"] + "net/minecraft/class_1937": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"] } } } diff --git a/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java b/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java index 7ec8530ad..f2973d8f1 100644 --- a/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java +++ b/fabric-data-attachment-api-v1/src/test/java/net/fabricmc/fabric/test/attachment/CommonAttachmentTests.java @@ -25,6 +25,7 @@ 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.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -82,6 +83,12 @@ public class CommonAttachmentTests { Bootstrap.initialize(); } + private static T mockAndDisableSync(Class cl) { + T target = mock(cl, CALLS_REAL_METHODS); + doReturn(false).when((AttachmentTargetImpl) target).fabric_shouldTryToSync(); + return target; + } + @Test void testTargets() { AttachmentType basic = AttachmentRegistry.create(Identifier.of(MOD_ID, "basic_attachment")); @@ -90,14 +97,14 @@ public class CommonAttachmentTests { * 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); + ServerWorld serverWorld = mockAndDisableSync(ServerWorld.class); + Entity entity = mockAndDisableSync(Entity.class); + BlockEntity blockEntity = mockAndDisableSync(BlockEntity.class); - WorldChunk worldChunk = mock(WorldChunk.class, CALLS_REAL_METHODS); + WorldChunk worldChunk = mockAndDisableSync(WorldChunk.class); worldChunk.setUnsavedListener(pos -> { }); - ProtoChunk protoChunk = mock(ProtoChunk.class, CALLS_REAL_METHODS); + ProtoChunk protoChunk = mockAndDisableSync(ProtoChunk.class); for (AttachmentTarget target : new AttachmentTarget[]{serverWorld, entity, blockEntity, worldChunk, protoChunk}) { testForTarget(target, basic); @@ -130,7 +137,7 @@ public class CommonAttachmentTests { Identifier.of(MOD_ID, "defaulted_attachment"), () -> 0 ); - Entity target = mock(Entity.class, CALLS_REAL_METHODS); + Entity target = mockAndDisableSync(Entity.class); assertFalse(target.hasAttached(defaulted)); assertEquals(0, target.getAttachedOrCreate(defaulted)); @@ -188,12 +195,12 @@ public class CommonAttachmentTests { AttachmentType copiedOnRespawn = AttachmentRegistry.create(Identifier.of(MOD_ID, "copied_on_respawn"), AttachmentRegistry.Builder::copyOnDeath); - Entity original = mock(Entity.class, CALLS_REAL_METHODS); + Entity original = mockAndDisableSync(Entity.class); original.setAttached(notCopiedOnRespawn, true); original.setAttached(copiedOnRespawn, true); - Entity respawnTarget = mock(Entity.class, CALLS_REAL_METHODS); - Entity nonRespawnTarget = mock(Entity.class, CALLS_REAL_METHODS); + Entity respawnTarget = mockAndDisableSync(Entity.class); + Entity nonRespawnTarget = mockAndDisableSync(Entity.class); AttachmentTargetImpl.transfer(original, respawnTarget, true); AttachmentTargetImpl.transfer(original, nonRespawnTarget, false); @@ -241,7 +248,7 @@ public class CommonAttachmentTests { @Test void testWorldPersistentState() { // Trying to simulate actual saving and loading for the world is too hard - ServerWorld world = mock(ServerWorld.class, CALLS_REAL_METHODS); + ServerWorld world = mockAndDisableSync(ServerWorld.class); AttachmentPersistentState state = new AttachmentPersistentState(world); assertFalse(world.hasAttached(PERSISTENT)); @@ -249,7 +256,7 @@ public class CommonAttachmentTests { world.setAttached(PERSISTENT, expected); NbtCompound fakeSave = state.writeNbt(new NbtCompound(), mockDRM()); - world = mock(ServerWorld.class, CALLS_REAL_METHODS); + world = mockAndDisableSync(ServerWorld.class); AttachmentPersistentState.read(world, fakeSave, mockDRM()); assertTrue(world.hasAttached(PERSISTENT)); assertEquals(expected, world.getAttached(PERSISTENT)); diff --git a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java index 5623c9ac9..2b68408a4 100644 --- a/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java +++ b/fabric-data-attachment-api-v1/src/testmod/java/net/fabricmc/fabric/test/attachment/AttachmentTestMod.java @@ -16,21 +16,36 @@ package net.fabricmc.fabric.test.attachment; +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + import java.io.File; import java.io.IOException; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.serialization.Codec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.command.argument.BlockPosArgumentType; +import net.minecraft.command.argument.ColumnPosArgumentType; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.network.codec.PacketCodecs; import net.minecraft.registry.Registries; import net.minecraft.registry.Registry; import net.minecraft.registry.RegistryKey; import net.minecraft.registry.RegistryKeys; +import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.WorldSavePath; import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.ColumnPos; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.ChunkStatus; import net.minecraft.world.chunk.ProtoChunk; @@ -41,9 +56,12 @@ import net.minecraft.world.gen.feature.DefaultFeatureConfig; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate; +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; @@ -57,6 +75,35 @@ public class AttachmentTestMod implements ModInitializer { public static final AttachmentType FEATURE_ATTACHMENT = AttachmentRegistry.create( Identifier.of(MOD_ID, "feature") ); + public static final AttachmentType SYNCED_WITH_ALL = AttachmentRegistry.create( + Identifier.of(MOD_ID, "synced_all"), + builder -> builder + .initializer(() -> false) + .persistent(Codec.BOOL) + .syncWith(PacketCodecs.BOOL.cast(), AttachmentSyncPredicate.all()) + ); + public static final AttachmentType SYNCED_WITH_TARGET = AttachmentRegistry.create( + Identifier.of(MOD_ID, "synced_target"), + builder -> builder + .initializer(() -> false) + .persistent(Codec.BOOL) + .syncWith(PacketCodecs.BOOL.cast(), AttachmentSyncPredicate.targetOnly()) + ); + public static final AttachmentType SYNCED_EXCEPT_TARGET = AttachmentRegistry.create( + Identifier.of(MOD_ID, "synced_except_target"), + builder -> builder + .initializer(() -> false) + .persistent(Codec.BOOL) + .syncWith(PacketCodecs.BOOL.cast(), AttachmentSyncPredicate.allButTarget()) + ); + public static final AttachmentType SYNCED_CREATIVE_ONLY = AttachmentRegistry.create( + Identifier.of(MOD_ID, "synced_custom"), + builder -> builder + .initializer(() -> false) + .persistent(Codec.BOOL) + .syncWith(PacketCodecs.BOOL.cast(), (target, player) -> player.isCreative()) + ); + public static final SimpleCommandExceptionType TARGET_NOT_FOUND = new SimpleCommandExceptionType(Text.literal("Target not found")); public static final ChunkPos FAR_CHUNK_POS = new ChunkPos(300, 0); @@ -103,6 +150,7 @@ public class AttachmentTestMod implements ModInitializer { overworld.setAttached(PERSISTENT, "world_data"); chunk.setAttached(PERSISTENT, "chunk_data"); + chunk.setAttached(SYNCED_WITH_ALL, true); ProtoChunk protoChunk = (ProtoChunk) overworld.getChunkManager().getChunk(FAR_CHUNK_POS.x, FAR_CHUNK_POS.z, ChunkStatus.STRUCTURE_STARTS, true); protoChunk.setAttached(PERSISTENT, "protochunk_data"); @@ -140,5 +188,62 @@ public class AttachmentTestMod implements ModInitializer { if (!"protochunk_data".equals(chunk.getAttached(PERSISTENT))) throw new AssertionError("ProtoChunk attachment was not transfered to WorldChunk"); })); + + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register( + literal("attachment") + .then(buildCommandForKind("all", "all", SYNCED_WITH_ALL)) + .then(buildCommandForKind("self_only", "only self", SYNCED_WITH_TARGET)) + .then(buildCommandForKind("others_only", "all but self", SYNCED_EXCEPT_TARGET)) + .then(buildCommandForKind("creative_only", "creative players only", SYNCED_CREATIVE_ONLY)) + )); + } + + private static LiteralArgumentBuilder buildCommandForKind(String id, String syncedWith, AttachmentType type) { + return literal(id).executes(context -> updateAttachmentFor( + context.getSource().getPlayerOrThrow(), + type, + context, + "Set self flag (synced with %s) to %%s".formatted(syncedWith) + )).then( + argument("target", EntityArgumentType.entity()).executes(context -> updateAttachmentFor( + EntityArgumentType.getEntity(context, "target"), + type, + context, + "Set entity flag (synced with %s) to %%s".formatted(syncedWith) + )) + ).then(argument("pos", BlockPosArgumentType.blockPos()).executes(context -> { + BlockEntity be = context.getSource().getWorld().getBlockEntity(BlockPosArgumentType.getBlockPos(context, "pos")); + + if (be == null) { + throw TARGET_NOT_FOUND.create(); + } + + return updateAttachmentFor( + be, + type, + context, + "Set block entity flag (synced with %s) to %%s".formatted(syncedWith) + ); + })).then(argument("chunkPos", ColumnPosArgumentType.columnPos()).executes(context -> { + ColumnPos pos = ColumnPosArgumentType.getColumnPos(context, "chunkpos"); + return updateAttachmentFor( + context.getSource().getWorld().getChunk(pos.x(), pos.z(), ChunkStatus.STRUCTURE_STARTS, true), + type, + context, + "Set chunk flag (synced with %s) to %%s".formatted(syncedWith) + ); + })).then(literal("world").executes(context -> updateAttachmentFor( + context.getSource().getWorld(), + type, + context, + "Set world flag (synced with %s) to %%s".formatted(syncedWith) + ))); + } + + private static int updateAttachmentFor(AttachmentTarget target, AttachmentType attachment, CommandContext context, String messageFormat) throws CommandSyntaxException { + boolean current = target.getAttachedOrElse(attachment, false); + target.setAttached(attachment, !current); + context.getSource().sendFeedback(() -> Text.literal(messageFormat.formatted(!current)), false); + return 1; } } diff --git a/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json b/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json index 28224b760..c4f42d5ca 100644 --- a/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json +++ b/fabric-data-attachment-api-v1/src/testmod/resources/fabric.mod.json @@ -8,18 +8,26 @@ "depends": { "fabric-data-attachment-api-v1": "*", "fabric-lifecycle-events-v1": "*", - "fabric-biome-api-v1": "*" + "fabric-biome-api-v1": "*", + "fabric-command-api-v2": "*" }, "entrypoints": { "main": [ "net.fabricmc.fabric.test.attachment.AttachmentTestMod" ], + "client": [ + "net.fabricmc.fabric.test.attachment.client.AttachmentTestModClient" + ], "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" + "fabric-data-attachment-api-v1-testmod.mixins.json", + { + "config": "fabric-data-attachment-api-v1-testmod.client.mixins.json", + "environment": "client" + } ] } diff --git a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/AttachmentTestModClient.java b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/AttachmentTestModClient.java new file mode 100644 index 000000000..3f8e7068e --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/AttachmentTestModClient.java @@ -0,0 +1,208 @@ +/* + * 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.client; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +import java.util.UUID; +import java.util.function.Function; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.command.argument.BlockPosArgumentType; +import net.minecraft.command.argument.DefaultPosArgument; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.command.argument.UuidArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.chunk.Chunk; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.fabric.test.attachment.AttachmentTestMod; +import net.fabricmc.fabric.test.attachment.client.mixin.ClientWorldAccessor; +import net.fabricmc.fabric.test.attachment.client.mixin.DefaultPosArgumentAccessor; + +public class AttachmentTestModClient implements ClientModInitializer { + private static AbstractClientPlayerEntity parseClientPlayer(FabricClientCommandSource source, String name) throws CommandSyntaxException { + for (AbstractClientPlayerEntity player : source.getWorld().getPlayers()) { + if (name.equals(player.getName().getLiteralString())) { + return player; + } + } + + throw EntityArgumentType.PLAYER_NOT_FOUND_EXCEPTION.create(); + } + + private static BlockPos getBlockPos(CommandContext context, String argName) { + DefaultPosArgumentAccessor posArg = (DefaultPosArgumentAccessor) context.getArgument(argName, DefaultPosArgument.class); + Vec3d pos = context.getSource().getPosition(); + return BlockPos.ofFloored(new Vec3d( + posArg.getX().toAbsoluteCoordinate(pos.x), + posArg.getY().toAbsoluteCoordinate(pos.y), + posArg.getZ().toAbsoluteCoordinate(pos.z) + )); + } + + private static void displayClientAttachmentInfo( + CommandContext context, + T target, + Function nameGetter + ) { + context.getSource().sendFeedback( + Text.literal("Attachments for target %s:".formatted(nameGetter.apply(target))) + ); + boolean attAll = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_WITH_ALL); + context.getSource().sendFeedback( + Text.literal("Synced-with-all attachment: %s".formatted(attAll)).withColor(attAll ? Colors.GREEN : Colors.WHITE) + ); + boolean attTarget = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_WITH_TARGET); + context.getSource().sendFeedback( + Text.literal("Synced-with-target attachment: %s".formatted(attTarget)) + .withColor(attTarget ? target == MinecraftClient.getInstance().player ? Colors.GREEN : Colors.RED : Colors.WHITE) + ); + boolean attOther = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_EXCEPT_TARGET); + context.getSource().sendFeedback( + Text.literal("Synced-with-non-targets attachment: %s".formatted(attOther)) + .withColor(attOther ? target != MinecraftClient.getInstance().player ? Colors.GREEN : Colors.RED : Colors.WHITE) + ); + boolean attCustom = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_CREATIVE_ONLY); + context.getSource().sendFeedback( + Text.literal("Synced-with-creative attachment: %s".formatted(attCustom)) + .withColor(attCustom ? target instanceof PlayerEntity p && p.isCreative() ? Colors.GREEN : Colors.RED : Colors.WHITE) + ); + } + + @Override + public void onInitializeClient() { + ClientCommandRegistrationCallback.EVENT.register( + (dispatcher, registryAccess) -> dispatcher.register( + literal("attachment_test") + .executes(context -> { + displayClientAttachmentInfo( + context, + context.getSource().getPlayer(), + PlayerEntity::getNameForScoreboard + ); + return 1; + }) + .then(chain( + context -> { + displayClientAttachmentInfo( + context, + parseClientPlayer(context.getSource(), StringArgumentType.getString(context, "target")), + PlayerEntity::getNameForScoreboard + ); + return 1; + }, + literal("player"), + argument("target", StringArgumentType.word()) + )) + .then(chain( + context -> { + UUID uuid = context.getArgument("uuid", UUID.class); + Entity entity = ((ClientWorldAccessor) context.getSource().getWorld()) + .invokeGetEntityLookup() + .get(uuid); + + if (entity == null) { + throw AttachmentTestMod.TARGET_NOT_FOUND.create(); + } + + displayClientAttachmentInfo(context, entity, e -> uuid.toString()); + return 1; + }, + literal("entity"), + argument("uuid", UuidArgumentType.uuid()) + )) + .then(chain( + context -> { + BlockPos pos = getBlockPos(context, "pos"); + BlockEntity be = context.getSource() + .getWorld() + .getBlockEntity(pos); + + if (be == null) { + throw AttachmentTestMod.TARGET_NOT_FOUND.create(); + } + + displayClientAttachmentInfo( + context, + be, + b -> pos.toShortString() + ); + return 1; + }, + literal("blockentity"), + argument("pos", BlockPosArgumentType.blockPos()) + )) + .then(chain( + context -> { + BlockPos pos = getBlockPos(context, "pos"); + Chunk chunk = context.getSource().getWorld().getChunk(pos); + displayClientAttachmentInfo( + context, + chunk, + c -> c.getPos().toString() + ); + return 1; + }, + literal("chunk"), + argument("pos", BlockPosArgumentType.blockPos()) + )) + .then(literal("world").executes( + context -> { + displayClientAttachmentInfo( + context, + context.getSource().getWorld(), + w -> "world" + ); + return 1; + } + )) + ) + ); + } + + @SafeVarargs + private static ArgumentBuilder chain( + Command command, + ArgumentBuilder... nodes + ) { + ArgumentBuilder result = nodes[nodes.length - 1].executes(command); + + for (int i = nodes.length - 2; i >= 0; i--) { + result = nodes[i].then(result); + } + + return result; + } +} diff --git a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/mixin/ClientWorldAccessor.java b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/mixin/ClientWorldAccessor.java new file mode 100644 index 000000000..23557cbf9 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/mixin/ClientWorldAccessor.java @@ -0,0 +1,30 @@ +/* + * 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.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.world.entity.EntityLookup; + +@Mixin(ClientWorld.class) +public interface ClientWorldAccessor { + @Invoker + EntityLookup invokeGetEntityLookup(); +} diff --git a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/mixin/DefaultPosArgumentAccessor.java b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/mixin/DefaultPosArgumentAccessor.java new file mode 100644 index 000000000..fb4db0bbf --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/mixin/DefaultPosArgumentAccessor.java @@ -0,0 +1,35 @@ +/* + * 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.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.command.argument.CoordinateArgument; +import net.minecraft.command.argument.DefaultPosArgument; + +@Mixin(DefaultPosArgument.class) +public interface DefaultPosArgumentAccessor { + @Accessor("x") + CoordinateArgument getX(); + + @Accessor("y") + CoordinateArgument getY(); + + @Accessor("z") + CoordinateArgument getZ(); +} diff --git a/fabric-data-attachment-api-v1/src/testmodClient/resources/fabric-data-attachment-api-v1-testmod.client.mixins.json b/fabric-data-attachment-api-v1/src/testmodClient/resources/fabric-data-attachment-api-v1-testmod.client.mixins.json new file mode 100644 index 000000000..f8303427d --- /dev/null +++ b/fabric-data-attachment-api-v1/src/testmodClient/resources/fabric-data-attachment-api-v1-testmod.client.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.test.attachment.client.mixin", + "compatibilityLevel": "JAVA_17", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "ClientWorldAccessor" + ], + "mixins": [ + "DefaultPosArgumentAccessor" + ] +}