mirror of
https://github.com/FabricMC/fabric.git
synced 2024-11-24 00:28:10 -05:00
Data Attachment Sync API (#4049)
Completes the data attachment API with client-server syncing capabilities. ## Motivation The existing API works great for attaching data to game objects, be they serverside or clientside, but lacks any ability to synchronize between the two. A mod that wants to add a "thirst" mechanic can easily do so on the server side by attaching an integer to every player. However, the mod may also want to use this information to render additional HUD elements on the client. Currently, informing the client of this data can only be done manually, which is cumbersome, error-prone, and is much better-suited as an API feature. ## API Changes The API doesn't change a lot (at least compared to the implementation), with a few new methods and one new class. One new method has been added to `AttachmentRegistry.Builder`, namely `syncWith`. It declares that an attachment type may be synchronized with some clients, and takes a `PacketCodec` to encode attachment data over the network, as well as an element of the new `AttachmentSyncPredicate` interface. This interface extends `BiPredicate<AttachmentTarget, ServerPlayerEntity>` to allow for user-defined predicates, and provides some common presets: * `all()`: attachment data will be synchronized with all clients (that track the target). * `targetOnly()`: attachment data will only be synchronized with the target it is attached to, when it is a player. If the target is not a player, it won't be synchronized with any client. * `allButTarget()`: reverse of the above. For non-player targets, attachment data will be synchronized with all clients. **NOTE**: for a user-defined condition, whether attachment data is synchronized with a client can change at runtime (e.g. "sync only with operators" when a player changes operator status). A specialized method to "refresh" data was considered, but in the end discarded due to complexity. It falls to individual mods to handle these edge cases. AttachmentType also gets one new `isSynced` method to know whether a given attachment type can be synced. ## Usage Here is how one could register an attachment for a "thirst" meter like mentioned above. ```java public static final AttachmentType<Integer> THIRST = AttachmentRegistry.<Integer>builder() .initializer(() -> 20) // start with a default value like hunger .persistent(Codec.INT) // persist across restarts .syncWith(PacketCodecs.VAR_INT.cast(), AttachmentSyncPredicate.targetOnly()) // only the player's own client needs the value for rendering .buildAndRegister(Identifier.of("modid", "thirst")); ```
This commit is contained in:
parent
a9aef6a748
commit
e9d2cfc835
39 changed files with 1798 additions and 62 deletions
|
@ -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'
|
||||
])
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,6 @@
|
|||
"required": true,
|
||||
"package": "net.fabricmc.fabric.mixin.attachment.client",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
|
|
|
@ -22,6 +22,8 @@ import java.util.function.Supplier;
|
|||
import com.mojang.serialization.Codec;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl;
|
||||
|
@ -45,9 +47,10 @@ public final class AttachmentRegistry {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates <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
|
||||
*/
|
||||
|
@ -60,7 +63,7 @@ public final class AttachmentRegistry {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -124,7 +127,7 @@ public final class AttachmentRegistry {
|
|||
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
|
||||
*/
|
||||
|
@ -138,7 +141,7 @@ public final class AttachmentRegistry {
|
|||
* <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>
|
||||
*
|
||||
|
@ -147,6 +150,15 @@ public final class AttachmentRegistry {
|
|||
*/
|
||||
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}.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.api.attachment.v1;
|
||||
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
/**
|
||||
* A predicate that determines, for a specific attachment type, whether the data should be synchronized with a
|
||||
* player's client, given the player's {@link ServerPlayerEntity} and the {@linkplain AttachmentTarget} the data is linked to.
|
||||
*
|
||||
* <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() {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import org.jetbrains.annotations.ApiStatus;
|
|||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
@ -36,7 +37,7 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
|||
/**
|
||||
* An attachment allows "attaching" arbitrary data to various game objects (entities, block entities, worlds and chunks at the moment).
|
||||
* Use the methods provided in {@link AttachmentRegistry} to create and register attachments. Attachments can
|
||||
* optionally be made to persist between restarts using a provided {@link Codec}.
|
||||
* optionally be made to persist between restarts using a provided {@link Codec}, and to synchronize with player clients.
|
||||
*
|
||||
* <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>
|
||||
|
@ -53,6 +54,9 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
|||
* </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
|
||||
|
@ -93,6 +97,15 @@ public interface AttachmentType<A> {
|
|||
@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();
|
||||
|
||||
/**
|
||||
* @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)
|
||||
|
|
|
@ -16,9 +16,12 @@
|
|||
|
||||
package net.fabricmc.fabric.impl.attachment;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
|
@ -26,20 +29,35 @@ import org.jetbrains.annotations.Nullable;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;
|
||||
|
||||
public final class AttachmentRegistryImpl {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("fabric-data-attachment-api-v1");
|
||||
private static final Map<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);
|
||||
|
||||
// Prevent duplicate registration from incorrectly overriding a synced type with a non-synced one or vice-versa
|
||||
if (existing.isSynced() && !attachmentType.isSynced()) {
|
||||
syncableAttachments.remove(id);
|
||||
} else if (!existing.isSynced() && attachmentType.isSynced()) {
|
||||
syncableAttachments.add(id);
|
||||
}
|
||||
} else if (attachmentType.isSynced()) {
|
||||
syncableAttachments.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +66,10 @@ public final class AttachmentRegistryImpl {
|
|||
return attachmentRegistry.get(id);
|
||||
}
|
||||
|
||||
public static Set<Identifier> getSyncableAttachments() {
|
||||
return syncableView;
|
||||
}
|
||||
|
||||
public static <A> AttachmentRegistry.Builder<A> builder() {
|
||||
return new BuilderImpl<>();
|
||||
}
|
||||
|
@ -57,6 +79,10 @@ public final class AttachmentRegistryImpl {
|
|||
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
|
||||
|
@ -81,11 +107,36 @@ public final class AttachmentRegistryImpl {
|
|||
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) {
|
||||
throw new IllegalArgumentException(
|
||||
"Identifier length is too long for a synced attachment type (was %d, maximum is %d)".formatted(
|
||||
id.toString().length(),
|
||||
AttachmentSync.MAX_IDENTIFIER_SIZE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var attachment = new AttachmentTypeImpl<>(
|
||||
id,
|
||||
defaultInitializer,
|
||||
persistenceCodec,
|
||||
packetCodec,
|
||||
syncPredicate,
|
||||
copyOnDeath
|
||||
);
|
||||
register(id, attachment);
|
||||
return attachment;
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ public class AttachmentSerializingImpl {
|
|||
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));
|
||||
|
@ -73,7 +73,7 @@ public class AttachmentSerializingImpl {
|
|||
AttachmentType<?> type = AttachmentRegistryImpl.get(Identifier.of(key));
|
||||
|
||||
if (type == null) {
|
||||
LOGGER.warn("Unknown attachment type " + key + " found when deserializing, skipping");
|
||||
LOGGER.warn("Unknown attachment type {} found when deserializing, skipping", key);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ public class AttachmentSerializingImpl {
|
|||
RegistryOps<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(
|
||||
|
|
|
@ -17,14 +17,19 @@
|
|||
package net.fabricmc.fabric.impl.attachment;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.registry.RegistryWrapper;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
public interface AttachmentTargetImpl extends AttachmentTarget {
|
||||
/**
|
||||
|
@ -32,7 +37,7 @@ public interface AttachmentTargetImpl extends AttachmentTarget {
|
|||
* WorldChunk, and when an entity is respawned and a new instance is created. For entity respawns, it is
|
||||
* triggered on player respawn, entity conversion, return from the End, or cross-world entity teleportation.
|
||||
* In the first two cases, only the attachments with {@link AttachmentType#copyOnDeath()} will be transferred.
|
||||
*/
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static void transfer(AttachmentTarget original, AttachmentTarget target, boolean isDeath) {
|
||||
Map<AttachmentType<?>, ?> attachments = ((AttachmentTargetImpl) original).fabric_getAttachments();
|
||||
|
@ -66,4 +71,26 @@ public interface AttachmentTargetImpl extends AttachmentTarget {
|
|||
default boolean fabric_hasPersistentAttachments() {
|
||||
throw new UnsupportedOperationException("Implemented via mixin");
|
||||
}
|
||||
|
||||
default AttachmentTargetInfo<?> fabric_getSyncTargetInfo() {
|
||||
// this only makes sense for server objects
|
||||
throw new UnsupportedOperationException("Sync target info was not retrieved on server!");
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes changes that should be communicated to newcomers (i.e. clients that start tracking this target)
|
||||
*/
|
||||
default void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer<AttachmentChange> changeOutput) {
|
||||
throw new UnsupportedOperationException("Implemented via mixin");
|
||||
}
|
||||
|
||||
default void fabric_syncChange(AttachmentType<?> type, AttachmentSyncPayloadS2C payload) {
|
||||
}
|
||||
|
||||
default void fabric_markChanged(AttachmentType<?> type) {
|
||||
}
|
||||
|
||||
default boolean fabric_shouldTryToSync() {
|
||||
throw new UnsupportedOperationException("Implemented via mixin");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,23 @@ import java.util.function.Supplier;
|
|||
import com.mojang.serialization.Codec;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
|
||||
public record AttachmentTypeImpl<A>(
|
||||
Identifier identifier,
|
||||
@Nullable Supplier<A> initializer,
|
||||
@Nullable Codec<A> persistenceCodec,
|
||||
@Nullable PacketCodec<PacketByteBuf, A> packetCodec,
|
||||
@Nullable AttachmentSyncPredicate syncPredicate,
|
||||
boolean copyOnDeath
|
||||
) implements AttachmentType<A> { }
|
||||
) implements AttachmentType<A> {
|
||||
@Override
|
||||
public boolean isSynced() {
|
||||
return syncPredicate != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.network.codec.PacketCodecs;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
import net.fabricmc.fabric.mixin.attachment.CustomPayloadS2CPacketAccessor;
|
||||
import net.fabricmc.fabric.mixin.attachment.VarIntsAccessor;
|
||||
import net.fabricmc.fabric.mixin.networking.accessor.ServerCommonNetworkHandlerAccessor;
|
||||
|
||||
public record AttachmentChange(AttachmentTargetInfo<?> targetInfo, AttachmentType<?> type, byte[] data) {
|
||||
public static final PacketCodec<PacketByteBuf, AttachmentChange> PACKET_CODEC = PacketCodec.tuple(
|
||||
AttachmentTargetInfo.PACKET_CODEC, AttachmentChange::targetInfo,
|
||||
Identifier.PACKET_CODEC.xmap(
|
||||
id -> Objects.requireNonNull(AttachmentRegistryImpl.get(id)),
|
||||
AttachmentType::identifier
|
||||
), AttachmentChange::type,
|
||||
PacketCodecs.BYTE_ARRAY, AttachmentChange::data,
|
||||
AttachmentChange::new
|
||||
);
|
||||
private static final int MAX_PADDING_SIZE_IN_BYTES = AttachmentTargetInfo.MAX_SIZE_IN_BYTES + AttachmentSync.MAX_IDENTIFIER_SIZE;
|
||||
private static final int MAX_DATA_SIZE_IN_BYTES = CustomPayloadS2CPacketAccessor.getMaxPayloadSize() - MAX_PADDING_SIZE_IN_BYTES;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static AttachmentChange create(AttachmentTargetInfo<?> targetInfo, AttachmentType<?> type, @Nullable Object value) {
|
||||
PacketCodec<PacketByteBuf, Object> codec = (PacketCodec<PacketByteBuf, Object>) ((AttachmentTypeImpl<?>) type).packetCodec();
|
||||
Objects.requireNonNull(codec, "attachment packet codec cannot be null");
|
||||
|
||||
PacketByteBuf buf = PacketByteBufs.create();
|
||||
buf.writeOptional(Optional.ofNullable(value), codec);
|
||||
byte[] encoded = buf.array();
|
||||
|
||||
if (encoded.length > MAX_DATA_SIZE_IN_BYTES) {
|
||||
throw new IllegalArgumentException("Data for attachment '%s' was too big (%d bytes, over maximum %d)".formatted(
|
||||
type.identifier(),
|
||||
encoded.length,
|
||||
MAX_DATA_SIZE_IN_BYTES
|
||||
));
|
||||
}
|
||||
|
||||
return new AttachmentChange(targetInfo, type, encoded);
|
||||
}
|
||||
|
||||
public static void partitionAndSendPackets(List<AttachmentChange> changes, ServerPlayerEntity player) {
|
||||
Set<Identifier> supported = ((SupportedAttachmentsClientConnection) ((ServerCommonNetworkHandlerAccessor) player.networkHandler).getConnection())
|
||||
.fabric_getSupportedAttachments();
|
||||
// sort by size to better partition packets
|
||||
changes.sort(Comparator.comparingInt(c -> c.data().length));
|
||||
List<AttachmentChange> packetChanges = new ArrayList<>();
|
||||
int maxVarIntSize = VarIntsAccessor.getMaxByteSize();
|
||||
int byteSize = maxVarIntSize;
|
||||
|
||||
for (AttachmentChange change : changes) {
|
||||
if (!supported.contains(change.type.identifier())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int size = MAX_PADDING_SIZE_IN_BYTES + change.data.length;
|
||||
|
||||
if (byteSize + size > MAX_DATA_SIZE_IN_BYTES) {
|
||||
ServerPlayNetworking.send(player, new AttachmentSyncPayloadS2C(packetChanges));
|
||||
packetChanges.clear();
|
||||
byteSize = maxVarIntSize;
|
||||
}
|
||||
|
||||
packetChanges.add(change);
|
||||
byteSize += size;
|
||||
}
|
||||
|
||||
if (!packetChanges.isEmpty()) {
|
||||
ServerPlayNetworking.send(player, new AttachmentSyncPayloadS2C(packetChanges));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
public Object decodeValue() {
|
||||
PacketCodec<PacketByteBuf, Object> codec = (PacketCodec<PacketByteBuf, Object>) ((AttachmentTypeImpl<?>) type).packetCodec();
|
||||
Objects.requireNonNull(codec, "codec was null");
|
||||
|
||||
PacketByteBuf buf = new PacketByteBuf(Unpooled.copiedBuffer(data));
|
||||
return buf.readOptional(codec).orElse(null);
|
||||
}
|
||||
|
||||
public void apply(World world) {
|
||||
targetInfo.getTarget(world).setAttached((AttachmentType<Object>) type, decodeValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import net.minecraft.network.ClientConnection;
|
||||
import net.minecraft.network.packet.Packet;
|
||||
import net.minecraft.server.network.ServerPlayerConfigurationTask;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerConfigurationConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentRegistryImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.c2s.AcceptedAttachmentsPayloadC2S;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.RequestAcceptedAttachmentsPayloadS2C;
|
||||
import net.fabricmc.fabric.mixin.networking.accessor.ServerCommonNetworkHandlerAccessor;
|
||||
|
||||
public class AttachmentSync implements ModInitializer {
|
||||
public static final int MAX_IDENTIFIER_SIZE = 256;
|
||||
|
||||
public static AcceptedAttachmentsPayloadC2S createResponsePayload() {
|
||||
return new AcceptedAttachmentsPayloadC2S(AttachmentRegistryImpl.getSyncableAttachments());
|
||||
}
|
||||
|
||||
public static void trySync(AttachmentSyncPayloadS2C payload, ServerPlayerEntity player) {
|
||||
if (!payload.attachments().isEmpty()) {
|
||||
ServerPlayNetworking.send(player, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Identifier> decodeResponsePayload(AcceptedAttachmentsPayloadC2S payload) {
|
||||
Set<Identifier> atts = payload.acceptedAttachments();
|
||||
Set<Identifier> syncable = AttachmentRegistryImpl.getSyncableAttachments();
|
||||
atts.retainAll(syncable);
|
||||
|
||||
if (atts.size() < syncable.size()) {
|
||||
// Client doesn't support all
|
||||
AttachmentEntrypoint.LOGGER.warn(
|
||||
"Client does not support the syncable attachments {}",
|
||||
syncable.stream().filter(id -> !atts.contains(id)).map(Identifier::toString).collect(Collectors.joining(", "))
|
||||
);
|
||||
}
|
||||
|
||||
return atts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
// Config
|
||||
PayloadTypeRegistry.configurationC2S()
|
||||
.register(AcceptedAttachmentsPayloadC2S.ID, AcceptedAttachmentsPayloadC2S.CODEC);
|
||||
PayloadTypeRegistry.configurationS2C()
|
||||
.register(RequestAcceptedAttachmentsPayloadS2C.ID, RequestAcceptedAttachmentsPayloadS2C.CODEC);
|
||||
|
||||
ServerConfigurationConnectionEvents.CONFIGURE.register((handler, server) -> {
|
||||
if (ServerConfigurationNetworking.canSend(handler, RequestAcceptedAttachmentsPayloadS2C.PACKET_ID)) {
|
||||
handler.addTask(new AttachmentSyncTask());
|
||||
} else {
|
||||
AttachmentEntrypoint.LOGGER.debug(
|
||||
"Couldn't send attachment configuration packet to client, as the client cannot receive the payload."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ServerConfigurationNetworking.registerGlobalReceiver(AcceptedAttachmentsPayloadC2S.ID, (payload, context) -> {
|
||||
Set<Identifier> supportedAttachments = decodeResponsePayload(payload);
|
||||
ClientConnection connection = ((ServerCommonNetworkHandlerAccessor) context.networkHandler()).getConnection();
|
||||
((SupportedAttachmentsClientConnection) connection).fabric_setSupportedAttachments(supportedAttachments);
|
||||
|
||||
context.networkHandler().completeTask(AttachmentSyncTask.KEY);
|
||||
});
|
||||
|
||||
// Play
|
||||
PayloadTypeRegistry.playS2C().register(AttachmentSyncPayloadS2C.ID, AttachmentSyncPayloadS2C.CODEC);
|
||||
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||
ServerPlayerEntity player = handler.player;
|
||||
List<AttachmentChange> changes = new ArrayList<>();
|
||||
// sync world attachments
|
||||
((AttachmentTargetImpl) player.getServerWorld()).fabric_computeInitialSyncChanges(player, changes::add);
|
||||
// sync player's own persistent attachments that couldn't be synced earlier
|
||||
((AttachmentTargetImpl) player).fabric_computeInitialSyncChanges(player, changes::add);
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
AttachmentChange.partitionAndSendPackets(changes, player);
|
||||
}
|
||||
});
|
||||
|
||||
// entity tracking handled in EntityTrackerEntryMixin instead, see comment
|
||||
}
|
||||
|
||||
private record AttachmentSyncTask() implements ServerPlayerConfigurationTask {
|
||||
public static final Key KEY = new Key(RequestAcceptedAttachmentsPayloadS2C.PACKET_ID.toString());
|
||||
|
||||
@Override
|
||||
public void sendPacket(Consumer<Packet<?>> sender) {
|
||||
sender.accept(ServerConfigurationNetworking.createS2CPacket(RequestAcceptedAttachmentsPayloadS2C.INSTANCE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key getKey() {
|
||||
return KEY;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap;
|
||||
import it.unimi.dsi.fastutil.bytes.Byte2ObjectMap;
|
||||
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.network.codec.PacketCodecs;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget;
|
||||
|
||||
public sealed interface AttachmentTargetInfo<T> {
|
||||
int MAX_SIZE_IN_BYTES = Byte.BYTES + Long.BYTES;
|
||||
PacketCodec<ByteBuf, AttachmentTargetInfo<?>> PACKET_CODEC = PacketCodecs.BYTE.dispatch(
|
||||
AttachmentTargetInfo::getId, Type::packetCodecFromId
|
||||
);
|
||||
|
||||
Type<T> getType();
|
||||
|
||||
default byte getId() {
|
||||
return getType().id;
|
||||
}
|
||||
|
||||
AttachmentTarget getTarget(World world);
|
||||
|
||||
record Type<T>(byte id, PacketCodec<ByteBuf, ? extends AttachmentTargetInfo<T>> packetCodec) {
|
||||
static Byte2ObjectMap<Type<?>> TYPES = new Byte2ObjectArrayMap<>();
|
||||
static Type<BlockEntity> BLOCK_ENTITY = new Type<>((byte) 0, BlockEntityTarget.PACKET_CODEC);
|
||||
static Type<Entity> ENTITY = new Type<>((byte) 1, EntityTarget.PACKET_CODEC);
|
||||
static Type<Chunk> CHUNK = new Type<>((byte) 2, ChunkTarget.PACKET_CODEC);
|
||||
static Type<World> WORLD = new Type<>((byte) 3, WorldTarget.PACKET_CODEC);
|
||||
|
||||
public Type {
|
||||
TYPES.put(id, this);
|
||||
}
|
||||
|
||||
static PacketCodec<ByteBuf, ? extends AttachmentTargetInfo<?>> packetCodecFromId(byte id) {
|
||||
return TYPES.get(id).packetCodec;
|
||||
}
|
||||
}
|
||||
|
||||
record BlockEntityTarget(BlockPos pos) implements AttachmentTargetInfo<BlockEntity> {
|
||||
static final PacketCodec<ByteBuf, BlockEntityTarget> PACKET_CODEC = PacketCodec.tuple(
|
||||
BlockPos.PACKET_CODEC, BlockEntityTarget::pos,
|
||||
BlockEntityTarget::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<BlockEntity> getType() {
|
||||
return Type.BLOCK_ENTITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTarget getTarget(World world) {
|
||||
return world.getBlockEntity(pos);
|
||||
}
|
||||
}
|
||||
|
||||
record EntityTarget(int networkId) implements AttachmentTargetInfo<Entity> {
|
||||
static final PacketCodec<ByteBuf, EntityTarget> PACKET_CODEC = PacketCodec.tuple(
|
||||
PacketCodecs.VAR_INT, EntityTarget::networkId,
|
||||
EntityTarget::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<Entity> getType() {
|
||||
return Type.ENTITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTarget getTarget(World world) {
|
||||
return world.getEntityById(networkId);
|
||||
}
|
||||
}
|
||||
|
||||
record ChunkTarget(ChunkPos pos) implements AttachmentTargetInfo<Chunk> {
|
||||
static final PacketCodec<ByteBuf, ChunkTarget> PACKET_CODEC = PacketCodecs.VAR_LONG
|
||||
.xmap(ChunkPos::new, ChunkPos::toLong)
|
||||
.xmap(ChunkTarget::new, ChunkTarget::pos);
|
||||
|
||||
@Override
|
||||
public Type<Chunk> getType() {
|
||||
return Type.CHUNK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTarget getTarget(World world) {
|
||||
return world.getChunk(pos.x, pos.z);
|
||||
}
|
||||
}
|
||||
|
||||
final class WorldTarget implements AttachmentTargetInfo<World> {
|
||||
public static final WorldTarget INSTANCE = new WorldTarget();
|
||||
static final PacketCodec<ByteBuf, WorldTarget> PACKET_CODEC = PacketCodec.unit(INSTANCE);
|
||||
|
||||
private WorldTarget() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<World> getType() {
|
||||
return Type.WORLD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTarget getTarget(World world) {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import net.minecraft.network.ClientConnection;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
/**
|
||||
* Implemented on {@link ClientConnection} to store which attachments the client supports.
|
||||
*/
|
||||
public interface SupportedAttachmentsClientConnection {
|
||||
void fabric_setSupportedAttachments(Set<Identifier> supportedAttachments);
|
||||
|
||||
Set<Identifier> fabric_getSupportedAttachments();
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync.c2s;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.network.codec.PacketCodecs;
|
||||
import net.minecraft.network.packet.CustomPayload;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public record AcceptedAttachmentsPayloadC2S(Set<Identifier> acceptedAttachments) implements CustomPayload {
|
||||
public static final PacketCodec<PacketByteBuf, AcceptedAttachmentsPayloadC2S> CODEC = PacketCodec.tuple(
|
||||
PacketCodecs.collection(HashSet::new, Identifier.PACKET_CODEC), AcceptedAttachmentsPayloadC2S::acceptedAttachments,
|
||||
AcceptedAttachmentsPayloadC2S::new
|
||||
);
|
||||
public static final Identifier PACKET_ID = Identifier.of("fabric", "accepted_attachments_v1");
|
||||
public static final Id<AcceptedAttachmentsPayloadC2S> ID = new Id<>(PACKET_ID);
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync.s2c;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.network.codec.PacketCodecs;
|
||||
import net.minecraft.network.packet.CustomPayload;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
|
||||
public record AttachmentSyncPayloadS2C(List<AttachmentChange> attachments) implements CustomPayload {
|
||||
public static final PacketCodec<PacketByteBuf, AttachmentSyncPayloadS2C> CODEC = PacketCodec.tuple(
|
||||
AttachmentChange.PACKET_CODEC.collect(PacketCodecs.toList()), AttachmentSyncPayloadS2C::attachments,
|
||||
AttachmentSyncPayloadS2C::new
|
||||
);
|
||||
public static final Identifier PACKET_ID = Identifier.of("fabric", "attachment_sync_v1");
|
||||
public static final Id<AttachmentSyncPayloadS2C> ID = new Id<>(PACKET_ID);
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.attachment.sync.s2c;
|
||||
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.codec.PacketCodec;
|
||||
import net.minecraft.network.packet.CustomPayload;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class RequestAcceptedAttachmentsPayloadS2C implements CustomPayload {
|
||||
public static final RequestAcceptedAttachmentsPayloadS2C INSTANCE = new RequestAcceptedAttachmentsPayloadS2C();
|
||||
public static final Identifier PACKET_ID = Identifier.of("fabric", "accepted_attachments_v1");
|
||||
public static final Id<RequestAcceptedAttachmentsPayloadS2C> ID = new Id<>(PACKET_ID);
|
||||
public static final PacketCodec<PacketByteBuf, RequestAcceptedAttachmentsPayloadS2C> CODEC = PacketCodec.unit(INSTANCE);
|
||||
|
||||
private RequestAcceptedAttachmentsPayloadS2C() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
|
@ -17,28 +17,35 @@
|
|||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.registry.RegistryWrapper;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
import net.minecraft.world.chunk.ChunkStatus;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentSerializingImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
@Mixin({BlockEntity.class, Entity.class, World.class, Chunk.class})
|
||||
abstract class AttachmentTargetsMixin implements AttachmentTargetImpl {
|
||||
@Nullable
|
||||
private IdentityHashMap<AttachmentType<?>, Object> fabric_dataAttachments = null;
|
||||
@Nullable
|
||||
private IdentityHashMap<AttachmentType<?>, AttachmentChange> fabric_syncedAttachments = null;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
|
@ -51,17 +58,12 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl {
|
|||
@Override
|
||||
@Nullable
|
||||
public <T> T setAttached(AttachmentType<T> type, @Nullable T value) {
|
||||
// Extremely inelegant, but the only alternative is separating out these two mixins and duplicating code
|
||||
Object thisObject = this;
|
||||
this.fabric_markChanged(type);
|
||||
|
||||
if (thisObject instanceof BlockEntity) {
|
||||
((BlockEntity) thisObject).markDirty();
|
||||
} else if (thisObject instanceof Chunk) {
|
||||
((Chunk) thisObject).markNeedsSaving();
|
||||
|
||||
if (type.isPersistent() && ((Chunk) thisObject).getStatus().equals(ChunkStatus.EMPTY)) {
|
||||
AttachmentEntrypoint.LOGGER.warn("Attaching persistent attachment {} to chunk with chunk status EMPTY. Attachment might be discarded.", type.identifier());
|
||||
}
|
||||
if (this.fabric_shouldTryToSync() && type.isSynced()) {
|
||||
AttachmentChange change = AttachmentChange.create(fabric_getSyncTargetInfo(), type, value);
|
||||
acknowledgeSyncedEntry(type, change);
|
||||
this.fabric_syncChange(type, new AttachmentSyncPayloadS2C(List.of(change)));
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
|
@ -69,13 +71,7 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl {
|
|||
return null;
|
||||
}
|
||||
|
||||
T removed = (T) fabric_dataAttachments.remove(type);
|
||||
|
||||
if (fabric_dataAttachments.isEmpty()) {
|
||||
fabric_dataAttachments = null;
|
||||
}
|
||||
|
||||
return removed;
|
||||
return (T) fabric_dataAttachments.remove(type);
|
||||
} else {
|
||||
if (fabric_dataAttachments == null) {
|
||||
fabric_dataAttachments = new IdentityHashMap<>();
|
||||
|
@ -97,7 +93,17 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl {
|
|||
|
||||
@Override
|
||||
public void fabric_readAttachmentsFromNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup wrapperLookup) {
|
||||
fabric_dataAttachments = AttachmentSerializingImpl.deserializeAttachmentData(nbt, wrapperLookup);
|
||||
// Note on player targets: no syncing can happen here as the networkHandler is still null
|
||||
// Instead it is done on player join (see AttachmentSync)
|
||||
this.fabric_dataAttachments = AttachmentSerializingImpl.deserializeAttachmentData(nbt, wrapperLookup);
|
||||
|
||||
if (this.fabric_shouldTryToSync() && this.fabric_dataAttachments != null) {
|
||||
this.fabric_dataAttachments.forEach((type, value) -> {
|
||||
if (type.isSynced()) {
|
||||
acknowledgeSynced(type, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -109,4 +115,39 @@ abstract class AttachmentTargetsMixin implements AttachmentTargetImpl {
|
|||
public Map<AttachmentType<?>, ?> fabric_getAttachments() {
|
||||
return fabric_dataAttachments;
|
||||
}
|
||||
|
||||
@Unique
|
||||
private void acknowledgeSynced(AttachmentType<?> type, Object value) {
|
||||
acknowledgeSyncedEntry(type, AttachmentChange.create(fabric_getSyncTargetInfo(), type, value));
|
||||
}
|
||||
|
||||
@Unique
|
||||
private void acknowledgeSyncedEntry(AttachmentType<?> type, @Nullable AttachmentChange change) {
|
||||
if (change == null) {
|
||||
if (fabric_syncedAttachments == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fabric_syncedAttachments.remove(type);
|
||||
} else {
|
||||
if (fabric_syncedAttachments == null) {
|
||||
fabric_syncedAttachments = new IdentityHashMap<>();
|
||||
}
|
||||
|
||||
fabric_syncedAttachments.put(type, change);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer<AttachmentChange> changeOutput) {
|
||||
if (fabric_syncedAttachments == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<AttachmentType<?>, AttachmentChange> entry : fabric_syncedAttachments.entrySet()) {
|
||||
if (((AttachmentTypeImpl<?>) entry.getKey()).syncPredicate().test(this, player)) {
|
||||
changeOutput.accept(entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
@ -25,11 +28,32 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.registry.RegistryWrapper;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
@Mixin(BlockEntity.class)
|
||||
abstract class BlockEntityMixin implements AttachmentTargetImpl {
|
||||
@Shadow
|
||||
@Final
|
||||
protected BlockPos pos;
|
||||
@Shadow
|
||||
@Nullable
|
||||
protected World world;
|
||||
|
||||
@Shadow
|
||||
public abstract void markDirty();
|
||||
|
||||
@Shadow
|
||||
public abstract boolean hasWorld();
|
||||
|
||||
@Inject(
|
||||
method = "read",
|
||||
at = @At("RETURN")
|
||||
|
@ -45,4 +69,30 @@ abstract class BlockEntityMixin implements AttachmentTargetImpl {
|
|||
private void writeBlockEntityAttachments(RegistryWrapper.WrapperLookup wrapperLookup, CallbackInfoReturnable<NbtCompound> cir) {
|
||||
this.fabric_writeAttachmentsToNbt(cir.getReturnValue(), wrapperLookup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_markChanged(AttachmentType<?> type) {
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTargetInfo<?> fabric_getSyncTargetInfo() {
|
||||
return new AttachmentTargetInfo.BlockEntityTarget(this.pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_syncChange(AttachmentType<?> type, AttachmentSyncPayloadS2C payload) {
|
||||
PlayerLookup.tracking((BlockEntity) (Object) this)
|
||||
.forEach(player -> {
|
||||
if (((AttachmentTypeImpl<?>) type).syncPredicate().test(this, player)) {
|
||||
AttachmentSync.trySync(payload, player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fabric_shouldTryToSync() {
|
||||
// Persistent attachments are read at a time with no world
|
||||
return !this.hasWorld() || !this.world.isClient();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
import net.minecraft.server.network.ChunkDataSender;
|
||||
import net.minecraft.server.network.ServerPlayNetworkHandler;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.world.chunk.WorldChunk;
|
||||
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
|
||||
@Mixin(ChunkDataSender.class)
|
||||
abstract class ChunkDataSenderMixin {
|
||||
@WrapOperation(
|
||||
method = "sendChunkBatches",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/server/network/ChunkDataSender;sendChunkData(Lnet/minecraft/server/network/ServerPlayNetworkHandler;Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/WorldChunk;)V"
|
||||
)
|
||||
)
|
||||
private void sendInitialAttachmentData(ServerPlayNetworkHandler handler, ServerWorld world, WorldChunk chunk, Operation<Void> original, ServerPlayerEntity player) {
|
||||
original.call(handler, world, chunk);
|
||||
// do a wrap operation so this packet is sent *after* the chunk ones
|
||||
List<AttachmentChange> changes = new ArrayList<>();
|
||||
((AttachmentTargetImpl) chunk).fabric_computeInitialSyncChanges(player, changes::add);
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
AttachmentChange.partitionAndSendPackets(changes, player);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
import net.minecraft.world.chunk.ChunkStatus;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo;
|
||||
|
||||
@Mixin(Chunk.class)
|
||||
abstract class ChunkMixin implements AttachmentTargetImpl {
|
||||
@Shadow
|
||||
@Final
|
||||
protected ChunkPos pos;
|
||||
|
||||
@Shadow
|
||||
public abstract ChunkStatus getStatus();
|
||||
|
||||
@Shadow
|
||||
public abstract ChunkPos getPos();
|
||||
|
||||
@Shadow
|
||||
public abstract boolean needsSaving();
|
||||
|
||||
@Override
|
||||
public AttachmentTargetInfo<?> fabric_getSyncTargetInfo() {
|
||||
return new AttachmentTargetInfo.ChunkTarget(this.pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_markChanged(AttachmentType<?> type) {
|
||||
needsSaving();
|
||||
|
||||
if (type.isPersistent() && this.getStatus().equals(ChunkStatus.EMPTY)) {
|
||||
AttachmentEntrypoint.LOGGER.warn(
|
||||
"Attaching persistent attachment {} to chunk {} with chunk status EMPTY. Attachment might be discarded.",
|
||||
type.identifier(),
|
||||
this.getPos()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fabric_shouldTryToSync() {
|
||||
// ProtoChunk or EmptyChunk
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
import net.minecraft.network.ClientConnection;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.fabric.impl.attachment.sync.SupportedAttachmentsClientConnection;
|
||||
|
||||
@Mixin(ClientConnection.class)
|
||||
public class ClientConnectionMixin implements SupportedAttachmentsClientConnection {
|
||||
private Set<Identifier> fabric_supportedAttachments = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void fabric_setSupportedAttachments(Set<Identifier> supportedAttachments) {
|
||||
fabric_supportedAttachments = supportedAttachments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Identifier> fabric_getSupportedAttachments() {
|
||||
return fabric_supportedAttachments;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket;
|
||||
|
||||
@Mixin(CustomPayloadC2SPacket.class)
|
||||
public interface CustomPayloadS2CPacketAccessor {
|
||||
@Accessor("MAX_PAYLOAD_SIZE")
|
||||
static int getMaxPayloadSize() {
|
||||
throw new UnsupportedOperationException("Implemented via mixin");
|
||||
}
|
||||
}
|
|
@ -25,12 +25,23 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
@Mixin(Entity.class)
|
||||
abstract class EntityMixin implements AttachmentTargetImpl {
|
||||
@Shadow
|
||||
private int id;
|
||||
|
||||
@Shadow
|
||||
public abstract World getWorld();
|
||||
|
||||
|
@ -49,4 +60,33 @@ abstract class EntityMixin implements AttachmentTargetImpl {
|
|||
private void writeEntityAttachments(NbtCompound nbt, CallbackInfoReturnable<NbtCompound> cir) {
|
||||
this.fabric_writeAttachmentsToNbt(nbt, getWorld().getRegistryManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTargetInfo<?> fabric_getSyncTargetInfo() {
|
||||
return new AttachmentTargetInfo.EntityTarget(this.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_syncChange(AttachmentType<?> type, AttachmentSyncPayloadS2C payload) {
|
||||
if (!this.getWorld().isClient()) {
|
||||
AttachmentSyncPredicate predicate = ((AttachmentTypeImpl<?>) type).syncPredicate();
|
||||
|
||||
if ((Object) this instanceof ServerPlayerEntity self && predicate.test(this, self)) {
|
||||
// Players do not track themselves
|
||||
AttachmentSync.trySync(payload, self);
|
||||
}
|
||||
|
||||
PlayerLookup.tracking((Entity) (Object) this)
|
||||
.forEach(player -> {
|
||||
if (predicate.test(this, player)) {
|
||||
AttachmentSync.trySync(payload, player);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fabric_shouldTryToSync() {
|
||||
return !this.getWorld().isClient();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.network.EntityTrackerEntry;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
|
||||
@Mixin(EntityTrackerEntry.class)
|
||||
abstract class EntityTrackerEntryMixin {
|
||||
@Shadow
|
||||
@Final
|
||||
private Entity entity;
|
||||
|
||||
@Inject(
|
||||
method = "startTracking",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/entity/Entity;onStartedTrackingBy(Lnet/minecraft/server/network/ServerPlayerEntity;)V"
|
||||
)
|
||||
)
|
||||
private void syncAttachmentsAfterSpawn(ServerPlayerEntity player, CallbackInfo ci) {
|
||||
// mixin because the START_TRACKING event triggers before the spawn packet is sent to the client,
|
||||
// whereas we want to modify the entity on the client
|
||||
List<AttachmentChange> changes = new ArrayList<>();
|
||||
((AttachmentTargetImpl) this.entity).fabric_computeInitialSyncChanges(player, changes::add);
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
AttachmentChange.partitionAndSendPackets(changes, player);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,18 +23,44 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.registry.DynamicRegistryManager;
|
||||
import net.minecraft.registry.RegistryKey;
|
||||
import net.minecraft.registry.entry.RegistryEntry;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.world.MutableWorldProperties;
|
||||
import net.minecraft.world.PersistentState;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.dimension.DimensionType;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentPersistentState;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
@Mixin(ServerWorld.class)
|
||||
abstract class ServerWorldMixin {
|
||||
abstract class ServerWorldMixin extends World implements AttachmentTargetImpl {
|
||||
@Shadow
|
||||
@Final
|
||||
private MinecraftServer server;
|
||||
|
||||
protected ServerWorldMixin(MutableWorldProperties properties, RegistryKey<World> registryRef, DynamicRegistryManager registryManager, RegistryEntry<DimensionType> dimensionEntry, boolean isClient, boolean debugWorld, long seed, int maxChainedNeighborUpdates) {
|
||||
super(
|
||||
properties,
|
||||
registryRef,
|
||||
registryManager,
|
||||
dimensionEntry,
|
||||
isClient,
|
||||
debugWorld,
|
||||
seed,
|
||||
maxChainedNeighborUpdates
|
||||
);
|
||||
}
|
||||
|
||||
@Inject(at = @At("TAIL"), method = "<init>")
|
||||
private void createAttachmentsPersistentState(CallbackInfo ci) {
|
||||
// Force persistent state creation
|
||||
|
@ -46,4 +72,21 @@ abstract class ServerWorldMixin {
|
|||
);
|
||||
world.getPersistentStateManager().getOrCreate(type, AttachmentPersistentState.ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_syncChange(AttachmentType<?> type, AttachmentSyncPayloadS2C payload) {
|
||||
if ((Object) this instanceof ServerWorld serverWorld) {
|
||||
PlayerLookup.world(serverWorld)
|
||||
.forEach(player -> {
|
||||
if (((AttachmentTypeImpl<?>) type).syncPredicate().test(this, player)) {
|
||||
AttachmentSync.trySync(payload, player);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTargetInfo<?> fabric_getSyncTargetInfo() {
|
||||
return AttachmentTargetInfo.WorldTarget.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import net.minecraft.network.encoding.VarInts;
|
||||
|
||||
@Mixin(VarInts.class)
|
||||
public interface VarIntsAccessor {
|
||||
@Accessor("MAX_BYTES")
|
||||
static int getMaxByteSize() {
|
||||
throw new UnsupportedOperationException("implemented via mixin");
|
||||
}
|
||||
}
|
|
@ -16,25 +16,71 @@
|
|||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
import net.minecraft.world.chunk.ProtoChunk;
|
||||
import net.minecraft.world.chunk.WorldChunk;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTypeImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentSync;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
@Mixin(WorldChunk.class)
|
||||
public class WorldChunkMixin {
|
||||
@Inject(
|
||||
method = "<init>(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/ProtoChunk;Lnet/minecraft/world/chunk/WorldChunk$EntityLoader;)V",
|
||||
at = @At("TAIL")
|
||||
)
|
||||
public void transferProtoChunkAttachement(ServerWorld world, ProtoChunk protoChunk, WorldChunk.EntityLoader entityLoader, CallbackInfo ci) {
|
||||
AttachmentTargetImpl.transfer(protoChunk, (AttachmentTarget) this, false);
|
||||
abstract class WorldChunkMixin extends AttachmentTargetsMixin implements AttachmentTargetImpl {
|
||||
@Shadow
|
||||
@Final
|
||||
World world;
|
||||
|
||||
@Shadow
|
||||
public abstract Map<BlockPos, BlockEntity> getBlockEntities();
|
||||
|
||||
@Inject(method = "<init>(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/world/chunk/ProtoChunk;Lnet/minecraft/world/chunk/WorldChunk$EntityLoader;)V", at = @At("TAIL"))
|
||||
private void transferProtoChunkAttachement(ServerWorld world, ProtoChunk protoChunk, WorldChunk.EntityLoader entityLoader, CallbackInfo ci) {
|
||||
AttachmentTargetImpl.transfer(protoChunk, this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer<AttachmentChange> changeOutput) {
|
||||
super.fabric_computeInitialSyncChanges(player, changeOutput);
|
||||
|
||||
for (BlockEntity be : this.getBlockEntities().values()) {
|
||||
((AttachmentTargetImpl) be).fabric_computeInitialSyncChanges(player, changeOutput);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_syncChange(AttachmentType<?> type, AttachmentSyncPayloadS2C payload) {
|
||||
if (this.world instanceof ServerWorld serverWorld) {
|
||||
// can't shadow from Chunk because this already extends a supermixin
|
||||
PlayerLookup.tracking(serverWorld, ((Chunk) (Object) this).getPos())
|
||||
.forEach(player -> {
|
||||
if (((AttachmentTypeImpl<?>) type).syncPredicate().test(this, player)) {
|
||||
AttachmentSync.trySync(payload, player);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fabric_shouldTryToSync() {
|
||||
return !this.world.isClient();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
|
||||
@Mixin(World.class)
|
||||
abstract class WorldMixin implements AttachmentTargetImpl {
|
||||
@Shadow
|
||||
public abstract boolean isClient();
|
||||
|
||||
@Override
|
||||
public boolean fabric_shouldTryToSync() {
|
||||
return !this.isClient();
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package net.fabricmc.fabric.mixin.attachment;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
|
@ -25,14 +26,18 @@ import org.spongepowered.asm.mixin.Shadow;
|
|||
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.registry.RegistryWrapper;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.world.chunk.WorldChunk;
|
||||
import net.minecraft.world.chunk.WrapperProtoChunk;
|
||||
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.impl.attachment.AttachmentTargetImpl;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentChange;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.AttachmentTargetInfo;
|
||||
import net.fabricmc.fabric.impl.attachment.sync.s2c.AttachmentSyncPayloadS2C;
|
||||
|
||||
@Mixin(WrapperProtoChunk.class)
|
||||
public class WrapperProtoChunkMixin implements AttachmentTargetImpl {
|
||||
abstract class WrapperProtoChunkMixin extends AttachmentTargetsMixin {
|
||||
@Shadow
|
||||
@Final
|
||||
private WorldChunk wrapped;
|
||||
|
@ -73,4 +78,29 @@ public class WrapperProtoChunkMixin implements AttachmentTargetImpl {
|
|||
public Map<AttachmentType<?>, ?> fabric_getAttachments() {
|
||||
return ((AttachmentTargetImpl) this.wrapped).fabric_getAttachments();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fabric_shouldTryToSync() {
|
||||
return ((AttachmentTargetImpl) wrapped).fabric_shouldTryToSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_computeInitialSyncChanges(ServerPlayerEntity player, Consumer<AttachmentChange> changeOutput) {
|
||||
((AttachmentTargetImpl) wrapped).fabric_computeInitialSyncChanges(player, changeOutput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentTargetInfo<?> fabric_getSyncTargetInfo() {
|
||||
return ((AttachmentTargetImpl) wrapped).fabric_getSyncTargetInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_syncChange(AttachmentType<?> type, AttachmentSyncPayloadS2C payload) {
|
||||
((AttachmentTargetImpl) wrapped).fabric_syncChange(type, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_markChanged(AttachmentType<?> type) {
|
||||
((AttachmentTargetImpl) wrapped).fabric_markChanged(type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,17 @@
|
|||
"AttachmentTargetsMixin",
|
||||
"BannerBlockEntityMixin",
|
||||
"BlockEntityMixin",
|
||||
"SerializedChunkMixin",
|
||||
"ChunkDataSenderMixin",
|
||||
"ChunkMixin",
|
||||
"ClientConnectionMixin",
|
||||
"CustomPayloadS2CPacketAccessor",
|
||||
"EntityMixin",
|
||||
"EntityTrackerEntryMixin",
|
||||
"SerializedChunkMixin",
|
||||
"ServerWorldMixin",
|
||||
"VarIntsAccessor",
|
||||
"WorldChunkMixin",
|
||||
"WorldMixin",
|
||||
"WrapperProtoChunkMixin"
|
||||
],
|
||||
"injectors": {
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
"depends": {
|
||||
"fabricloader": ">=0.16.8",
|
||||
"fabric-entity-events-v1": "*",
|
||||
"fabric-object-builder-api-v1": "*"
|
||||
"fabric-object-builder-api-v1": "*",
|
||||
"fabric-networking-api-v1": "*"
|
||||
},
|
||||
"description": "Allows conveniently attaching data to existing game objects",
|
||||
"mixins": [
|
||||
|
@ -30,7 +31,11 @@
|
|||
],
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint"
|
||||
"net.fabricmc.fabric.impl.attachment.AttachmentEntrypoint",
|
||||
"net.fabricmc.fabric.impl.attachment.sync.AttachmentSync"
|
||||
],
|
||||
"client": [
|
||||
"net.fabricmc.fabric.impl.attachment.client.AttachmentSyncClient"
|
||||
]
|
||||
},
|
||||
"custom": {
|
||||
|
@ -39,7 +44,7 @@
|
|||
"net/minecraft/class_2586": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"],
|
||||
"net/minecraft/class_2791": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"],
|
||||
"net/minecraft/class_1297": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"],
|
||||
"net/minecraft/class_3218": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"]
|
||||
"net/minecraft/class_1937": ["net/fabricmc/fabric/api/attachment/v1/AttachmentTarget"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -82,6 +83,12 @@ public class CommonAttachmentTests {
|
|||
Bootstrap.initialize();
|
||||
}
|
||||
|
||||
private static <T> T mockAndDisableSync(Class<T> cl) {
|
||||
T target = mock(cl, CALLS_REAL_METHODS);
|
||||
doReturn(false).when((AttachmentTargetImpl) target).fabric_shouldTryToSync();
|
||||
return target;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTargets() {
|
||||
AttachmentType<String> basic = AttachmentRegistry.create(Identifier.of(MOD_ID, "basic_attachment"));
|
||||
|
@ -90,14 +97,14 @@ public class CommonAttachmentTests {
|
|||
* CALLS_REAL_METHODS makes sense here because AttachmentTarget does not refer to anything in the underlying
|
||||
* class, and it saves us a lot of pain trying to get the regular constructors for ServerWorld and WorldChunk to work.
|
||||
*/
|
||||
ServerWorld serverWorld = mock(ServerWorld.class, CALLS_REAL_METHODS);
|
||||
Entity entity = mock(Entity.class, CALLS_REAL_METHODS);
|
||||
BlockEntity blockEntity = mock(BlockEntity.class, CALLS_REAL_METHODS);
|
||||
ServerWorld serverWorld = mockAndDisableSync(ServerWorld.class);
|
||||
Entity entity = mockAndDisableSync(Entity.class);
|
||||
BlockEntity blockEntity = mockAndDisableSync(BlockEntity.class);
|
||||
|
||||
WorldChunk worldChunk = mock(WorldChunk.class, CALLS_REAL_METHODS);
|
||||
WorldChunk worldChunk = mockAndDisableSync(WorldChunk.class);
|
||||
worldChunk.setUnsavedListener(pos -> { });
|
||||
|
||||
ProtoChunk protoChunk = mock(ProtoChunk.class, CALLS_REAL_METHODS);
|
||||
ProtoChunk protoChunk = mockAndDisableSync(ProtoChunk.class);
|
||||
|
||||
for (AttachmentTarget target : new AttachmentTarget[]{serverWorld, entity, blockEntity, worldChunk, protoChunk}) {
|
||||
testForTarget(target, basic);
|
||||
|
@ -130,7 +137,7 @@ public class CommonAttachmentTests {
|
|||
Identifier.of(MOD_ID, "defaulted_attachment"),
|
||||
() -> 0
|
||||
);
|
||||
Entity target = mock(Entity.class, CALLS_REAL_METHODS);
|
||||
Entity target = mockAndDisableSync(Entity.class);
|
||||
|
||||
assertFalse(target.hasAttached(defaulted));
|
||||
assertEquals(0, target.getAttachedOrCreate(defaulted));
|
||||
|
@ -188,12 +195,12 @@ public class CommonAttachmentTests {
|
|||
AttachmentType<Boolean> copiedOnRespawn = AttachmentRegistry.create(Identifier.of(MOD_ID, "copied_on_respawn"),
|
||||
AttachmentRegistry.Builder::copyOnDeath);
|
||||
|
||||
Entity original = mock(Entity.class, CALLS_REAL_METHODS);
|
||||
Entity original = mockAndDisableSync(Entity.class);
|
||||
original.setAttached(notCopiedOnRespawn, true);
|
||||
original.setAttached(copiedOnRespawn, true);
|
||||
|
||||
Entity respawnTarget = mock(Entity.class, CALLS_REAL_METHODS);
|
||||
Entity nonRespawnTarget = mock(Entity.class, CALLS_REAL_METHODS);
|
||||
Entity respawnTarget = mockAndDisableSync(Entity.class);
|
||||
Entity nonRespawnTarget = mockAndDisableSync(Entity.class);
|
||||
|
||||
AttachmentTargetImpl.transfer(original, respawnTarget, true);
|
||||
AttachmentTargetImpl.transfer(original, nonRespawnTarget, false);
|
||||
|
@ -241,7 +248,7 @@ public class CommonAttachmentTests {
|
|||
@Test
|
||||
void testWorldPersistentState() {
|
||||
// Trying to simulate actual saving and loading for the world is too hard
|
||||
ServerWorld world = mock(ServerWorld.class, CALLS_REAL_METHODS);
|
||||
ServerWorld world = mockAndDisableSync(ServerWorld.class);
|
||||
AttachmentPersistentState state = new AttachmentPersistentState(world);
|
||||
assertFalse(world.hasAttached(PERSISTENT));
|
||||
|
||||
|
@ -249,7 +256,7 @@ public class CommonAttachmentTests {
|
|||
world.setAttached(PERSISTENT, expected);
|
||||
NbtCompound fakeSave = state.writeNbt(new NbtCompound(), mockDRM());
|
||||
|
||||
world = mock(ServerWorld.class, CALLS_REAL_METHODS);
|
||||
world = mockAndDisableSync(ServerWorld.class);
|
||||
AttachmentPersistentState.read(world, fakeSave, mockDRM());
|
||||
assertTrue(world.hasAttached(PERSISTENT));
|
||||
assertEquals(expected, world.getAttached(PERSISTENT));
|
||||
|
|
|
@ -16,21 +16,36 @@
|
|||
|
||||
package net.fabricmc.fabric.test.attachment;
|
||||
|
||||
import static net.minecraft.server.command.CommandManager.argument;
|
||||
import static net.minecraft.server.command.CommandManager.literal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import com.mojang.serialization.Codec;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.command.argument.BlockPosArgumentType;
|
||||
import net.minecraft.command.argument.ColumnPosArgumentType;
|
||||
import net.minecraft.command.argument.EntityArgumentType;
|
||||
import net.minecraft.network.codec.PacketCodecs;
|
||||
import net.minecraft.registry.Registries;
|
||||
import net.minecraft.registry.Registry;
|
||||
import net.minecraft.registry.RegistryKey;
|
||||
import net.minecraft.registry.RegistryKeys;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.WorldSavePath;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.util.math.ColumnPos;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
import net.minecraft.world.chunk.ChunkStatus;
|
||||
import net.minecraft.world.chunk.ProtoChunk;
|
||||
|
@ -41,9 +56,12 @@ import net.minecraft.world.gen.feature.DefaultFeatureConfig;
|
|||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentType;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeSelectors;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
|
||||
|
@ -57,6 +75,35 @@ public class AttachmentTestMod implements ModInitializer {
|
|||
public static final AttachmentType<String> FEATURE_ATTACHMENT = AttachmentRegistry.create(
|
||||
Identifier.of(MOD_ID, "feature")
|
||||
);
|
||||
public static final AttachmentType<Boolean> SYNCED_WITH_ALL = AttachmentRegistry.create(
|
||||
Identifier.of(MOD_ID, "synced_all"),
|
||||
builder -> builder
|
||||
.initializer(() -> false)
|
||||
.persistent(Codec.BOOL)
|
||||
.syncWith(PacketCodecs.BOOL.cast(), AttachmentSyncPredicate.all())
|
||||
);
|
||||
public static final AttachmentType<Boolean> SYNCED_WITH_TARGET = AttachmentRegistry.create(
|
||||
Identifier.of(MOD_ID, "synced_target"),
|
||||
builder -> builder
|
||||
.initializer(() -> false)
|
||||
.persistent(Codec.BOOL)
|
||||
.syncWith(PacketCodecs.BOOL.cast(), AttachmentSyncPredicate.targetOnly())
|
||||
);
|
||||
public static final AttachmentType<Boolean> SYNCED_EXCEPT_TARGET = AttachmentRegistry.create(
|
||||
Identifier.of(MOD_ID, "synced_except_target"),
|
||||
builder -> builder
|
||||
.initializer(() -> false)
|
||||
.persistent(Codec.BOOL)
|
||||
.syncWith(PacketCodecs.BOOL.cast(), AttachmentSyncPredicate.allButTarget())
|
||||
);
|
||||
public static final AttachmentType<Boolean> SYNCED_CREATIVE_ONLY = AttachmentRegistry.create(
|
||||
Identifier.of(MOD_ID, "synced_custom"),
|
||||
builder -> builder
|
||||
.initializer(() -> false)
|
||||
.persistent(Codec.BOOL)
|
||||
.syncWith(PacketCodecs.BOOL.cast(), (target, player) -> player.isCreative())
|
||||
);
|
||||
public static final SimpleCommandExceptionType TARGET_NOT_FOUND = new SimpleCommandExceptionType(Text.literal("Target not found"));
|
||||
|
||||
public static final ChunkPos FAR_CHUNK_POS = new ChunkPos(300, 0);
|
||||
|
||||
|
@ -103,6 +150,7 @@ public class AttachmentTestMod implements ModInitializer {
|
|||
overworld.setAttached(PERSISTENT, "world_data");
|
||||
|
||||
chunk.setAttached(PERSISTENT, "chunk_data");
|
||||
chunk.setAttached(SYNCED_WITH_ALL, true);
|
||||
|
||||
ProtoChunk protoChunk = (ProtoChunk) overworld.getChunkManager().getChunk(FAR_CHUNK_POS.x, FAR_CHUNK_POS.z, ChunkStatus.STRUCTURE_STARTS, true);
|
||||
protoChunk.setAttached(PERSISTENT, "protochunk_data");
|
||||
|
@ -140,5 +188,62 @@ public class AttachmentTestMod implements ModInitializer {
|
|||
|
||||
if (!"protochunk_data".equals(chunk.getAttached(PERSISTENT))) throw new AssertionError("ProtoChunk attachment was not transfered to WorldChunk");
|
||||
}));
|
||||
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(
|
||||
literal("attachment")
|
||||
.then(buildCommandForKind("all", "all", SYNCED_WITH_ALL))
|
||||
.then(buildCommandForKind("self_only", "only self", SYNCED_WITH_TARGET))
|
||||
.then(buildCommandForKind("others_only", "all but self", SYNCED_EXCEPT_TARGET))
|
||||
.then(buildCommandForKind("creative_only", "creative players only", SYNCED_CREATIVE_ONLY))
|
||||
));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<ServerCommandSource> buildCommandForKind(String id, String syncedWith, AttachmentType<Boolean> type) {
|
||||
return literal(id).executes(context -> updateAttachmentFor(
|
||||
context.getSource().getPlayerOrThrow(),
|
||||
type,
|
||||
context,
|
||||
"Set self flag (synced with %s) to %%s".formatted(syncedWith)
|
||||
)).then(
|
||||
argument("target", EntityArgumentType.entity()).executes(context -> updateAttachmentFor(
|
||||
EntityArgumentType.getEntity(context, "target"),
|
||||
type,
|
||||
context,
|
||||
"Set entity flag (synced with %s) to %%s".formatted(syncedWith)
|
||||
))
|
||||
).then(argument("pos", BlockPosArgumentType.blockPos()).executes(context -> {
|
||||
BlockEntity be = context.getSource().getWorld().getBlockEntity(BlockPosArgumentType.getBlockPos(context, "pos"));
|
||||
|
||||
if (be == null) {
|
||||
throw TARGET_NOT_FOUND.create();
|
||||
}
|
||||
|
||||
return updateAttachmentFor(
|
||||
be,
|
||||
type,
|
||||
context,
|
||||
"Set block entity flag (synced with %s) to %%s".formatted(syncedWith)
|
||||
);
|
||||
})).then(argument("chunkPos", ColumnPosArgumentType.columnPos()).executes(context -> {
|
||||
ColumnPos pos = ColumnPosArgumentType.getColumnPos(context, "chunkpos");
|
||||
return updateAttachmentFor(
|
||||
context.getSource().getWorld().getChunk(pos.x(), pos.z(), ChunkStatus.STRUCTURE_STARTS, true),
|
||||
type,
|
||||
context,
|
||||
"Set chunk flag (synced with %s) to %%s".formatted(syncedWith)
|
||||
);
|
||||
})).then(literal("world").executes(context -> updateAttachmentFor(
|
||||
context.getSource().getWorld(),
|
||||
type,
|
||||
context,
|
||||
"Set world flag (synced with %s) to %%s".formatted(syncedWith)
|
||||
)));
|
||||
}
|
||||
|
||||
private static int updateAttachmentFor(AttachmentTarget target, AttachmentType<Boolean> attachment, CommandContext<ServerCommandSource> context, String messageFormat) throws CommandSyntaxException {
|
||||
boolean current = target.getAttachedOrElse(attachment, false);
|
||||
target.setAttached(attachment, !current);
|
||||
context.getSource().sendFeedback(() -> Text.literal(messageFormat.formatted(!current)), false);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,18 +8,26 @@
|
|||
"depends": {
|
||||
"fabric-data-attachment-api-v1": "*",
|
||||
"fabric-lifecycle-events-v1": "*",
|
||||
"fabric-biome-api-v1": "*"
|
||||
"fabric-biome-api-v1": "*",
|
||||
"fabric-command-api-v2": "*"
|
||||
},
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"net.fabricmc.fabric.test.attachment.AttachmentTestMod"
|
||||
],
|
||||
"client": [
|
||||
"net.fabricmc.fabric.test.attachment.client.AttachmentTestModClient"
|
||||
],
|
||||
"fabric-gametest": [
|
||||
"net.fabricmc.fabric.test.attachment.gametest.AttachmentCopyTests",
|
||||
"net.fabricmc.fabric.test.attachment.gametest.BlockEntityTests"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"fabric-data-attachment-api-v1-testmod.mixins.json"
|
||||
"fabric-data-attachment-api-v1-testmod.mixins.json",
|
||||
{
|
||||
"config": "fabric-data-attachment-api-v1-testmod.client.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.attachment.client;
|
||||
|
||||
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
|
||||
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||
import net.minecraft.command.argument.BlockPosArgumentType;
|
||||
import net.minecraft.command.argument.DefaultPosArgument;
|
||||
import net.minecraft.command.argument.EntityArgumentType;
|
||||
import net.minecraft.command.argument.UuidArgumentType;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Colors;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
|
||||
import net.fabricmc.fabric.test.attachment.AttachmentTestMod;
|
||||
import net.fabricmc.fabric.test.attachment.client.mixin.ClientWorldAccessor;
|
||||
import net.fabricmc.fabric.test.attachment.client.mixin.DefaultPosArgumentAccessor;
|
||||
|
||||
public class AttachmentTestModClient implements ClientModInitializer {
|
||||
private static AbstractClientPlayerEntity parseClientPlayer(FabricClientCommandSource source, String name) throws CommandSyntaxException {
|
||||
for (AbstractClientPlayerEntity player : source.getWorld().getPlayers()) {
|
||||
if (name.equals(player.getName().getLiteralString())) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
throw EntityArgumentType.PLAYER_NOT_FOUND_EXCEPTION.create();
|
||||
}
|
||||
|
||||
private static BlockPos getBlockPos(CommandContext<FabricClientCommandSource> context, String argName) {
|
||||
DefaultPosArgumentAccessor posArg = (DefaultPosArgumentAccessor) context.getArgument(argName, DefaultPosArgument.class);
|
||||
Vec3d pos = context.getSource().getPosition();
|
||||
return BlockPos.ofFloored(new Vec3d(
|
||||
posArg.getX().toAbsoluteCoordinate(pos.x),
|
||||
posArg.getY().toAbsoluteCoordinate(pos.y),
|
||||
posArg.getZ().toAbsoluteCoordinate(pos.z)
|
||||
));
|
||||
}
|
||||
|
||||
private static <T extends AttachmentTarget> void displayClientAttachmentInfo(
|
||||
CommandContext<FabricClientCommandSource> context,
|
||||
T target,
|
||||
Function<T, String> nameGetter
|
||||
) {
|
||||
context.getSource().sendFeedback(
|
||||
Text.literal("Attachments for target %s:".formatted(nameGetter.apply(target)))
|
||||
);
|
||||
boolean attAll = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_WITH_ALL);
|
||||
context.getSource().sendFeedback(
|
||||
Text.literal("Synced-with-all attachment: %s".formatted(attAll)).withColor(attAll ? Colors.GREEN : Colors.WHITE)
|
||||
);
|
||||
boolean attTarget = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_WITH_TARGET);
|
||||
context.getSource().sendFeedback(
|
||||
Text.literal("Synced-with-target attachment: %s".formatted(attTarget))
|
||||
.withColor(attTarget ? target == MinecraftClient.getInstance().player ? Colors.GREEN : Colors.RED : Colors.WHITE)
|
||||
);
|
||||
boolean attOther = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_EXCEPT_TARGET);
|
||||
context.getSource().sendFeedback(
|
||||
Text.literal("Synced-with-non-targets attachment: %s".formatted(attOther))
|
||||
.withColor(attOther ? target != MinecraftClient.getInstance().player ? Colors.GREEN : Colors.RED : Colors.WHITE)
|
||||
);
|
||||
boolean attCustom = target.getAttachedOrCreate(AttachmentTestMod.SYNCED_CREATIVE_ONLY);
|
||||
context.getSource().sendFeedback(
|
||||
Text.literal("Synced-with-creative attachment: %s".formatted(attCustom))
|
||||
.withColor(attCustom ? target instanceof PlayerEntity p && p.isCreative() ? Colors.GREEN : Colors.RED : Colors.WHITE)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ClientCommandRegistrationCallback.EVENT.register(
|
||||
(dispatcher, registryAccess) -> dispatcher.register(
|
||||
literal("attachment_test")
|
||||
.executes(context -> {
|
||||
displayClientAttachmentInfo(
|
||||
context,
|
||||
context.getSource().getPlayer(),
|
||||
PlayerEntity::getNameForScoreboard
|
||||
);
|
||||
return 1;
|
||||
})
|
||||
.then(chain(
|
||||
context -> {
|
||||
displayClientAttachmentInfo(
|
||||
context,
|
||||
parseClientPlayer(context.getSource(), StringArgumentType.getString(context, "target")),
|
||||
PlayerEntity::getNameForScoreboard
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
literal("player"),
|
||||
argument("target", StringArgumentType.word())
|
||||
))
|
||||
.then(chain(
|
||||
context -> {
|
||||
UUID uuid = context.getArgument("uuid", UUID.class);
|
||||
Entity entity = ((ClientWorldAccessor) context.getSource().getWorld())
|
||||
.invokeGetEntityLookup()
|
||||
.get(uuid);
|
||||
|
||||
if (entity == null) {
|
||||
throw AttachmentTestMod.TARGET_NOT_FOUND.create();
|
||||
}
|
||||
|
||||
displayClientAttachmentInfo(context, entity, e -> uuid.toString());
|
||||
return 1;
|
||||
},
|
||||
literal("entity"),
|
||||
argument("uuid", UuidArgumentType.uuid())
|
||||
))
|
||||
.then(chain(
|
||||
context -> {
|
||||
BlockPos pos = getBlockPos(context, "pos");
|
||||
BlockEntity be = context.getSource()
|
||||
.getWorld()
|
||||
.getBlockEntity(pos);
|
||||
|
||||
if (be == null) {
|
||||
throw AttachmentTestMod.TARGET_NOT_FOUND.create();
|
||||
}
|
||||
|
||||
displayClientAttachmentInfo(
|
||||
context,
|
||||
be,
|
||||
b -> pos.toShortString()
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
literal("blockentity"),
|
||||
argument("pos", BlockPosArgumentType.blockPos())
|
||||
))
|
||||
.then(chain(
|
||||
context -> {
|
||||
BlockPos pos = getBlockPos(context, "pos");
|
||||
Chunk chunk = context.getSource().getWorld().getChunk(pos);
|
||||
displayClientAttachmentInfo(
|
||||
context,
|
||||
chunk,
|
||||
c -> c.getPos().toString()
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
literal("chunk"),
|
||||
argument("pos", BlockPosArgumentType.blockPos())
|
||||
))
|
||||
.then(literal("world").executes(
|
||||
context -> {
|
||||
displayClientAttachmentInfo(
|
||||
context,
|
||||
context.getSource().getWorld(),
|
||||
w -> "world"
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static ArgumentBuilder<FabricClientCommandSource, ?> chain(
|
||||
Command<FabricClientCommandSource> command,
|
||||
ArgumentBuilder<FabricClientCommandSource, ?>... nodes
|
||||
) {
|
||||
ArgumentBuilder<FabricClientCommandSource, ?> result = nodes[nodes.length - 1].executes(command);
|
||||
|
||||
for (int i = nodes.length - 2; i >= 0; i--) {
|
||||
result = nodes[i].then(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.attachment.client.mixin;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
import net.minecraft.client.world.ClientWorld;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityLookup;
|
||||
|
||||
@Mixin(ClientWorld.class)
|
||||
public interface ClientWorldAccessor {
|
||||
@Invoker
|
||||
EntityLookup<Entity> invokeGetEntityLookup();
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.attachment.client.mixin;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import net.minecraft.command.argument.CoordinateArgument;
|
||||
import net.minecraft.command.argument.DefaultPosArgument;
|
||||
|
||||
@Mixin(DefaultPosArgument.class)
|
||||
public interface DefaultPosArgumentAccessor {
|
||||
@Accessor("x")
|
||||
CoordinateArgument getX();
|
||||
|
||||
@Accessor("y")
|
||||
CoordinateArgument getY();
|
||||
|
||||
@Accessor("z")
|
||||
CoordinateArgument getZ();
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"required": true,
|
||||
"package": "net.fabricmc.fabric.test.attachment.client.mixin",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"client": [
|
||||
"ClientWorldAccessor"
|
||||
],
|
||||
"mixins": [
|
||||
"DefaultPosArgumentAccessor"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue