Skip to content
Open
Original file line number Diff line number Diff line change
@@ -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<Level> dimension, RegistryAccess registryAccess, Holder<DimensionType> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<init>", at = @At("TAIL"))
private void initGlobalAttachments(CallbackInfo ci) {
globalAttachments = new GlobalAttachmentsImpl(null);
}

@WrapOperation(
method = "handleRespawn",
at = @At(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
},
"client": [
"ClientPacketListenerMixin"
],
"mixins": [
"ClientLevelMixin"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
/**
* Marks all objects on which data can be attached using {@link AttachmentType}s.
*
* <p>Fabric implements this on {@link Entity}, {@link BlockEntity}, {@link ServerLevel} and {@link ChunkAccess} via mixin.</p>
* <p>
* 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.
* </p>
*
* <p>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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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.
*
* <p>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 {
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,48 +28,57 @@
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;
import net.minecraft.world.level.storage.TagValueInput;
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<AttachmentSavedData> 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<AttachmentSavedData> 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<AttachmentSavedData> codec(AttachmentTargetImpl target, ProblemReporter.PathElement reporterContext) {
return Codec.of(new Encoder<>() {
@Override
public <T> DataResult<T> encode(AttachmentSavedData input, DynamicOps<T> 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()));
}
}
}, new Decoder<>() {
@Override
public <T> DataResult<Pair<AttachmentSavedData, T>> decode(DynamicOps<T> 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()));
}
}
});
Expand All @@ -78,6 +87,6 @@ public <T> DataResult<Pair<AttachmentSavedData, T>> decode(DynamicOps<T> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ public void onInitialize() {

ServerPlayerEvents.JOIN.register((player) -> {
List<AttachmentChange> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
int MAX_SIZE_IN_BYTES = Byte.BYTES + Long.BYTES;
Expand All @@ -60,6 +61,7 @@ record Type<T>(byte id, StreamCodec<ByteBuf, ? extends AttachmentTargetInfo<T>>
static Type<Entity> ENTITY = new Type<>((byte) 1, EntityTarget.PACKET_CODEC);
static Type<ChunkAccess> CHUNK = new Type<>((byte) 2, ChunkTarget.PACKET_CODEC);
static Type<Level> WORLD = new Type<>((byte) 3, LevelTarget.PACKET_CODEC);
static Type<GlobalAttachments> GLOBAL = new Type<>((byte) 4, GlobalTarget.PACKET_CODEC);

public Type {
TYPES.put(id, this);
Expand Down Expand Up @@ -195,4 +197,32 @@ public void appendDebugInformation(MutableComponent component) {
.append(CommonComponents.NEW_LINE);
}
}

final class GlobalTarget implements AttachmentTargetInfo<GlobalAttachments> {
public static final GlobalTarget INSTANCE = new GlobalTarget();
static final StreamCodec<ByteBuf, GlobalTarget> PACKET_CODEC = StreamCodec.unit(INSTANCE);

private GlobalTarget() {
}

@Override
public Type<GlobalAttachments> 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);
}
}
}
Loading
Loading