Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Attachment Sync API #4049

Merged
merged 43 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cbd9f5f
API changes and packet classes
Syst3ms Aug 22, 2024
8ae0d17
Delineate new payload types
Syst3ms Aug 22, 2024
2446047
Bulk of the API
Syst3ms Aug 23, 2024
3757c71
Fix tests
Syst3ms Aug 23, 2024
21dd494
Merge all sync payload types
Syst3ms Aug 24, 2024
7ab67f2
Clean up mixins
Syst3ms Aug 24, 2024
2d61882
Add sync types and optimize
Syst3ms Aug 24, 2024
bcf2f53
Further simplifications
Syst3ms Aug 24, 2024
c1f8290
improve logic for sync on change
Syst3ms Aug 26, 2024
5d276f9
fix config phase logic
Syst3ms Aug 26, 2024
bbd6096
add attachment refresh packet
Syst3ms Aug 26, 2024
235b40c
Merge branch '1.21.1' into data-attachment-sync
Syst3ms Aug 26, 2024
4e47318
Shelve attachment refresh for now
Syst3ms Aug 27, 2024
3a0d2d3
Proper testmod
Syst3ms Aug 28, 2024
6862b45
Some requests
Syst3ms Aug 28, 2024
b49bf22
Rework client-supported attachments detection
Syst3ms Sep 4, 2024
613a9a9
Fixed issue with persistence and sync
Syst3ms Sep 4, 2024
4b34dc2
fixed other sync+persistence bug
Syst3ms Sep 5, 2024
2f9567c
checkstyle
Syst3ms Sep 7, 2024
d13233f
requests
Syst3ms Sep 7, 2024
e488165
more fixes
Syst3ms Sep 15, 2024
b25343e
fix immutability issue
Syst3ms Sep 15, 2024
77a8f5d
nits + doc
Syst3ms Sep 20, 2024
8027a71
simplify sync predicates
Syst3ms Sep 23, 2024
a4d2bd8
typo
Syst3ms Sep 23, 2024
6019ff0
fix sidedness issue
Syst3ms Sep 23, 2024
aab31f9
Merge branch '1.21.1' into data-attachment-sync
Syst3ms Sep 23, 2024
2416956
Merge branch '1.21.1' into data-attachment-sync
Syst3ms Sep 23, 2024
6e54721
nit
Syst3ms Sep 24, 2024
d8089a8
simplify testmod with commands
Syst3ms Sep 24, 2024
adc80c5
Merge branch '1.21.1' into data-attachment-sync
Syst3ms Sep 25, 2024
dcd3926
rename payload class
Syst3ms Sep 27, 2024
ea1e22f
fix critical bugs
Syst3ms Sep 28, 2024
68851d5
flesh out testmod
Syst3ms Sep 28, 2024
95c3d0e
cosmetic fixes
Syst3ms Sep 30, 2024
721617c
fix checks
Syst3ms Sep 30, 2024
b1d7b18
testmod successful
Syst3ms Oct 20, 2024
8bfabb7
Merge branch '1.21.1' into data-attachment-sync
Syst3ms Oct 20, 2024
3991d7b
Merge branch '1.21.1' into data-attachment-sync
Syst3ms Oct 26, 2024
2a7da02
use improved registration
Syst3ms Oct 26, 2024
541deac
javadoc fixes
Syst3ms Oct 26, 2024
14087a8
javadoc nit
Syst3ms Nov 3, 2024
058597f
requests
Syst3ms Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions fabric-data-attachment-api-v1/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ version = getSubprojectVersion(project)
moduleDependencies(project, [
'fabric-api-base',
':fabric-entity-events-v1',
':fabric-object-builder-api-v1'
':fabric-object-builder-api-v1',
':fabric-networking-api-v1'
])

testDependencies(project, [
':fabric-lifecycle-events-v1',
':fabric-biome-api-v1'
':fabric-biome-api-v1',
':fabric-command-api-v2',
':fabric-rendering-v1'
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.attachment.client;

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
import net.fabricmc.fabric.impl.attachment.sync.s2c.RequestAcceptedAttachmentsPayloadS2C;

public class AttachmentSyncClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// config
ClientConfigurationNetworking.registerGlobalReceiver(
RequestAcceptedAttachmentsPayloadS2C.ID,
(payload, context) -> context.responseSender().sendPacket(AttachmentSync.createResponsePayload())
);

// play
ClientPlayNetworking.registerGlobalReceiver(
AttachmentSyncPayloadS2C.ID,
(payload, context) -> payload.attachments().forEach(attachmentChange -> attachmentChange.apply(context.client().world))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"required": true,
"package": "net.fabricmc.fabric.mixin.attachment.client",
"compatibilityLevel": "JAVA_17",
"mixins": [
],
"injectors": {
"defaultRequire": 1
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import com.mojang.serialization.Codec;
import org.jetbrains.annotations.ApiStatus;

import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl;
Expand All @@ -45,9 +47,10 @@ private AttachmentRegistry() {
}

/**
* Creates <i>and registers</i> an attachment, configuring the builder used underneath.
* Creates <i>and registers</i> an attachment using a {@linkplain Builder builder}.
*
* @param id the identifier of this attachment
* @param consumer a lambda that configures a {@link Builder} for this attachment type
* @param <A> the type of attached data
* @return the registered {@link AttachmentType} instance
*/
Expand All @@ -60,7 +63,7 @@ public static <A> AttachmentType<A> create(Identifier id, Consumer<Builder<A>> c
}

/**
* Creates <i>and registers</i> an attachment. The data will not be persisted.
* Creates <i>and registers</i> an attachment. The data will not be persisted or synchronized.
*
* @param id the identifier of this attachment
* @param <A> the type of attached data
Expand Down Expand Up @@ -124,7 +127,7 @@ public interface Builder<A> {
Builder<A> persistent(Codec<A> codec);

/**
* Declares that when a player dies and respawns, the attachments corresponding of this type should remain.
* Declares that when a player dies and respawns, the attachments of this type should remain.
*
* @return the builder
*/
Expand All @@ -138,7 +141,7 @@ public interface Builder<A> {
* <p>It is <i>encouraged</i> for {@link A} to be an immutable data type, such as a primitive type
* or an immutable record.</p>
*
* <p>Otherwise, one must be very careful, as attachments <i>must not share any mutable state</i>.
* <p>Otherwise, it is important to ensure that attachments <i>do not share any mutable state</i>.
* As an example, for a (mutable) list/array attachment type,
* the initializer should create a new independent instance each time it is called.</p>
*
Expand All @@ -147,6 +150,15 @@ public interface Builder<A> {
*/
Builder<A> initializer(Supplier<A> initializer);

/**
* Declares that this attachment type may be automatically synchronized with some clients, as determined by {@code syncPredicate}.
*
* @param packetCodec the codec used to serialize the attachment data over the network
* @param syncPredicate an {@link AttachmentSyncPredicate} determining with which clients to synchronize data
* @return the builder
*/
AttachmentRegistry.Builder<A> syncWith(PacketCodec<PacketByteBuf, A> packetCodec, AttachmentSyncPredicate syncPredicate);

/**
* Builds and registers the {@link AttachmentType}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.attachment.v1;

import java.util.function.BiPredicate;

import org.jetbrains.annotations.ApiStatus;

import net.minecraft.server.network.ServerPlayerEntity;

/**
* A predicate that determines, for a specific attachment type, given the {@linkplain AttachmentTarget target} it is
* attached to an a {@link ServerPlayerEntity}, whether the data should be synchronized with the given player's client.
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved
*
* <p>The class extends {@link BiPredicate} to allow for custom predicates, outside the ones provided by methods.</p>
*/
@ApiStatus.NonExtendable
@FunctionalInterface
public interface AttachmentSyncPredicate extends BiPredicate<AttachmentTarget, ServerPlayerEntity> {
/**
* @return a predicate that syncs an attachment with all clients
*/
static AttachmentSyncPredicate all() {
return (t, p) -> true;
}

/**
* @return a predicate that syncs an attachment only with the target it is attached to, when that is a player. If the
* target isn't a player, the attachment will be synced with no clients.
*/
static AttachmentSyncPredicate targetOnly() {
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved
return (target, player) -> target == player;
}

/**
* @return a predicate that syncs an attachment with every client except the target it is attached to, when that is a player.
* When the target isn't a player, the attachment will be synced with all clients.
*/
static AttachmentSyncPredicate allButTarget() {
return (target, player) -> target != player;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.jetbrains.annotations.Nullable;

import net.minecraft.entity.Entity;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import net.minecraft.world.chunk.Chunk;
Expand All @@ -36,7 +37,7 @@
/**
* An attachment allows "attaching" arbitrary data to various game objects (entities, block entities, worlds and chunks at the moment).
* Use the methods provided in {@link AttachmentRegistry} to create and register attachments. Attachments can
* optionally be made to persist between restarts using a provided {@link Codec}.
* optionally be made to persist between restarts using a provided {@link Codec}, and to synchronize with player clients.
*
* <p>While the API places no restrictions on the types of data that can be attached, it is generally encouraged to use
* immutable types. More generally, different attachments <i>must not</i> share mutable state, and it is <i>strongly advised</i>
Expand All @@ -53,6 +54,9 @@
* </p>
*
* @param <A> type of the attached data. It is encouraged for this to be an immutable type.
* @see AttachmentRegistry
* @see AttachmentRegistry.Builder#persistent(Codec)
* @see AttachmentRegistry.Builder#syncWith(PacketCodec, AttachmentSyncPredicate)
*/
@ApiStatus.NonExtendable
@ApiStatus.Experimental
Expand Down Expand Up @@ -93,6 +97,15 @@ default boolean isPersistent() {
@Nullable
Supplier<A> initializer();

/**
* Whether this attachment type can be synchronized with clients. This method returning {@code true} does not in any way
* indicate that the attachment type will synchronize data with any given client, only that it is able to, as per its
* {@link AttachmentSyncPredicate}.
*
* @return whether this attachment type is synced
*/
boolean isSynced();
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved

/**
* @return whether the attachments should persist after an entity dies, for example when a player respawns or
* when a mob is converted (e.g. zombie → drowned)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,48 @@

package net.fabricmc.fabric.impl.attachment;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;

import com.mojang.serialization.Codec;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.util.Identifier;

import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry;
import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate;
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;

public final class AttachmentRegistryImpl {
private static final Logger LOGGER = LoggerFactory.getLogger("fabric-data-attachment-api-v1");
private static final Map<Identifier, AttachmentType<?>> attachmentRegistry = new HashMap<>();
private static final Set<Identifier> syncableAttachments = new HashSet<>();
private static final Set<Identifier> syncableView = Collections.unmodifiableSet(syncableAttachments);

public static <A> void register(Identifier id, AttachmentType<A> attachmentType) {
AttachmentType<?> existing = attachmentRegistry.put(id, attachmentType);

if (existing != null) {
LOGGER.warn("Encountered duplicate type registration for id {}", id);
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved

// Prevent duplicate registration from incorrectly overriding a synced type with a non-synced one
if (existing.isSynced()) {
syncableAttachments.remove(id);
}
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved
}

if (attachmentType.isSynced()) {
syncableAttachments.add(id);
}
}

Expand All @@ -48,6 +66,10 @@ public static AttachmentType<?> get(Identifier id) {
return attachmentRegistry.get(id);
}

public static Set<Identifier> getSyncableAttachments() {
return syncableView;
}

public static <A> AttachmentRegistry.Builder<A> builder() {
return new BuilderImpl<>();
}
Expand All @@ -57,6 +79,10 @@ public static class BuilderImpl<A> implements AttachmentRegistry.Builder<A> {
private Supplier<A> defaultInitializer = null;
@Nullable
private Codec<A> persistenceCodec = null;
@Nullable
private PacketCodec<PacketByteBuf, A> packetCodec = null;
@Nullable
private AttachmentSyncPredicate syncPredicate = null;
private boolean copyOnDeath = false;

@Override
Expand All @@ -81,11 +107,33 @@ public AttachmentRegistry.Builder<A> initializer(Supplier<A> initializer) {
return this;
}

public AttachmentRegistry.Builder<A> syncWith(PacketCodec<PacketByteBuf, A> packetCodec, AttachmentSyncPredicate syncPredicate) {
Objects.requireNonNull(packetCodec, "packet codec cannot be null");
Objects.requireNonNull(syncPredicate, "sync predicate cannot be null");

this.packetCodec = packetCodec;
this.syncPredicate = syncPredicate;
return this;
}

@Override
public AttachmentType<A> buildAndRegister(Identifier id) {
Objects.requireNonNull(id, "identifier cannot be null");

var attachment = new AttachmentTypeImpl<>(id, defaultInitializer, persistenceCodec, copyOnDeath);
if (syncPredicate != null && id.toString().length() >= AttachmentSync.MAX_IDENTIFIER_SIZE) {
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException(
"Identifier length is too long for a synced attachment type (max %d)".formatted(AttachmentSync.MAX_IDENTIFIER_SIZE)
Syst3ms marked this conversation as resolved.
Show resolved Hide resolved
);
}

var attachment = new AttachmentTypeImpl<>(
id,
defaultInitializer,
persistenceCodec,
packetCodec,
syncPredicate,
copyOnDeath
);
register(id, attachment);
return attachment;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static void serializeAttachmentData(NbtCompound nbt, RegistryWrapper.Wrap
RegistryOps<NbtElement> registryOps = wrapperLookup.getOps(NbtOps.INSTANCE);
codec.encodeStart(registryOps, entry.getValue())
.ifError(partial -> {
LOGGER.warn("Couldn't serialize attachment " + type.identifier() + ", skipping. Error:");
LOGGER.warn("Couldn't serialize attachment {}, skipping. Error:", type.identifier());
LOGGER.warn(partial.message());
})
.ifSuccess(serialized -> compound.put(type.identifier().toString(), serialized));
Expand All @@ -73,7 +73,7 @@ public static IdentityHashMap<AttachmentType<?>, Object> deserializeAttachmentDa
AttachmentType<?> type = AttachmentRegistryImpl.get(Identifier.of(key));

if (type == null) {
LOGGER.warn("Unknown attachment type " + key + " found when deserializing, skipping");
LOGGER.warn("Unknown attachment type {} found when deserializing, skipping", key);
continue;
}

Expand All @@ -83,7 +83,7 @@ public static IdentityHashMap<AttachmentType<?>, Object> deserializeAttachmentDa
RegistryOps<NbtElement> registryOps = wrapperLookup.getOps(NbtOps.INSTANCE);
codec.parse(registryOps, compound.get(key))
.ifError(partial -> {
LOGGER.warn("Couldn't deserialize attachment " + type.identifier() + ", skipping. Error:");
LOGGER.warn("Couldn't deserialize attachment {}, skipping. Error:", type.identifier());
LOGGER.warn(partial.message());
})
.ifSuccess(
Expand Down
Loading
Loading