diff --git a/patches/net/minecraft/commands/arguments/selector/EntitySelector.java.patch b/patches/net/minecraft/commands/arguments/selector/EntitySelector.java.patch index 19b61399be8..ec955e8a61b 100644 --- a/patches/net/minecraft/commands/arguments/selector/EntitySelector.java.patch +++ b/patches/net/minecraft/commands/arguments/selector/EntitySelector.java.patch @@ -9,3 +9,38 @@ throw EntityArgument.ERROR_SELECTORS_NOT_ALLOWED.create(); } } +@@ -150,8 +_,9 @@ + } else { + Predicate predicate = this.getPredicate(vec3, aabb, p_121161_.enabledFeatures()); + List list = new ObjectArrayList<>(); ++ // TODO: remove all this messy temp stuff + if (this.isWorldLimited()) { +- this.addEntities(list, p_121161_.getLevel(), aabb, predicate); ++ this.addEntities(list, p_121161_.getUnsidedLevel(), aabb, predicate); + } else { + for (ServerLevel serverlevel : p_121161_.getServer().getAllLevels()) { + this.addEntities(list, serverlevel, aabb, predicate); +@@ -163,13 +_,21 @@ + } + } + +- private void addEntities(List p_121155_, ServerLevel p_121156_, @Nullable AABB p_352947_, Predicate p_121158_) { ++ private void addEntities(List p_121155_, net.minecraft.world.level.Level p_121156_, @Nullable AABB p_352947_, Predicate p_121158_) { + int i = this.getResultLimit(); + if (p_121155_.size() < i) { + if (p_352947_ != null) { + p_121156_.getEntities(this.type, p_352947_, p_121158_, p_121155_, i); + } else { +- p_121156_.getEntities(this.type, p_121158_, p_121155_, i); ++ if (p_121156_ instanceof ServerLevel serverLevel) { ++ serverLevel.getEntities(this.type, p_121158_, p_121155_, i); ++ } else { ++ ((net.minecraft.client.multiplayer.ClientLevel) p_121156_).entitiesForRendering().forEach(entity -> { ++ if (p_121158_.test(entity) && p_121155_.size() < i) { ++ p_121155_.add(entity); ++ } ++ }); ++ } + } + } + } diff --git a/patches/net/minecraft/server/level/ChunkMap.java.patch b/patches/net/minecraft/server/level/ChunkMap.java.patch index d6dcc919098..aaa62346f9c 100644 --- a/patches/net/minecraft/server/level/ChunkMap.java.patch +++ b/patches/net/minecraft/server/level/ChunkMap.java.patch @@ -65,6 +65,27 @@ EntityType entitytype = p_140200_.getType(); int i = entitytype.clientTrackingRange() * 16; if (i != 0) { +@@ -1240,6 +_,20 @@ + }); + } + ++ // Neo: Getter for players watching an entity. ++ public List getPlayersWatching(Entity entity) { ++ var trackedEntity = entityMap.get(entity.getId()); ++ if (trackedEntity != null) { ++ var ret = new java.util.ArrayList(trackedEntity.seenBy.size()); ++ for (var connection : trackedEntity.seenBy) { ++ ret.add(connection.getPlayer()); ++ } ++ return List.copyOf(ret); ++ } else { ++ return List.of(); ++ } ++ } ++ + class DistanceManager extends net.minecraft.server.level.DistanceManager { + protected DistanceManager(Executor p_140459_, Executor p_140460_) { + super(p_140459_, p_140460_); @@ -1354,5 +_,20 @@ this.updatePlayer(serverplayer); } diff --git a/patches/net/minecraft/server/level/ServerEntity.java.patch b/patches/net/minecraft/server/level/ServerEntity.java.patch index 4f957b4d613..b62d7872a27 100644 --- a/patches/net/minecraft/server/level/ServerEntity.java.patch +++ b/patches/net/minecraft/server/level/ServerEntity.java.patch @@ -12,7 +12,7 @@ if (mapitemsaveddata != null) { for (ServerPlayer serverplayer : this.level.players()) { mapitemsaveddata.tickCarriedBy(serverplayer, itemstack); -@@ -273,22 +_,25 @@ +@@ -273,6 +_,7 @@ public void removePairing(ServerPlayer p_8535_) { this.entity.stopSeenByPlayer(p_8535_); p_8535_.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId())); @@ -20,23 +20,31 @@ } public void addPairing(ServerPlayer p_8542_) { - List> list = new ArrayList<>(); -- this.sendPairingData(p_8542_, list::add); -+ this.sendPairingData(p_8542_, new net.neoforged.neoforge.network.bundle.PacketAndPayloadAcceptor<>(list::add)); +@@ -280,15 +_,17 @@ + this.sendPairingData(p_8542_, list::add); p_8542_.connection.send(new ClientboundBundlePacket(list)); this.entity.startSeenByPlayer(p_8542_); + net.neoforged.neoforge.event.EventHooks.onStartEntityTracking(this.entity, p_8542_); } - public void sendPairingData(ServerPlayer p_289562_, Consumer> p_289563_) { -+ public void sendPairingData(ServerPlayer p_289562_, net.neoforged.neoforge.network.bundle.PacketAndPayloadAcceptor p_289563_) { ++ public void sendPairingData(ServerPlayer p_289562_, Consumer> p_289563_) { if (this.entity.isRemoved()) { LOGGER.warn("Fetching packet for removed entity {}", this.entity); } Packet packet = this.entity.getAddEntityPacket(this); p_289563_.accept(packet); -+ this.entity.sendPairingData(p_289562_, p_289563_::accept); ++ this.entity.sendPairingData(p_289562_, payload -> p_289563_.accept(payload.toVanillaClientbound())); if (this.trackedDataValues != null) { p_289563_.accept(new ClientboundSetEntityDataPacket(this.entity.getId(), this.trackedDataValues)); } +@@ -335,6 +_,8 @@ + if (this.entity instanceof Leashable leashable && leashable.isLeashed()) { + p_289563_.accept(new ClientboundSetEntityLinkPacket(this.entity, leashable.getLeashHolder())); + } ++ ++ net.neoforged.neoforge.attachment.AttachmentSync.sendEntityPairingData(this.entity, p_289562_, p_289563_); + } + + public Vec3 getPositionBase() { diff --git a/patches/net/minecraft/server/level/ServerLevel.java.patch b/patches/net/minecraft/server/level/ServerLevel.java.patch index bce6a22dcce..80c2e8261c8 100644 --- a/patches/net/minecraft/server/level/ServerLevel.java.patch +++ b/patches/net/minecraft/server/level/ServerLevel.java.patch @@ -242,7 +242,7 @@ ServerLevel.this.dragonParts.put(enderdragonpart.getId(), enderdragonpart); } } -@@ -1783,24 +_,101 @@ +@@ -1783,24 +_,106 @@ if (ServerLevel.this.isUpdatingNavigations) { String s = "onTrackingStart called during navigation iteration"; Util.logAndPauseIfInIde( @@ -273,6 +273,11 @@ } } + ++ @Override ++ public final void syncData(net.neoforged.neoforge.attachment.AttachmentType type) { ++ net.neoforged.neoforge.attachment.AttachmentSync.syncLevelUpdate(this, type); ++ } ++ + private final net.neoforged.neoforge.capabilities.CapabilityListenerHolder capListenerHolder = new net.neoforged.neoforge.capabilities.CapabilityListenerHolder(); + + @Override diff --git a/patches/net/minecraft/server/players/PlayerList.java.patch b/patches/net/minecraft/server/players/PlayerList.java.patch index a3c0ab67185..460e2d8fc01 100644 --- a/patches/net/minecraft/server/players/PlayerList.java.patch +++ b/patches/net/minecraft/server/players/PlayerList.java.patch @@ -116,6 +116,14 @@ p_11230_.connection.send(new ClientboundSetDefaultSpawnPositionPacket(p_11231_.getSharedSpawnPos(), p_11231_.getSharedSpawnAngle())); if (p_11231_.isRaining()) { p_11230_.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); +@@ -670,6 +_,7 @@ + + p_11230_.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0.0F)); + this.server.tickRateManager().updateJoiningPlayer(p_11230_); ++ net.neoforged.neoforge.attachment.AttachmentSync.sendLevelInfo(p_11231_, p_11230_); + } + + public void sendAllPlayerInfo(ServerPlayer p_11293_) { @@ -785,13 +_,6 @@ if (serverstatscounter == null) { File file1 = this.server.getWorldPath(LevelResource.PLAYER_STATS_DIR).toFile(); diff --git a/patches/net/minecraft/world/entity/Entity.java.patch b/patches/net/minecraft/world/entity/Entity.java.patch index 35d70c49b2f..381f757ccdf 100644 --- a/patches/net/minecraft/world/entity/Entity.java.patch +++ b/patches/net/minecraft/world/entity/Entity.java.patch @@ -438,7 +438,7 @@ } public void checkDespawn() { -@@ -3627,6 +_,128 @@ +@@ -3627,6 +_,133 @@ public boolean mayInteract(ServerLevel p_376870_, BlockPos p_146844_) { return true; @@ -555,6 +555,11 @@ + return super.setData(type, data); + } + ++ @Override ++ public final void syncData(net.neoforged.neoforge.attachment.AttachmentType type) { ++ net.neoforged.neoforge.attachment.AttachmentSync.syncEntityUpdate(this, type); ++ } ++ + // Neo: Hookup Capabilities getters to entities + @Nullable + public final T getCapability(net.neoforged.neoforge.capabilities.EntityCapability capability, C context) { diff --git a/patches/net/minecraft/world/level/block/entity/BlockEntity.java.patch b/patches/net/minecraft/world/level/block/entity/BlockEntity.java.patch index c2a70f7e4a1..e3b8517af8d 100644 --- a/patches/net/minecraft/world/level/block/entity/BlockEntity.java.patch +++ b/patches/net/minecraft/world/level/block/entity/BlockEntity.java.patch @@ -63,7 +63,7 @@ } public boolean triggerEvent(int p_58889_, int p_58890_) { -@@ -234,6 +_,27 @@ +@@ -234,6 +_,32 @@ return this.type; } @@ -87,6 +87,11 @@ + setChanged(); + return super.removeData(type); + } ++ ++ @Override ++ public final void syncData(net.neoforged.neoforge.attachment.AttachmentType type) { ++ net.neoforged.neoforge.attachment.AttachmentSync.syncBlockEntityUpdate(this, type); ++ } + @Deprecated public void setBlockState(BlockState p_155251_) { diff --git a/patches/net/minecraft/world/level/chunk/ChunkAccess.java.patch b/patches/net/minecraft/world/level/chunk/ChunkAccess.java.patch index 303f56095aa..c02b6d9a96d 100644 --- a/patches/net/minecraft/world/level/chunk/ChunkAccess.java.patch +++ b/patches/net/minecraft/world/level/chunk/ChunkAccess.java.patch @@ -107,7 +107,7 @@ + } + + @org.jetbrains.annotations.ApiStatus.Internal -+ protected net.neoforged.neoforge.attachment.AttachmentHolder.AsField getAttachmentHolder() { ++ public net.neoforged.neoforge.attachment.AttachmentHolder.AsField getAttachmentHolder() { + return attachmentHolder; + } + diff --git a/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch b/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch index 4a9a08cd9f0..ca0ecdca58d 100644 --- a/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch +++ b/patches/net/minecraft/world/level/chunk/LevelChunk.java.patch @@ -120,7 +120,7 @@ this.blockEntities.values().forEach(p_187988_ -> { if (this.level instanceof ServerLevel serverlevel) { this.addGameEventListener(p_187988_, serverlevel); -@@ -694,6 +_,14 @@ +@@ -694,6 +_,19 @@ return new LevelChunk.BoundTickingBlockEntity<>(p_156376_, p_156377_); } @@ -131,6 +131,11 @@ + public net.neoforged.neoforge.common.world.LevelChunkAuxiliaryLightManager getAuxLightManager(ChunkPos pos) { + return auxLightManager; + } ++ ++ @Override ++ public final void syncData(net.neoforged.neoforge.attachment.AttachmentType type) { ++ net.neoforged.neoforge.attachment.AttachmentSync.syncChunkUpdate(this, getAttachmentHolder(), type); ++ } + class BoundTickingBlockEntity implements TickingBlockEntity { private final T blockEntity; diff --git a/src/main/java/net/neoforged/neoforge/attachment/AttachmentHolder.java b/src/main/java/net/neoforged/neoforge/attachment/AttachmentHolder.java index e822d369cf1..f9633c7867c 100644 --- a/src/main/java/net/neoforged/neoforge/attachment/AttachmentHolder.java +++ b/src/main/java/net/neoforged/neoforge/attachment/AttachmentHolder.java @@ -78,6 +78,7 @@ public final T getData(AttachmentType type) { if (ret == null) { ret = type.defaultValueSupplier.apply(getExposedHolder()); attachments.put(type, ret); + syncData(type); } return ret; } @@ -96,7 +97,9 @@ public Optional getExistingData(AttachmentType type) { public @Nullable T setData(AttachmentType type, T data) { validateAttachmentType(type); Objects.requireNonNull(data); - return (T) getAttachmentMap().put(type, data); + var previousData = (T) getAttachmentMap().put(type, data); + syncData(type); + return previousData; } @Override @@ -106,7 +109,9 @@ public Optional getExistingData(AttachmentType type) { if (attachments == null) { return null; } - return (T) attachments.remove(type); + var previousData = (T) attachments.remove(type); + syncData(type); + return previousData; } /** @@ -179,5 +184,10 @@ IAttachmentHolder getExposedHolder() { public void deserializeInternal(HolderLookup.Provider provider, CompoundTag tag) { deserializeAttachments(provider, tag); } + + @Override + public void syncData(AttachmentType type) { + exposedHolder.syncData(type); + } } } diff --git a/src/main/java/net/neoforged/neoforge/attachment/AttachmentSync.java b/src/main/java/net/neoforged/neoforge/attachment/AttachmentSync.java new file mode 100644 index 00000000000..8e60708a05c --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/attachment/AttachmentSync.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.attachment; + +import io.netty.buffer.Unpooled; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBundlePacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.common.util.FriendlyByteBufUtil; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.level.ChunkWatchEvent; +import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; +import net.neoforged.neoforge.network.connection.ConnectionType; +import net.neoforged.neoforge.network.payload.SyncAttachmentsPayload; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegistryBuilder; +import net.neoforged.neoforge.registries.callback.AddCallback; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +@EventBusSubscriber(modid = NeoForgeVersion.MOD_ID) +public final class AttachmentSync { + /** + * Contains all entries added to {@link NeoForgeRegistries#ATTACHMENT_TYPES} with a sync handler. + * Should never be registered against directly. + */ + public static final Registry> SYNCED_ATTACHMENT_TYPES = new RegistryBuilder<>( + ResourceKey.>createRegistryKey( + ResourceLocation.fromNamespaceAndPath(NeoForgeVersion.MOD_ID, "synced_attachment_types"))) + .sync(true) + .create(); + + public static final AddCallback> ATTACHMENT_TYPE_ADD_CALLBACK = (registry, id, key, value) -> { + if (value.syncHandler != null) { + Registry.register(SYNCED_ATTACHMENT_TYPES, key.location(), value); + } + }; + + static SyncAttachmentsPayload.Target syncTarget(AttachmentHolder holder) { + return switch (holder) { + case BlockEntity blockEntity -> new SyncAttachmentsPayload.BlockEntityTarget(blockEntity.getBlockPos()); + case AttachmentHolder.AsField asField when asField.getExposedHolder() instanceof LevelChunk chunk -> new SyncAttachmentsPayload.ChunkTarget(chunk.getPos()); + case Entity entity -> new SyncAttachmentsPayload.EntityTarget(entity.getId()); + case Level ignored -> new SyncAttachmentsPayload.LevelTarget(); + default -> throw new UnsupportedOperationException("Attachment holder class is not supported: " + holder); + }; + } + + private static void syncUpdate(AttachmentHolder holder, AttachmentType type, List players) { + RegistryAccess registryAccess = null; + for (var player : players) { + if (type.syncHandler.sendToPlayer(holder.getExposedHolder(), player)) { + registryAccess = player.registryAccess(); + break; + } + } + // This also serves as a short-circuit if there are no players to sync data to. + if (registryAccess == null) { + return; + } + var data = FriendlyByteBufUtil.writeCustomData(buf -> { + // TODO: what if data is missing? + type.syncHandler.write(buf, holder.getData(type), false); + }, registryAccess); + var packet = new SyncAttachmentsPayload(syncTarget(holder), List.of(type), data).toVanillaClientbound(); + for (var player : players) { + if (type.syncHandler.sendToPlayer(holder.getExposedHolder(), player)) { + player.connection.send(packet); + } + } + } + + public static void syncBlockEntityUpdate(BlockEntity blockEntity, AttachmentType type) { + if (type.syncHandler == null || !(blockEntity.getLevel() instanceof ServerLevel serverLevel)) { + return; + } + syncUpdate(blockEntity, type, serverLevel.getChunkSource().chunkMap.getPlayers(new ChunkPos(blockEntity.getBlockPos()), false)); + } + + public static void syncChunkUpdate(LevelChunk chunk, AttachmentHolder.AsField holder, AttachmentType type) { + if (type.syncHandler == null || !(chunk.getLevel() instanceof ServerLevel serverLevel)) { + return; + } + syncUpdate(holder, type, serverLevel.getChunkSource().chunkMap.getPlayers(chunk.getPos(), false)); + } + + public static void syncEntityUpdate(Entity entity, AttachmentType type) { + if (type.syncHandler == null || !(entity.level() instanceof ServerLevel serverLevel)) { + return; + } + var players = serverLevel.getChunkSource().chunkMap.getPlayersWatching(entity); + if (entity instanceof ServerPlayer serverPlayer) { + // Players do not track themselves + var newPlayers = new ArrayList(players.size() + 1); + newPlayers.addAll(players); + newPlayers.add(serverPlayer); + players = newPlayers; + } + syncUpdate(entity, type, players); + } + + public static void syncLevelUpdate(ServerLevel level, AttachmentType type) { + if (type.syncHandler == null) { + return; + } + syncUpdate(level, type, level.players()); + } + + @Nullable + private static SyncAttachmentsPayload syncInitialAttachments(AttachmentHolder holder, ServerPlayer to) { + if (holder.attachments == null) { + return null; + } + boolean anySyncableAttachment = false; + for (var attachment : holder.attachments.keySet()) { + anySyncableAttachment = anySyncableAttachment | attachment.syncHandler != null; + } + if (!anySyncableAttachment) { + return null; + } + List> syncedTypes = new ArrayList<>(); + var data = FriendlyByteBufUtil.writeCustomData(buf -> { + for (var entry : holder.attachments.entrySet()) { + AttachmentType type = entry.getKey(); + @SuppressWarnings("unchecked") + var syncHandler = (IAttachmentSyncHandler) type.syncHandler; + if (syncHandler != null) { + int indexBefore = buf.writerIndex(); + syncHandler.write(buf, entry.getValue(), true); + if (indexBefore < buf.writerIndex()) { + // Actually wrote something + syncedTypes.add(type); + } + } + } + }, to.registryAccess()); + return new SyncAttachmentsPayload(syncTarget(holder), syncedTypes, data); + } + + /** + * Handles initial syncing of block entity and chunk attachments. + */ + @SubscribeEvent + public static void onChunkSent(ChunkWatchEvent.Sent event) { + List> packets = new ArrayList<>(); + var chunkPayload = syncInitialAttachments(event.getChunk().getAttachmentHolder(), event.getPlayer()); + if (chunkPayload != null) { + packets.add(chunkPayload.toVanillaClientbound()); + } + for (var blockEntity : event.getChunk().getBlockEntities().values()) { + var blockEntityPayload = syncInitialAttachments(blockEntity, event.getPlayer()); + if (blockEntityPayload != null) { + packets.add(blockEntityPayload.toVanillaClientbound()); + } + } + if (!packets.isEmpty()) { + event.getPlayer().connection.send(new ClientboundBundlePacket(packets)); + } + } + + /** + * Handles initial syncing of entity attachments, except for a player's own attachments. + */ + public static void sendEntityPairingData(Entity entity, ServerPlayer to, Consumer> packetConsumer) { + var packet = syncInitialAttachments(entity, to); + if (packet != null) { + packetConsumer.accept(packet.toVanillaClientbound()); + } + } + + /** + * Handles initial syncing of a player's own attachments. + */ + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + var player = (ServerPlayer) event.getEntity(); + var packet = syncInitialAttachments(event.getEntity(), player); + if (packet != null) { + player.connection.send(packet.toVanillaClientbound()); + } + } + + /** + * Handles initial syncing of level attachments. Needs to be called for login, respawn and teleports. + */ + public static void sendLevelInfo(ServerLevel level, ServerPlayer to) { + var packet = syncInitialAttachments(level, to); + if (packet != null) { + to.connection.send(packet.toVanillaClientbound()); + } + } + + public static void receiveSyncedDataAttachments(AttachmentHolder holder, RegistryAccess registryAccess, List> types, byte[] bytes) { + var buf = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(bytes), registryAccess, ConnectionType.NEOFORGE); + try { + for (var type : types) { + @SuppressWarnings("unchecked") + var syncHandler = (IAttachmentSyncHandler) type.syncHandler; + if (syncHandler == null) { + throw new IllegalArgumentException("Received synced attachment type without a sync handler registered: " + NeoForgeRegistries.ATTACHMENT_TYPES.getKey(type)); + } + var previousValue = holder.attachments == null ? null : holder.attachments.get(type); + var result = syncHandler.read(holder.getExposedHolder(), buf, previousValue); + if (result == null) { + if (holder.attachments != null) { + holder.attachments.remove(type); + } + } else { + holder.getAttachmentMap().put(type, result); + } + } + } catch (Exception exception) { + throw new RuntimeException("Encountered exception when reading synced data attachments: " + types, exception); + } finally { + buf.release(); + } + } + + private AttachmentSync() {} +} diff --git a/src/main/java/net/neoforged/neoforge/attachment/AttachmentType.java b/src/main/java/net/neoforged/neoforge/attachment/AttachmentType.java index f7994a8cc7c..40e760e3927 100644 --- a/src/main/java/net/neoforged/neoforge/attachment/AttachmentType.java +++ b/src/main/java/net/neoforged/neoforge/attachment/AttachmentType.java @@ -14,8 +14,9 @@ import net.minecraft.core.HolderLookup; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.entity.Entity; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.chunk.ChunkAccess; @@ -39,19 +40,13 @@ *
  • Serializable entity attachments are not copied on death by default (but they are copied when returning from the end).
  • *
  • Serializable entity attachments can opt into copying on death via {@link Builder#copyOnDeath()}.
  • * - *

    {@link ItemStack}-exclusive behavior:

    - *
      - *
    • Serializable item stack attachments are synced between the server and the client.
    • - *
    • Serializable item stack attachments are copied when an item stack is copied.
    • - *
    • Serializable item stack attachments must match for item stack comparison to succeed.
    • - *
    *

    {@link Level}-exclusive behavior:

    *
      *
    • (nothing)
    • *
    *

    {@link ChunkAccess}-exclusive behavior:

    *
      - *
    • Modifications to attachments should be followed by a call to {@link ChunkAccess#setUnsaved(boolean)}.
    • + *
    • Modifications to attachments should be followed by a call to {@link ChunkAccess#markUnsaved}.
    • *
    • Serializable attachments are copied from a {@link ProtoChunk} to a {@link LevelChunk} on promotion.
    • *
    */ @@ -61,12 +56,15 @@ public final class AttachmentType { final IAttachmentSerializer serializer; final boolean copyOnDeath; final IAttachmentCopyHandler copyHandler; + @Nullable + IAttachmentSyncHandler syncHandler; private AttachmentType(Builder builder) { this.defaultValueSupplier = builder.defaultValueSupplier; this.serializer = builder.serializer; this.copyOnDeath = builder.copyOnDeath; this.copyHandler = builder.copyHandler != null ? builder.copyHandler : defaultCopyHandler(serializer); + this.syncHandler = builder.syncHandler; } private static IAttachmentCopyHandler defaultCopyHandler(@Nullable IAttachmentSerializer serializer) { @@ -152,6 +150,8 @@ public static class Builder { private boolean copyOnDeath; @Nullable private IAttachmentCopyHandler copyHandler; + @Nullable + private IAttachmentSyncHandler syncHandler; private Builder(Function defaultValueSupplier) { this.defaultValueSupplier = defaultValueSupplier; @@ -174,9 +174,6 @@ public Builder serialize(IAttachmentSerializer serializer) { /** * Requests that this attachment be persisted to disk (on the logical server side), using a {@link Codec}. * - *

    Using a {@link Codec} to serialize attachments is discouraged for item stack attachments, - * for performance reasons. Prefer one of the other options. - * *

    Codec-based attachments cannot capture a reference to their holder. * * @param codec The codec to use. @@ -188,9 +185,6 @@ public Builder serialize(Codec codec) { /** * Requests that this attachment be persisted to disk (on the logical server side), using a {@link Codec}. * - *

    Using a {@link Codec} to serialize attachments is discouraged for item stack attachments, - * for performance reasons. Prefer one of the other options. - * *

    Codec-based attachments cannot capture a reference to their holder. * * @param codec The codec to use. @@ -239,6 +233,31 @@ public Builder copyHandler(IAttachmentCopyHandler cloner) { return this; } + /** + * Requests that this attachment be synced to clients. + */ + public Builder sync(IAttachmentSyncHandler syncHandler) { + Objects.requireNonNull(syncHandler); + this.syncHandler = syncHandler; + return this; + } + + // TODO: Predicate version too? Some data is not relevant to other players. + public Builder sync(StreamCodec streamCodec) { + Objects.requireNonNull(streamCodec); + return sync(new IAttachmentSyncHandler<>() { + @Override + public void write(RegistryFriendlyByteBuf buf, T attachment, boolean initialSync) { + streamCodec.encode(buf, attachment); + } + + @Override + public T read(IAttachmentHolder holder, RegistryFriendlyByteBuf buf, @Nullable T previousValue) { + return streamCodec.decode(buf); + } + }); + } + public AttachmentType build() { return new AttachmentType<>(this); } diff --git a/src/main/java/net/neoforged/neoforge/attachment/IAttachmentHolder.java b/src/main/java/net/neoforged/neoforge/attachment/IAttachmentHolder.java index 11797e3c9ba..7aec4819440 100644 --- a/src/main/java/net/neoforged/neoforge/attachment/IAttachmentHolder.java +++ b/src/main/java/net/neoforged/neoforge/attachment/IAttachmentHolder.java @@ -94,4 +94,24 @@ default Optional getExistingData(Supplier> type) { default @Nullable T removeData(Supplier> type) { return removeData(type.get()); } + + /** + * Syncs a data attachment of the given type with all relevant clients. + * + * @see IAttachmentSyncHandler + */ + // TODO: what happens if there is no such data? + // TODO: auto sync on getData, removeData, other modifications + default void syncData(AttachmentType type) { + // TODO: do nothing by default? + } + + /** + * Syncs a data attachment of the given type with all relevant clients. + * + * @see IAttachmentSyncHandler + */ + default void syncData(Supplier> type) { + syncData(type.get()); + } } diff --git a/src/main/java/net/neoforged/neoforge/attachment/IAttachmentSyncHandler.java b/src/main/java/net/neoforged/neoforge/attachment/IAttachmentSyncHandler.java new file mode 100644 index 00000000000..b90c8a4505d --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/attachment/IAttachmentSyncHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.attachment; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +/** + * Manages how data attachments are written (on the server) and read (on the client) from packets. + * + *

    Sync is handled automatically in the following cases: + *

      + *
    • A client is receiving initial data for this attachment holder.
    • + *
    • An attachment is default-created through {@link IAttachmentHolder#getData(AttachmentType)}.
    • + *
    • An attachment is updated through {@link IAttachmentHolder#setData(AttachmentType, Object)}.
    • + *
    • An attachment is removed through {@link IAttachmentHolder#removeData(AttachmentType)}.
    • + *
    + * + *

    For other cases such as modifications to mutable synced attachments, + * {@link IAttachmentHolder#syncData(AttachmentType)} can be called to trigger syncing. + */ +public interface IAttachmentSyncHandler { + /** + * Decides whether data should be sent to some player that can see the holder. + * + *

    By default, all players that can see the holder are sent the data. + * A typical use case for this method is to only send player-specific data to that player. + * + *

    The returned value should be consistent for a given holder and player. + * + * @param holder the holder for the attachment, can be cast if the subtype is known + * @param to the player that might receive the data + * @return {@code true} to send data to the player, {@code false} otherwise + */ + default boolean sendToPlayer(IAttachmentHolder holder, ServerPlayer to) { + return true; + } + + /** + * Writes attachment data to a buffer. + * + *

    If {@code initialSync} is {@code true}, + * the data should be written in full because the client does not have any previous data. + * + *

    If {@code initialSync} is {@code false}, + * the client already received a previous version of the data. + * In this case, this method is only called once for the attachment, + * and the resulting data is broadcast to all relevant players. + */ + void write(RegistryFriendlyByteBuf buf, T attachment, boolean initialSync); + + /** + * Reads attachment data on the client side. + * + * @param holder the attachment holder, can be cast if the subtype is known + * @param previousValue the previous value of the attachment, or {@code null} if there was no previous value + * @return the new value of the attachment, or {@code null} if the attachment should be removed + */ + @Nullable + T read(IAttachmentHolder holder, RegistryFriendlyByteBuf buf, @Nullable T previousValue); +} diff --git a/src/main/java/net/neoforged/neoforge/client/ClientCommandSourceStack.java b/src/main/java/net/neoforged/neoforge/client/ClientCommandSourceStack.java index 471b085f821..29761953e04 100644 --- a/src/main/java/net/neoforged/neoforge/client/ClientCommandSourceStack.java +++ b/src/main/java/net/neoforged/neoforge/client/ClientCommandSourceStack.java @@ -27,6 +27,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; +import net.minecraft.world.flag.FeatureFlagSet; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec2; import net.minecraft.world.phys.Vec3; @@ -95,6 +96,11 @@ public RegistryAccess registryAccess() { return Minecraft.getInstance().getConnection().registryAccess(); } + @Override + public FeatureFlagSet enabledFeatures() { + return Minecraft.getInstance().getConnection().enabledFeatures(); + } + /** * {@return the scoreboard from the client side} */ diff --git a/src/main/java/net/neoforged/neoforge/network/NetworkInitialization.java b/src/main/java/net/neoforged/neoforge/network/NetworkInitialization.java index 699ece2487a..4ff2ed90043 100644 --- a/src/main/java/net/neoforged/neoforge/network/NetworkInitialization.java +++ b/src/main/java/net/neoforged/neoforge/network/NetworkInitialization.java @@ -30,6 +30,7 @@ import net.neoforged.neoforge.network.payload.KnownRegistryDataMapsPayload; import net.neoforged.neoforge.network.payload.KnownRegistryDataMapsReplyPayload; import net.neoforged.neoforge.network.payload.RegistryDataMapSyncPayload; +import net.neoforged.neoforge.network.payload.SyncAttachmentsPayload; import net.neoforged.neoforge.network.registration.PayloadRegistrar; import net.neoforged.neoforge.registries.ClientRegistryManager; import net.neoforged.neoforge.registries.RegistryManager; @@ -105,6 +106,10 @@ private static void register(final RegisterPayloadHandlersEvent event) { .playToClient( ClientboundCustomSetTimePayload.TYPE, ClientboundCustomSetTimePayload.STREAM_CODEC, + ClientPayloadHandler::handle) + .playToClient( + SyncAttachmentsPayload.TYPE, + SyncAttachmentsPayload.STREAM_CODEC, ClientPayloadHandler::handle); } } diff --git a/src/main/java/net/neoforged/neoforge/network/bundle/PacketAndPayloadAcceptor.java b/src/main/java/net/neoforged/neoforge/network/bundle/PacketAndPayloadAcceptor.java deleted file mode 100644 index d68959fee75..00000000000 --- a/src/main/java/net/neoforged/neoforge/network/bundle/PacketAndPayloadAcceptor.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.network.bundle; - -import java.util.function.Consumer; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.common.ClientCommonPacketListener; -import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; -import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public class PacketAndPayloadAcceptor { - private final Consumer> consumer; - - public PacketAndPayloadAcceptor(Consumer> consumer) { - this.consumer = consumer; - } - - public PacketAndPayloadAcceptor accept(Packet packet) { - consumer.accept(packet); - return this; - } - - public PacketAndPayloadAcceptor accept(CustomPacketPayload payload) { - return accept(new ClientboundCustomPayloadPacket(payload)); - } -} diff --git a/src/main/java/net/neoforged/neoforge/network/handlers/ClientPayloadHandler.java b/src/main/java/net/neoforged/neoforge/network/handlers/ClientPayloadHandler.java index efd17ee853c..4d2461c0bb9 100644 --- a/src/main/java/net/neoforged/neoforge/network/handlers/ClientPayloadHandler.java +++ b/src/main/java/net/neoforged/neoforge/network/handlers/ClientPayloadHandler.java @@ -24,6 +24,8 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.neoforged.neoforge.attachment.AttachmentSync; import net.neoforged.neoforge.common.world.AuxiliaryLightManager; import net.neoforged.neoforge.common.world.LevelChunkAuxiliaryLightManager; import net.neoforged.neoforge.entity.IEntityWithComplexSpawn; @@ -38,6 +40,7 @@ import net.neoforged.neoforge.network.payload.FrozenRegistryPayload; import net.neoforged.neoforge.network.payload.FrozenRegistrySyncCompletedPayload; import net.neoforged.neoforge.network.payload.FrozenRegistrySyncStartPayload; +import net.neoforged.neoforge.network.payload.SyncAttachmentsPayload; import net.neoforged.neoforge.registries.RegistryManager; import net.neoforged.neoforge.registries.RegistrySnapshot; import org.jetbrains.annotations.ApiStatus; @@ -155,4 +158,37 @@ public static void handle(final ClientboundCustomSetTimePayload payload, final I level.setDayTimeFraction(payload.dayTimeFraction()); level.setDayTimePerTick(payload.dayTimePerTick()); } + + public static void handle(SyncAttachmentsPayload payload, IPayloadContext context) { + switch (payload.target()) { + case SyncAttachmentsPayload.BlockEntityTarget target -> { + var blockEntity = context.player().level().getBlockEntity(target.pos()); + if (blockEntity == null) { + LOGGER.warn("Received synced attachments from unknown block entity"); + } else { + AttachmentSync.receiveSyncedDataAttachments(blockEntity, context.player().registryAccess(), payload.types(), payload.syncPayload()); + } + } + case SyncAttachmentsPayload.ChunkTarget target -> { + var pos = target.pos(); + var chunk = context.player().level().getChunk(pos.x, pos.z, ChunkStatus.FULL, false); + if (chunk == null) { + LOGGER.warn("Received synced attachments from unknown chunk"); + } else { + AttachmentSync.receiveSyncedDataAttachments(chunk.getAttachmentHolder(), chunk.getLevel().registryAccess(), payload.types(), payload.syncPayload()); + } + } + case SyncAttachmentsPayload.EntityTarget target -> { + var entity = context.player().level().getEntity(target.entity()); + if (entity == null) { + LOGGER.warn("Received synced attachments from unknown entity"); + } else { + AttachmentSync.receiveSyncedDataAttachments(entity, entity.registryAccess(), payload.types(), payload.syncPayload()); + } + } + case SyncAttachmentsPayload.LevelTarget ignored -> { + AttachmentSync.receiveSyncedDataAttachments(context.player().level(), context.player().registryAccess(), payload.types(), payload.syncPayload()); + } + } + } } diff --git a/src/main/java/net/neoforged/neoforge/network/payload/SyncAttachmentsPayload.java b/src/main/java/net/neoforged/neoforge/network/payload/SyncAttachmentsPayload.java new file mode 100644 index 00000000000..083527d228d --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/network/payload/SyncAttachmentsPayload.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.network.payload; + +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.neoforged.neoforge.attachment.AttachmentSync; +import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; +import net.neoforged.neoforge.network.codec.NeoForgeStreamCodecs; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public record SyncAttachmentsPayload( + Target target, + List> types, + byte[] syncPayload) + implements CustomPacketPayload { + + public static final Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(NeoForgeVersion.MOD_ID, "sync_attachments"));; + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + Target.STREAM_CODEC, + SyncAttachmentsPayload::target, + ByteBufCodecs.registry(AttachmentSync.SYNCED_ATTACHMENT_TYPES.key()).apply(ByteBufCodecs.list()), + SyncAttachmentsPayload::types, + NeoForgeStreamCodecs.UNBOUNDED_BYTE_ARRAY, + SyncAttachmentsPayload::syncPayload, + SyncAttachmentsPayload::new); + + @Override + public Type type() { + return TYPE; + } + public sealed interface Target { + StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, target) -> { + switch (target) { + case BlockEntityTarget blockEntityTarget -> { + buf.writeByte(0); + buf.writeBlockPos(blockEntityTarget.pos()); + } + case ChunkTarget chunkTarget -> { + buf.writeByte(1); + buf.writeChunkPos(chunkTarget.pos()); + } + case EntityTarget entityTarget -> { + buf.writeByte(2); + buf.writeVarInt(entityTarget.entity()); + } + case LevelTarget ignored -> { + buf.writeByte(3); + } + } + }, + buf -> { + int type = buf.readByte(); + switch (type) { + case 0 -> { + return new BlockEntityTarget(buf.readBlockPos()); + } + case 1 -> { + return new ChunkTarget(buf.readChunkPos()); + } + case 2 -> { + return new EntityTarget(buf.readVarInt()); + } + case 3 -> { + return new LevelTarget(); + } + default -> throw new IllegalArgumentException("Unknown target type: " + type); + } + }); + } + + public record BlockEntityTarget(BlockPos pos) implements Target {} + + public record ChunkTarget(ChunkPos pos) implements Target {} + + public record EntityTarget(int entity) implements Target {} + + // TODO: Should there be a way to sync overworld data while the player is in another level? (For "global" data). + public record LevelTarget() implements Target {} +} diff --git a/src/main/java/net/neoforged/neoforge/registries/NeoForgeRegistriesSetup.java b/src/main/java/net/neoforged/neoforge/registries/NeoForgeRegistriesSetup.java index bd505e9fe42..dba99f3ee5a 100644 --- a/src/main/java/net/neoforged/neoforge/registries/NeoForgeRegistriesSetup.java +++ b/src/main/java/net/neoforged/neoforge/registries/NeoForgeRegistriesSetup.java @@ -9,6 +9,7 @@ import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.attachment.AttachmentSync; import org.jetbrains.annotations.ApiStatus; @ApiStatus.Internal @@ -71,6 +72,7 @@ private static void registerRegistries(NewRegistryEvent event) { event.register(NeoForgeRegistries.FLUID_INGREDIENT_TYPES); event.register(NeoForgeRegistries.CONDITION_SERIALIZERS); event.register(NeoForgeRegistries.ATTACHMENT_TYPES); + event.register(AttachmentSync.SYNCED_ATTACHMENT_TYPES); } private static void modifyRegistries(ModifyRegistriesEvent event) { @@ -82,5 +84,6 @@ private static void modifyRegistries(ModifyRegistriesEvent event) { BuiltInRegistries.ITEM.addCallback(NeoForgeRegistryCallbacks.ItemCallbacks.INSTANCE); BuiltInRegistries.ATTRIBUTE.addCallback(NeoForgeRegistryCallbacks.AttributeCallbacks.INSTANCE); BuiltInRegistries.POINT_OF_INTEREST_TYPE.addCallback(NeoForgeRegistryCallbacks.PoiTypeCallbacks.INSTANCE); + NeoForgeRegistries.ATTACHMENT_TYPES.addCallback(AttachmentSync.ATTACHMENT_TYPE_ADD_CALLBACK); } } diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/AttachmentSyncTest.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/AttachmentSyncTest.java new file mode 100644 index 00000000000..7e3065dda61 --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/oldtest/AttachmentSyncTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.oldtest; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.serialization.Codec; +import java.util.function.Supplier; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.attachment.IAttachmentHolder; +import net.neoforged.neoforge.attachment.IAttachmentSyncHandler; +import net.neoforged.neoforge.client.event.RegisterClientCommandsEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import org.jetbrains.annotations.Nullable; + +@Mod(AttachmentSyncTest.MOD_ID) +public class AttachmentSyncTest { + public static final String MOD_ID = "attachment_sync_test"; + private static final DeferredRegister> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.ATTACHMENT_TYPES, MOD_ID); + private static final Supplier> ATTACHMENT_TYPE = ATTACHMENT_TYPES.register("test", + () -> AttachmentType.builder(() -> 0) + .serialize(Codec.INT) + // TODO: use streamcodec version at some point + .sync(new IAttachmentSyncHandler() { + @Override + public void write(RegistryFriendlyByteBuf buf, Integer attachment, boolean initialSync) { + buf.writeInt(attachment); + } + + @Override + public Integer read(IAttachmentHolder holder, RegistryFriendlyByteBuf buf, @Nullable Integer previousValue) { + return buf.readInt(); + } + }) + .build()); + + public AttachmentSyncTest(IEventBus modBus) { + ATTACHMENT_TYPES.register(modBus); + + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> { + registerCommands(event.getDispatcher(), "attachment_sync_test"); + }); + } + + @EventBusSubscriber(Dist.CLIENT) + static class ClientOnly { + @SubscribeEvent + private static void registerClientCommands(RegisterClientCommandsEvent event) { + registerCommands(event.getDispatcher(), "attachment_sync_test_client"); + } + } + + private static final SimpleCommandExceptionType ERROR_NOT_A_BLOCK_ENTITY = new SimpleCommandExceptionType(Component.literal("Not a block entity")); + + private static void registerCommands(CommandDispatcher dispatcher, String commandName) { + dispatcher.register(Commands.literal(commandName) + .requires(source -> source.hasPermission(4)) + .then(Commands.literal("blockentity") + .then( + addGetSet( + Commands.argument("pos", BlockPosArgument.blockPos()), + context -> { + var pos = BlockPosArgument.getBlockPos(context, "pos"); + var blockEntity = context.getSource().getUnsidedLevel().getBlockEntity(pos); + if (blockEntity == null) { + throw ERROR_NOT_A_BLOCK_ENTITY.create(); + } + return blockEntity; + }))) + .then(Commands.literal("chunk") + .then( + addGetSet( + Commands.argument("pos", BlockPosArgument.blockPos()), + context -> { + var pos = BlockPosArgument.getBlockPos(context, "pos"); + return context.getSource().getUnsidedLevel().getChunkAt(pos); + }))) + .then(Commands.literal("entity") + .then( + addGetSet( + Commands.argument("entity", EntityArgument.entity()), + context -> EntityArgument.getEntity(context, "entity")))) + .then( + addGetSet( + Commands.literal("level"), + context -> context.getSource().getUnsidedLevel()))); + } + + private interface HolderFinder { + IAttachmentHolder find(CommandContext source) throws CommandSyntaxException; + } + + private static ArgumentBuilder addGetSet(ArgumentBuilder builder, HolderFinder holderFinder) { + return builder + .then(Commands.literal("get") + .executes(context -> { + var holder = holderFinder.find(context); + var data = holder.getExistingData(ATTACHMENT_TYPE).orElse(null); + context.getSource().sendSuccess(() -> Component.literal("Value of data: " + data), false); + return Command.SINGLE_SUCCESS; + })) + .then(Commands.literal("set") + .then(Commands.argument("value", IntegerArgumentType.integer()) + .executes(context -> { + var holder = holderFinder.find(context); + var data = IntegerArgumentType.getInteger(context, "value"); + var previousData = holder.setData(ATTACHMENT_TYPE, data); + context.getSource().sendSuccess(() -> Component.literal("Previous value of data: " + previousData + ". New value: " + data), false); + return Command.SINGLE_SUCCESS; + }))); + } +} diff --git a/tests/src/main/resources/META-INF/neoforge.mods.toml b/tests/src/main/resources/META-INF/neoforge.mods.toml index 481bcfb8c27..5f494851df9 100644 --- a/tests/src/main/resources/META-INF/neoforge.mods.toml +++ b/tests/src/main/resources/META-INF/neoforge.mods.toml @@ -32,6 +32,8 @@ license="LGPL v2.1" [[mods]] modId="custom_preset_editor_test" +[[mods]] + modId="attachment_sync_test" [[mods]] modId="custom_break_sound_test"