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..7d49458a16 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/client/java/net/fabricmc/fabric/mixin/attachment/client/ClientLevelMixin.java @@ -0,0 +1,49 @@ +/* + * 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; +import net.fabricmc.fabric.api.attachment.v1.GlobalAttachmentsProvider; + +@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 ((GlobalAttachmentsProvider) 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/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 new file mode 100644 index 0000000000..b5f07fe04d --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachments.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.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 new file mode 100644 index 0000000000..0df302b5b5 --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/api/attachment/v1/GlobalAttachmentsProvider.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * 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!"); + } +} 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..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 @@ -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,39 @@ 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(), () -> "AttachmentSavedData @ 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, () -> "AttachmentSavedData @ " + level.dimension().identifier()); + } + // TODO 1.21.5 look at making this more idiomatic + private static Codec codec(AttachmentTargetImpl target, ProblemReporter.PathElement reporterContext) { 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 +76,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 +87,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..d22f938e6c --- /dev/null +++ b/fabric-data-attachment-api-v1/src/main/java/net/fabricmc/fabric/impl/attachment/GlobalAttachmentsImpl.java @@ -0,0 +1,76 @@ +/* + * 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.minecraft.server.network.ServerGamePacketListenerImpl; + +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. + 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); + } + } + }); + } + } + + @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 c2803bf85e..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 @@ -149,6 +149,8 @@ public void onInitialize() { 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 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..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 @@ -46,11 +46,12 @@ 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}) +@Mixin({BlockEntity.class, Entity.class, Level.class, ChunkAccess.class, GlobalAttachmentsImpl.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..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 @@ -3,3 +3,5 @@ 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 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 2a50c31cac..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 @@ -7,13 +7,14 @@ "BannerBlockEntityMixin", "BlockEntityMixin", "ChunkAccessMixin", - "ClientboundCustomPayloadPacketAccessor", "ChunkHolderMixin", + "ClientboundCustomPayloadPacketAccessor", "DimensionStorageFileFixMixin", "EntityMixin", "ImposterProtoChunkMixin", "LevelChunkMixin", "LevelMixin", + "MinecraftServerMixin", "PlayerChunkSenderMixin", "SerializableChunkDataMixin", "ServerLevelMixin", 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..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 @@ -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; @@ -69,6 +70,7 @@ 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; @@ -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. */ + GlobalAttachmentsImpl globalAttachments = mockAndDisableSync(GlobalAttachmentsImpl.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,35 @@ void testWorldPersistentState() { assertEquals(expected, level.getAttached(PERSISTENT)); } + @Test + void testGlobalSavedData() { + RegistryAccess.Frozen ra = mockFrozenRA(); + + MinecraftServer server = mock(MinecraftServer.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)); + 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); + 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)); + assertEquals(expected, globalAttachments.getAttached(PERSISTENT)); + } + @Test void applyToInvalidTarget() throws AttachmentSyncException { RegistryAccess ra = mockRA(); @@ -332,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; + } } 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); });