From a126cd6f61424e91b0da0629feec0b173c34e638 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Thu, 5 Feb 2026 13:31:49 +0800 Subject: [PATCH 1/9] initial implementation --- .../attachment/client/ClientLevelMixin.java | 48 +++++++++++++ .../client/ClientPacketListenerMixin.java | 21 +++++- ...-data-attachment-api-v1.client.mixins.json | 3 + .../api/attachment/v1/GlobalAttachments.java | 20 ++++++ .../v1/GlobalAttachmentsProvider.java | 23 ++++++ .../impl/attachment/AttachmentSavedData.java | 35 +++++---- .../attachment/GlobalAttachmentsImpl.java | 72 +++++++++++++++++++ .../impl/attachment/sync/AttachmentSync.java | 20 ++++++ .../attachment/sync/AttachmentTargetInfo.java | 30 ++++++++ .../attachment/AttachmentTargetsMixin.java | 3 +- .../fabric/mixin/attachment/LevelMixin.java | 3 +- .../attachment/MinecraftServerMixin.java | 62 ++++++++++++++++ .../mixin/attachment/ServerLevelMixin.java | 6 ++ .../lang/en_us.json | 1 + ...fabric-data-attachment-api-v1.classtweaker | 3 + .../fabric-data-attachment-api-v1.mixins.json | 3 +- 16 files changed, 337 insertions(+), 16 deletions(-) create mode 100644 fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java create mode 100644 fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java create mode 100644 fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java create mode 100644 fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java create mode 100644 fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/MinecraftServerMixin.java diff --git a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java new file mode 100644 index 0000000000..96b8ecd453 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.attachment.client; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.WritableLevelData; + +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; + +@Mixin(ClientLevel.class) +abstract class ClientLevelMixin extends Level { + @Shadow + @Final + private ClientPacketListener connection; + + protected ClientLevelMixin(WritableLevelData levelData, ResourceKey dimension, RegistryAccess registryAccess, Holder dimensionTypeRegistration, boolean isClientSide, boolean isDebug, long biomeZoomSeed, int maxChainedNeighborUpdates) { + super(levelData, dimension, registryAccess, dimensionTypeRegistration, isClientSide, isDebug, biomeZoomSeed, maxChainedNeighborUpdates); + } + + @Override + public GlobalAttachments globalAttachments() { + return connection.globalAttachments(); + } +} diff --git a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPacketListenerMixin.java b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPacketListenerMixin.java index e3024244ac..cdae6c21bf 100644 --- a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPacketListenerMixin.java +++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientPacketListenerMixin.java @@ -21,18 +21,37 @@ import com.llamalad7.mixinextras.sugar.Local; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Slice; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.player.LocalPlayer; import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachmentsProvider; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.GlobalAttachmentsImpl; @Mixin(ClientPacketListener.class) -abstract class ClientPacketListenerMixin { +abstract class ClientPacketListenerMixin implements GlobalAttachmentsProvider { + @Unique + private GlobalAttachmentsImpl globalAttachments; + + @Override + public GlobalAttachments globalAttachments() { + return globalAttachments; + } + + @Inject(method = "", at = @At("TAIL")) + private void initGlobalAttachments(CallbackInfo ci) { + globalAttachments = new GlobalAttachmentsImpl(null); + } + @WrapOperation( method = "handleRespawn", at = @At( 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 d90d77d019..03d963e7c0 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 @@ -10,5 +10,8 @@ }, "client": [ "ClientPacketListenerMixin" + ], + "mixins": [ + "ClientLevelMixin" ] } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java new file mode 100644 index 0000000000..8a9a00e473 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface GlobalAttachments extends AttachmentTarget { +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java new file mode 100644 index 0000000000..f756a6fbeb --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java @@ -0,0 +1,23 @@ +/* + * 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; + +public interface GlobalAttachmentsProvider { + default GlobalAttachments globalAttachments() { + throw new UnsupportedOperationException("Implemented via mixin!"); + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java index 18200ee32c..393724226b 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java @@ -28,6 +28,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.ProblemReporter; import net.minecraft.world.level.saveddata.SavedData; @@ -35,31 +36,41 @@ import net.minecraft.world.level.storage.TagValueOutput; import net.minecraft.world.level.storage.ValueInput; +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; + /** - * Backing storage for server-side level attachments. + * Backing storage for server-side global and level attachments. * Thanks to custom {@link #isDirty()} logic, the file is only written if something needs to be persisted. */ public class AttachmentSavedData extends SavedData { private static final Logger LOGGER = LoggerFactory.getLogger(AttachmentSavedData.class); public static final Identifier ID = Identifier.fromNamespaceAndPath("fabric", "attachments"); - private final AttachmentTargetImpl levelTarget; + private final AttachmentTargetImpl target; private final boolean wasSerialized; - public AttachmentSavedData(ServerLevel level) { - this.levelTarget = (AttachmentTargetImpl) level; - this.wasSerialized = levelTarget.fabric_hasPersistentAttachments(); + public AttachmentSavedData(AttachmentTarget target) { + this.target = (AttachmentTargetImpl) target; + this.wasSerialized = this.target.fabric_hasPersistentAttachments(); + } + + public static Codec codec(MinecraftServer server) { + return codec((AttachmentTargetImpl) server.globalAttachments(), "global server attachments"); } - // TODO 1.21.5 look at making this more idiomatic public static Codec codec(ServerLevel level) { - final ProblemReporter.PathElement reporterContext = () -> "AttachmentSavedData @ " + level.dimension().identifier(); + return codec((AttachmentTargetImpl) level, level.dimension().identifier().toString()); + } + + // TODO 1.21.5 look at making this more idiomatic + private static Codec codec(AttachmentTargetImpl target, String reportIdentifier) { + final ProblemReporter.PathElement reporterContext = () -> "AttachmentSavedData @ " + reportIdentifier; return Codec.of(new Encoder<>() { @Override public DataResult encode(AttachmentSavedData input, DynamicOps ops, T prefix) { try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(reporterContext, LOGGER)) { TagValueOutput output = TagValueOutput.createWithoutContext(reporter); - ((AttachmentTargetImpl) level).fabric_writeAttachmentsToNbt(output); + target.fabric_writeAttachmentsToNbt(output); return DataResult.success(NbtOps.INSTANCE.convertTo(ops, output.buildResult())); } } @@ -67,9 +78,9 @@ public DataResult encode(AttachmentSavedData input, DynamicOps ops, T @Override public DataResult> decode(DynamicOps ops, T input) { try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(reporterContext, LOGGER)) { - ValueInput valueInput = TagValueInput.create(reporter, level.registryAccess(), (CompoundTag) ops.convertTo(NbtOps.INSTANCE, input)); - ((AttachmentTargetImpl) level).fabric_readAttachmentsFromNbt(valueInput); - return DataResult.success(Pair.of(new AttachmentSavedData(level), ops.empty())); + ValueInput valueInput = TagValueInput.create(reporter, target.fabric_getRegistryAccess(), (CompoundTag) ops.convertTo(NbtOps.INSTANCE, input)); + target.fabric_readAttachmentsFromNbt(valueInput); + return DataResult.success(Pair.of(new AttachmentSavedData(target), ops.empty())); } } }); @@ -78,6 +89,6 @@ public DataResult> decode(DynamicOps ops, T @Override public boolean isDirty() { // Only write data if there are attachments, or if we previously wrote data. - return wasSerialized || levelTarget.fabric_hasPersistentAttachments(); + return wasSerialized || target.fabric_hasPersistentAttachments(); } } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java new file mode 100644 index 0000000000..1234974211 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.attachment; + +import org.jspecify.annotations.Nullable; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.MinecraftServer; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; +import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; + +public class GlobalAttachmentsImpl implements GlobalAttachments, AttachmentTargetImpl { + @Nullable + private final MinecraftServer server; + + public GlobalAttachmentsImpl(@Nullable MinecraftServer server) { + this.server = server; + } + + @Override + public void fabric_syncChange(AttachmentType type, AttachmentChange change) { + if (server != null) { + // We don't use PlayerLookup.all() because when a player respawns, + // there is a brief period where said player will not be in the server player list. + // If a global attachment is set then, the respawning player will never receive the update. + // Though that is quite an edge case and idk if this is really worth it. + AttachmentSync.packetListeners.forEach(serverGamePacketListener -> { + if (((AttachmentTypeImpl) type).syncPredicate().test(this, serverGamePacketListener.player)) { + AttachmentSync.trySync(change, serverGamePacketListener.player); + } + }); + } + } + + @Override + public boolean fabric_shouldTryToSync() { + return server != null; + } + + @Override + public AttachmentTargetInfo fabric_getSyncTargetInfo() { + return AttachmentTargetInfo.GlobalTarget.INSTANCE; + } + + @Override + public RegistryAccess fabric_getRegistryAccess() { + if (server != null) { + return server.registryAccess(); + } + + // only used for deserializing on the server and syncing, so should not be possible to get here. + throw new UnsupportedOperationException("GlobalAttachments does not have a registry access on the client side."); + } +} 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 index 8a992ebd3c..48d691bde3 100644 --- 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 @@ -17,6 +17,7 @@ package net.fabricmc.fabric.impl.attachment.sync; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -32,6 +33,7 @@ import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.ConfigurationTask; +import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; @@ -40,6 +42,7 @@ 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; @@ -55,6 +58,7 @@ public class AttachmentSync implements ModInitializer { public static final int MAX_PADDING_SIZE_IN_BYTES = AttachmentTargetInfo.MAX_SIZE_IN_BYTES + MAX_IDENTIFIER_SIZE; public static final int DEFAULT_MAX_DATA_SIZE; public static final int DEFAULT_ATTACHMENT_SYNC_PACKET_SIZE; + public static final Set packetListeners = new HashSet<>(); static { // ensure no splitting by default @@ -150,6 +154,22 @@ public void onInitialize() { PayloadTypeRegistry.clientboundPlay().registerLarge( ClientboundAttachmentSyncPayload.TYPE, ClientboundAttachmentSyncPayload.CODEC, AttachmentRegistryImpl::getMaxSyncPacketSize); + ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> { + packetListeners.add(listener); + + // player may not be fully loaded yet, but since these are global attachments it doesn't really matter. + List changes = new ArrayList<>(); + ((AttachmentTargetImpl) server.globalAttachments()).fabric_computeInitialSyncChanges(listener.player, changes::add); + + if (!changes.isEmpty()) { + trySync(changes, listener.player); + } + }); + + ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> { + packetListeners.remove(listener); + }); + ServerPlayerEvents.JOIN.register((player) -> { List changes = new ArrayList<>(); // sync level attachments 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 index 8ae72fe1ea..9f56160a28 100644 --- 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 @@ -36,6 +36,7 @@ import net.minecraft.world.level.chunk.status.ChunkStatus; import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; public sealed interface AttachmentTargetInfo { int MAX_SIZE_IN_BYTES = Byte.BYTES + Long.BYTES; @@ -60,6 +61,7 @@ record Type(byte id, StreamCodec> 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, LevelTarget.PACKET_CODEC); + static Type GLOBAL = new Type<>((byte) 4, GlobalTarget.PACKET_CODEC); public Type { TYPES.put(id, this); @@ -195,4 +197,32 @@ public void appendDebugInformation(MutableComponent component) { .append(CommonComponents.NEW_LINE); } } + + final class GlobalTarget implements AttachmentTargetInfo { + public static final GlobalTarget INSTANCE = new GlobalTarget(); + static final StreamCodec PACKET_CODEC = StreamCodec.unit(INSTANCE); + + private GlobalTarget() { + } + + @Override + public Type getType() { + return Type.GLOBAL; + } + + @Override + public AttachmentTarget getTarget(Level level) { + return level.globalAttachments(); + } + + @Override + public void appendDebugInformation(MutableComponent component) { + component + .append(Component.translatable( + "fabric-data-attachment-api-v1.unknown-target.target-type", + Component.translatable("fabric-data-attachment-api-v1.unknown-target.target-type.global").withStyle(ChatFormatting.YELLOW) + )) + .append(CommonComponents.NEW_LINE); + } + } } 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 2b13697b26..5fd39f15ee 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 @@ -41,6 +41,7 @@ import net.minecraft.world.level.storage.ValueOutput; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; @@ -50,7 +51,7 @@ import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; -@Mixin({BlockEntity.class, Entity.class, Level.class, ChunkAccess.class}) +@Mixin({BlockEntity.class, Entity.class, Level.class, ChunkAccess.class, GlobalAttachments.class}) abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { @Unique @Nullable diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/LevelMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/LevelMixin.java index b6c2c0b711..593e417744 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/LevelMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/LevelMixin.java @@ -22,10 +22,11 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.world.level.Level; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachmentsProvider; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; @Mixin(Level.class) -abstract class LevelMixin implements AttachmentTargetImpl { +abstract class LevelMixin implements AttachmentTargetImpl, GlobalAttachmentsProvider { @Shadow public abstract boolean isClientSide(); diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/MinecraftServerMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/MinecraftServerMixin.java new file mode 100644 index 0000000000..4680687db6 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/MinecraftServerMixin.java @@ -0,0 +1,62 @@ +/* + * 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 org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.saveddata.SavedDataType; +import net.minecraft.world.level.storage.SavedDataStorage; + +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachmentsProvider; +import net.fabricmc.fabric.impl.attachment.AttachmentSavedData; +import net.fabricmc.fabric.impl.attachment.GlobalAttachmentsImpl; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServerMixin implements GlobalAttachmentsProvider { + @Shadow + @Final + private SavedDataStorage savedDataStorage; + @Unique + private GlobalAttachmentsImpl globalAttachments; + + @Inject(method = "", at = @At("TAIL")) + private void initGlobalAttachments(CallbackInfo ci) { + MinecraftServer server = (MinecraftServer) (Object) this; + globalAttachments = new GlobalAttachmentsImpl(server); + + var type = new SavedDataType<>( + AttachmentSavedData.ID, + () -> new AttachmentSavedData(globalAttachments), + AttachmentSavedData.codec(server), + null + ); + savedDataStorage.computeIfAbsent(type); + } + + @Override + public GlobalAttachments globalAttachments() { + return globalAttachments; + } +} diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerLevelMixin.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerLevelMixin.java index cccf4a261f..90ba99cad5 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerLevelMixin.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/mixin/attachment/ServerLevelMixin.java @@ -34,6 +34,7 @@ import net.minecraft.world.level.storage.WritableLevelData; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.impl.attachment.AttachmentSavedData; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; @@ -95,4 +96,9 @@ public AttachmentTargetInfo fabric_getSyncTargetInfo() { public RegistryAccess fabric_getRegistryAccess() { return registryAccess(); } + + @Override + public GlobalAttachments globalAttachments() { + return server.globalAttachments(); + } } diff --git a/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/lang/en_us.json b/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/lang/en_us.json index 0d4e8360d4..1112a8cc50 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/lang/en_us.json +++ b/fabric-data-attachment-api-v1/src/main/resources/assets/fabric-data-attachment-api-v1/lang/en_us.json @@ -8,6 +8,7 @@ "fabric-data-attachment-api-v1.unknown-target.target-type.block-entity": "Block entity", "fabric-data-attachment-api-v1.unknown-target.target-type.chunk": "Chunk", "fabric-data-attachment-api-v1.unknown-target.target-type.entity": "Entity", + "fabric-data-attachment-api-v1.unknown-target.target-type.global": "Global", "fabric-data-attachment-api-v1.unknown-target.target-type.level": "Level", "fabric-data-attachment-api-v1.unknown-target.title": "Received attachment change for unknown target!" } diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker index d12013819c..a2f473baab 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker @@ -3,3 +3,6 @@ transitive-inject-interface net/minecraft/world/level/block/entity/BlockEntity n transitive-inject-interface net/minecraft/world/level/chunk/ChunkAccess net/fabricmc/fabric/api/attachment/v1/AttachmentTarget transitive-inject-interface net/minecraft/world/entity/Entity net/fabricmc/fabric/api/attachment/v1/AttachmentTarget transitive-inject-interface net/minecraft/world/level/Level net/fabricmc/fabric/api/attachment/v1/AttachmentTarget +transitive-inject-interface net/minecraft/server/MinecraftServer net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider +transitive-inject-interface net/minecraft/world/level/Level net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider +transitive-inject-interface net/minecraft/client/multiplayer/ClientPacketListener net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider 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 674bd0c47e..873eb50879 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 @@ -7,14 +7,15 @@ "BannerBlockEntityMixin", "BlockEntityMixin", "ChunkAccessMixin", - "ClientboundCustomPayloadPacketAccessor", "ChunkHolderMixin", + "ClientboundCustomPayloadPacketAccessor", "ConnectionMixin", "DimensionStorageFileFixMixin", "EntityMixin", "ImposterProtoChunkMixin", "LevelChunkMixin", "LevelMixin", + "MinecraftServerMixin", "PlayerChunkSenderMixin", "SerializableChunkDataMixin", "ServerLevelMixin", From 86d39e3ab7a8bb39859bbffa7b0451cff8f9d874 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Thu, 5 Feb 2026 13:35:33 +0800 Subject: [PATCH 2/9] fix AttachmentTargetsMixin error --- .../fabric/mixin/attachment/AttachmentTargetsMixin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5fd39f15ee..1fdc8fb9bb 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 @@ -41,17 +41,17 @@ import net.minecraft.world.level.storage.ValueOutput; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; -import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; 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.GlobalAttachmentsImpl; import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync; import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; -@Mixin({BlockEntity.class, Entity.class, Level.class, ChunkAccess.class, GlobalAttachments.class}) +@Mixin({BlockEntity.class, Entity.class, Level.class, ChunkAccess.class, GlobalAttachmentsImpl.class}) abstract class AttachmentTargetsMixin implements AttachmentTargetImpl { @Unique @Nullable From 7132132d0f30f65667dd2b2aa90bfcdc24cefd3f Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Thu, 5 Feb 2026 14:00:44 +0800 Subject: [PATCH 3/9] add some tests --- .../attachment/CommonAttachmentTests.java | 33 +++++++++++++++++-- .../client/gametest/PersistenceGametest.java | 2 ++ .../client/gametest/SyncGametest.java | 4 +++ 3 files changed, 37 insertions(+), 2 deletions(-) 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 da40e82550..24f6342e3d 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 @@ -46,6 +46,7 @@ import net.minecraft.resources.Identifier; import net.minecraft.resources.RegistryOps; import net.minecraft.server.Bootstrap; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.ProblemReporter; @@ -66,6 +67,7 @@ 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.attachment.v1.GlobalAttachments; import net.fabricmc.fabric.impl.attachment.AttachmentSavedData; import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; @@ -112,6 +114,7 @@ void testTargets() { * 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 ServerLevel and LevelChunk to work. */ + GlobalAttachments globalAttachments = mockAndDisableSync(GlobalAttachments.class); ServerLevel serverLevel = mockAndDisableSync(ServerLevel.class); Entity entity = mockAndDisableSync(Entity.class); BlockEntity blockEntity = mockAndDisableSync(BlockEntity.class); @@ -121,7 +124,7 @@ void testTargets() { ProtoChunk protoChunk = mockAndDisableSync(ProtoChunk.class); - for (AttachmentTarget target : new AttachmentTarget[]{serverLevel, entity, blockEntity, levelChunk, protoChunk}) { + for (AttachmentTarget target : new AttachmentTarget[]{globalAttachments, serverLevel, entity, blockEntity, levelChunk, protoChunk}) { testForTarget(target, basic); } } @@ -278,7 +281,7 @@ void testBlockEntityPersistence() { } @Test - void testWorldPersistentState() { + void testLevelSavedData() { // Trying to simulate actual saving and loading for the world is too hard RegistryAccess ra = mockRA(); @@ -303,6 +306,32 @@ void testWorldPersistentState() { assertEquals(expected, level.getAttached(PERSISTENT)); } + @Test + void testGlobalSavedData() { + RegistryAccess.Frozen ra = mockRA().freeze(); + + MinecraftServer server = mock(MinecraftServer.class); + GlobalAttachments globalAttachments = mockAndDisableSync(GlobalAttachments.class); + when(server.registryAccess()).thenReturn(ra); + + AttachmentSavedData state = new AttachmentSavedData(globalAttachments); + assertFalse(globalAttachments.hasAttached(PERSISTENT)); + assertFalse(state.isDirty()); + + int expected = 1; + globalAttachments.setAttached(PERSISTENT, expected); + assertTrue(state.isDirty()); + CompoundTag fakeSave = (CompoundTag) AttachmentSavedData.codec(server).encodeStart(RegistryOps.create(NbtOps.INSTANCE, ra), state).getOrThrow(); + assertEquals("{\"fabric:attachments\":{\"example:persistent\":1}}", fakeSave.toString()); + + server = mock(MinecraftServer.class); + when(server.registryAccess()).thenReturn(ra); + + AttachmentSavedData.codec(server).decode(RegistryOps.create(NbtOps.INSTANCE, ra), fakeSave).getOrThrow(); + assertTrue(globalAttachments.hasAttached(PERSISTENT)); + assertEquals(expected, globalAttachments.getAttached(PERSISTENT)); + } + @Test void applyToInvalidTarget() throws AttachmentSyncException { RegistryAccess ra = mockRA(); diff --git a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/PersistenceGametest.java b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/PersistenceGametest.java index 1f1b372a17..cc3771927c 100644 --- a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/PersistenceGametest.java +++ b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/PersistenceGametest.java @@ -83,6 +83,7 @@ public void runTest(ClientGameTestContext context) { ); // setting up persistent attachments for second run + server.globalAttachments().setAttached(PERSISTENT, "global_data"); getSinglePlayer(server).setAttached(PERSISTENT, "player_data"); overworld.setAttached(PERSISTENT, "level_data"); originChunk.setAttached(PERSISTENT, "chunk_data"); @@ -105,6 +106,7 @@ public void runTest(ClientGameTestContext context) { ServerLevel overworld = server.overworld(); LevelChunk originChunk = overworld.getChunk(0, 0); + assertAttached(server.globalAttachments(), PERSISTENT, "global_data", "Global attachment did not persist"); assertAttached(getSinglePlayer(server), PERSISTENT, "player_data", "Player attachment did not persist"); assertAttached(overworld, PERSISTENT, "level_data", "Level attachment did not persist"); assertAttached(originChunk, PERSISTENT, "chunk_data", "LevelChunk attachment did not persist"); diff --git a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/SyncGametest.java b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/SyncGametest.java index a35391b1d5..e87dac12d3 100644 --- a/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/SyncGametest.java +++ b/fabric-data-attachment-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/attachment/client/gametest/SyncGametest.java @@ -120,6 +120,8 @@ public void runTest(ClientGameTestContext context) { ServerLevel nether = server.getLevel(Level.NETHER); setSyncedWithAll(Objects.requireNonNull(nether)); + + setSyncedWithAll(server.globalAttachments()); }); LOGGER.info("Joining dedicated server"); @@ -157,6 +159,7 @@ public void runTest(ClientGameTestContext context) { assertHasSyncedWithAll(villager); assertHasSyncedWithAll(level.getChunk(0, 0)); assertHasSyncedWithAll(client.player); + assertHasSyncedWithAll(level.globalAttachments()); assertHasSynced(client.player, AttachmentTestMod.SYNCED_CREATIVE_ONLY); assertHasSynced(client.player, AttachmentTestMod.SYNCED_ITEM); assertHasSynced(villager, AttachmentTestMod.SYNCED_LARGE); @@ -187,6 +190,7 @@ public void runTest(ClientGameTestContext context) { LOGGER.info("Testing synced attachments (2/2)"); context.runOnClient(client -> { assertHasSyncedWithAll(client.level); + assertHasSyncedWithAll(client.level.globalAttachments()); // asserts the removal wasn't synced assertPresence(client.player, AttachmentTestMod.SYNCED_CREATIVE_ONLY, true); }); From f561a6ce8fc8cde1a5233a40623b0b6c888092f4 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Thu, 5 Feb 2026 15:05:47 +0800 Subject: [PATCH 4/9] fix tests --- .../impl/attachment/AttachmentSavedData.java | 8 +++----- .../test/attachment/CommonAttachmentTests.java | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java index 393724226b..3aa07cb4ba 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/AttachmentSavedData.java @@ -54,17 +54,15 @@ public AttachmentSavedData(AttachmentTarget target) { } public static Codec codec(MinecraftServer server) { - return codec((AttachmentTargetImpl) server.globalAttachments(), "global server attachments"); + return codec((AttachmentTargetImpl) server.globalAttachments(), () -> "AttachmentSavedData @ global server attachments"); } public static Codec codec(ServerLevel level) { - return codec((AttachmentTargetImpl) level, level.dimension().identifier().toString()); + return codec((AttachmentTargetImpl) level, () -> "AttachmentSavedData @ " + level.dimension().identifier()); } // TODO 1.21.5 look at making this more idiomatic - private static Codec codec(AttachmentTargetImpl target, String reportIdentifier) { - final ProblemReporter.PathElement reporterContext = () -> "AttachmentSavedData @ " + reportIdentifier; - + private static Codec codec(AttachmentTargetImpl target, ProblemReporter.PathElement reporterContext) { return Codec.of(new Encoder<>() { @Override public DataResult encode(AttachmentSavedData input, DynamicOps ops, T prefix) { 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 24f6342e3d..ebdd78928f 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 @@ -67,10 +67,10 @@ 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.attachment.v1.GlobalAttachments; import net.fabricmc.fabric.impl.attachment.AttachmentSavedData; import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl; import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl; +import net.fabricmc.fabric.impl.attachment.GlobalAttachmentsImpl; import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange; import net.fabricmc.fabric.impl.attachment.sync.AttachmentSyncException; import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo; @@ -114,7 +114,7 @@ void testTargets() { * 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 ServerLevel and LevelChunk to work. */ - GlobalAttachments globalAttachments = mockAndDisableSync(GlobalAttachments.class); + GlobalAttachmentsImpl globalAttachments = mockAndDisableSync(GlobalAttachmentsImpl.class); ServerLevel serverLevel = mockAndDisableSync(ServerLevel.class); Entity entity = mockAndDisableSync(Entity.class); BlockEntity blockEntity = mockAndDisableSync(BlockEntity.class); @@ -308,11 +308,12 @@ void testLevelSavedData() { @Test void testGlobalSavedData() { - RegistryAccess.Frozen ra = mockRA().freeze(); + RegistryAccess.Frozen ra = mockFrozenRA(); MinecraftServer server = mock(MinecraftServer.class); - GlobalAttachments globalAttachments = mockAndDisableSync(GlobalAttachments.class); + GlobalAttachmentsImpl globalAttachments = new GlobalAttachmentsImpl(server); when(server.registryAccess()).thenReturn(ra); + when(server.globalAttachments()).thenReturn(globalAttachments); AttachmentSavedData state = new AttachmentSavedData(globalAttachments); assertFalse(globalAttachments.hasAttached(PERSISTENT)); @@ -325,7 +326,9 @@ void testGlobalSavedData() { assertEquals("{\"fabric:attachments\":{\"example:persistent\":1}}", fakeSave.toString()); server = mock(MinecraftServer.class); + globalAttachments = new GlobalAttachmentsImpl(server); when(server.registryAccess()).thenReturn(ra); + when(server.globalAttachments()).thenReturn(globalAttachments); AttachmentSavedData.codec(server).decode(RegistryOps.create(NbtOps.INSTANCE, ra), fakeSave).getOrThrow(); assertTrue(globalAttachments.hasAttached(PERSISTENT)); @@ -361,4 +364,10 @@ private static RegistryAccess mockRA() { when(ra.createSerializationContext(any())).thenReturn((RegistryOps) (Object) RegistryOps.create(NbtOps.INSTANCE, ra)); return ra; } + + private static RegistryAccess.Frozen mockFrozenRA() { + RegistryAccess.Frozen ra = mock(RegistryAccess.Frozen.class); + when(ra.createSerializationContext(any())).thenReturn((RegistryOps) (Object) RegistryOps.create(NbtOps.INSTANCE, ra)); + return ra; + } } From 61c81b779341f783db3cd60a68be9d6520ea6463 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Tue, 24 Feb 2026 22:11:42 +0800 Subject: [PATCH 5/9] remove transitive interface injection for ClientPacketListener --- .../fabric/mixin/attachment/client/ClientLevelMixin.java | 3 ++- .../main/resources/fabric-data-attachment-api-v1.classtweaker | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java index 96b8ecd453..7d49458a16 100644 --- a/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java +++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java @@ -30,6 +30,7 @@ import net.minecraft.world.level.storage.WritableLevelData; import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachmentsProvider; @Mixin(ClientLevel.class) abstract class ClientLevelMixin extends Level { @@ -43,6 +44,6 @@ protected ClientLevelMixin(WritableLevelData levelData, ResourceKey dimen @Override public GlobalAttachments globalAttachments() { - return connection.globalAttachments(); + return ((GlobalAttachmentsProvider) connection).globalAttachments(); } } diff --git a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker index a2f473baab..0fd93ce8d8 100644 --- a/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker +++ b/fabric-data-attachment-api-v1/src/main/resources/fabric-data-attachment-api-v1.classtweaker @@ -5,4 +5,3 @@ transitive-inject-interface net/minecraft/world/entity/Entity net/fabricmc/fabri transitive-inject-interface net/minecraft/world/level/Level net/fabricmc/fabric/api/attachment/v1/AttachmentTarget transitive-inject-interface net/minecraft/server/MinecraftServer net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider transitive-inject-interface net/minecraft/world/level/Level net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider -transitive-inject-interface net/minecraft/client/multiplayer/ClientPacketListener net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider From 62ef014525f93bac53cc283acbd99aeeeab321a2 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Tue, 24 Feb 2026 22:12:00 +0800 Subject: [PATCH 6/9] use IdentityHashMap for player connections --- .../impl/attachment/GlobalAttachmentsImpl.java | 3 +-- .../fabric/impl/attachment/sync/AttachmentSync.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java index 1234974211..2bf25af4d3 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java @@ -41,8 +41,7 @@ public void fabric_syncChange(AttachmentType type, AttachmentChange change) { // We don't use PlayerLookup.all() because when a player respawns, // there is a brief period where said player will not be in the server player list. // If a global attachment is set then, the respawning player will never receive the update. - // Though that is quite an edge case and idk if this is really worth it. - AttachmentSync.packetListeners.forEach(serverGamePacketListener -> { + AttachmentSync.getPlayerConnections().forEach(serverGamePacketListener -> { if (((AttachmentTypeImpl) type).syncPredicate().test(this, serverGamePacketListener.player)) { AttachmentSync.trySync(change, serverGamePacketListener.player); } 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 index 48d691bde3..44d6b298a3 100644 --- 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 @@ -17,7 +17,8 @@ package net.fabricmc.fabric.impl.attachment.sync; import java.util.ArrayList; -import java.util.HashSet; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -58,7 +59,7 @@ public class AttachmentSync implements ModInitializer { public static final int MAX_PADDING_SIZE_IN_BYTES = AttachmentTargetInfo.MAX_SIZE_IN_BYTES + MAX_IDENTIFIER_SIZE; public static final int DEFAULT_MAX_DATA_SIZE; public static final int DEFAULT_ATTACHMENT_SYNC_PACKET_SIZE; - public static final Set packetListeners = new HashSet<>(); + private static final Set playerConnections = Collections.newSetFromMap(new IdentityHashMap<>()); static { // ensure no splitting by default @@ -123,6 +124,10 @@ private static Set decodeResponsePayload( return atts; } + public static Set getPlayerConnections() { + return Collections.unmodifiableSet(playerConnections); + } + @Override public void onInitialize() { // Config @@ -155,7 +160,7 @@ public void onInitialize() { ClientboundAttachmentSyncPayload.TYPE, ClientboundAttachmentSyncPayload.CODEC, AttachmentRegistryImpl::getMaxSyncPacketSize); ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> { - packetListeners.add(listener); + playerConnections.add(listener); // player may not be fully loaded yet, but since these are global attachments it doesn't really matter. List changes = new ArrayList<>(); @@ -167,7 +172,7 @@ public void onInitialize() { }); ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> { - packetListeners.remove(listener); + playerConnections.remove(listener); }); ServerPlayerEvents.JOIN.register((player) -> { From e5485819361632ab3ed7af5d636ff0895a3fda08 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Tue, 24 Feb 2026 22:18:09 +0800 Subject: [PATCH 7/9] woops readd ClientboundCustomPayloadPacketAccessor to mixins.json --- .../src/main/resources/fabric-data-attachment-api-v1.mixins.json | 1 + 1 file changed, 1 insertion(+) 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 c6f0c8c8d4..0d53f8c364 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 @@ -8,6 +8,7 @@ "BlockEntityMixin", "ChunkAccessMixin", "ChunkHolderMixin", + "ClientboundCustomPayloadPacketAccessor", "DimensionStorageFileFixMixin", "EntityMixin", "ImposterProtoChunkMixin", From 192412a4acf4ddd2680af4a480332db2e385aac8 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Wed, 25 Feb 2026 08:37:28 +0800 Subject: [PATCH 8/9] add proper docs to new API --- .../api/attachment/v1/AttachmentTarget.java | 5 ++++- .../api/attachment/v1/GlobalAttachments.java | 15 +++++++++++++++ .../attachment/v1/GlobalAttachmentsProvider.java | 5 +++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java index 8f5382b513..cc52f0584c 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/AttachmentTarget.java @@ -35,7 +35,10 @@ /** * Marks all objects on which data can be attached using {@link AttachmentType}s. * - *

Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerLevel} and {@link ChunkAccess} via mixin.

+ *

+ * Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerLevel} and {@link ChunkAccess} via mixin. + * Additionally, {@link GlobalAttachments} also implements this for server-wide data attachments. + *

* *

Note about {@link BlockEntity} and {@link ChunkAccess} targets: these objects need to be notified of changes to their * state (using {@link BlockEntity#setChanged()} and {@link ChunkAccess#markUnsaved()} respectively), otherwise the modifications will not take effect properly. diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java index 8a9a00e473..b5f07fe04d 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.java @@ -16,5 +16,20 @@ package net.fabricmc.fabric.api.attachment.v1; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; + +/** + * An {@link AttachmentTarget} representing global (server-wide) data attachments that are not tied + * to any specific {@link Level Level}. + * + *

This target can be obtained via {@link Level#globalAttachments()}, which returns the appropriate instance + * on either side. Additionally, {@link MinecraftServer#globalAttachments()} can be used on the server-side + * for convenience. + * + *

On the server, the lifecycle of this target is bound to the lifecycle of the {@link MinecraftServer} + * while on the client it is bound to {@code ClientPacketListener} and should only be accessed when in a world + * (when {@code Minecraft.getInstance().level} is not null). + */ public interface GlobalAttachments extends AttachmentTarget { } diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java index f756a6fbeb..0df302b5b5 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java @@ -16,6 +16,11 @@ package net.fabricmc.fabric.api.attachment.v1; +/** + * Interface to obtain {@link GlobalAttachments} from {@link net.minecraft.world.level.Level Level} + * and {@link net.minecraft.server.MinecraftServer MinecraftServer}. + */ +// Internally, also implemented on ClientPacketListener for use in ClientLevelMixin. public interface GlobalAttachmentsProvider { default GlobalAttachments globalAttachments() { throw new UnsupportedOperationException("Implemented via mixin!"); From 558f97ebb8673c1b83984c2bd6a00acaa75ae791 Mon Sep 17 00:00:00 2001 From: Dennis Ochulor Date: Wed, 25 Feb 2026 20:32:21 +0800 Subject: [PATCH 9/9] incremental sync global attachments using ServerConnectionListener --- .../attachment/GlobalAttachmentsImpl.java | 11 +++++--- .../impl/attachment/sync/AttachmentSync.java | 27 ++----------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java index 2bf25af4d3..d22f938e6c 100644 --- a/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java @@ -20,6 +20,7 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.fabricmc.fabric.api.attachment.v1.AttachmentType; import net.fabricmc.fabric.api.attachment.v1.GlobalAttachments; @@ -41,9 +42,13 @@ public void fabric_syncChange(AttachmentType type, AttachmentChange change) { // We don't use PlayerLookup.all() because when a player respawns, // there is a brief period where said player will not be in the server player list. // If a global attachment is set then, the respawning player will never receive the update. - AttachmentSync.getPlayerConnections().forEach(serverGamePacketListener -> { - if (((AttachmentTypeImpl) type).syncPredicate().test(this, serverGamePacketListener.player)) { - AttachmentSync.trySync(change, serverGamePacketListener.player); + server.getConnection().getConnections().forEach(connection -> { + // if packet listener is not ServerGamePacketListenerImpl, then player is not in PLAY phase yet + // initial sync will handle it + if (connection.getPacketListener() instanceof ServerGamePacketListenerImpl serverGamePacketListener) { + if (((AttachmentTypeImpl) type).syncPredicate().test(this, serverGamePacketListener.player)) { + AttachmentSync.trySync(change, serverGamePacketListener.player); + } } }); } 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 index 030df7cdfc..2a7b1b0a22 100644 --- 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 @@ -17,8 +17,6 @@ package net.fabricmc.fabric.impl.attachment.sync; import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -33,7 +31,6 @@ import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.ConfigurationTask; -import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; @@ -42,7 +39,6 @@ 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.api.networking.v1.context.PacketContext; import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint; @@ -58,7 +54,6 @@ public class AttachmentSync implements ModInitializer { public static final int MAX_PADDING_SIZE_IN_BYTES = AttachmentTargetInfo.MAX_SIZE_IN_BYTES + MAX_IDENTIFIER_SIZE; public static final int DEFAULT_MAX_DATA_SIZE; public static final int DEFAULT_ATTACHMENT_SYNC_PACKET_SIZE; - private static final Set playerConnections = Collections.newSetFromMap(new IdentityHashMap<>()); private static final PacketContext.Key> SUPPORTED_ATTACHMENTS_KEY = PacketContext.key(Identifier.fromNamespaceAndPath("fabric", "supported_attachments")); static { @@ -122,10 +117,6 @@ private static Set decodeResponsePayload( return atts; } - public static Set getPlayerConnections() { - return Collections.unmodifiableSet(playerConnections); - } - @Override public void onInitialize() { // Config @@ -156,24 +147,10 @@ public void onInitialize() { PayloadTypeRegistry.clientboundPlay().registerLarge( ClientboundAttachmentSyncPayload.TYPE, ClientboundAttachmentSyncPayload.CODEC, AttachmentRegistryImpl::getMaxSyncPacketSize); - ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> { - playerConnections.add(listener); - - // player may not be fully loaded yet, but since these are global attachments it doesn't really matter. - List changes = new ArrayList<>(); - ((AttachmentTargetImpl) server.globalAttachments()).fabric_computeInitialSyncChanges(listener.player, changes::add); - - if (!changes.isEmpty()) { - trySync(changes, listener.player); - } - }); - - ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> { - playerConnections.remove(listener); - }); - ServerPlayerEvents.JOIN.register((player) -> { List changes = new ArrayList<>(); + // sync global attachments + ((AttachmentTargetImpl) player.level().globalAttachments()).fabric_computeInitialSyncChanges(player, changes::add); // sync level attachments ((AttachmentTargetImpl) player.level()).fabric_computeInitialSyncChanges(player, changes::add); // sync player's own persistent attachments that couldn't be synced earlier