From b2d329c4693098f7db451db10bb426d86a1538c5 Mon Sep 17 00:00:00 2001 From: qiuye2024github Date: Thu, 30 Apr 2026 15:06:29 +0800 Subject: [PATCH 1/3] feat(sync): validate annotation metadata eagerly --- .../api/sync_system/meta/ClassSyncData.java | 73 ++++++++++++++++++- .../synceddata/test/SyncAnnotationTest.java | 25 +++++++ .../synceddata/test/SyncTestFixtures.java | 18 +++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/meta/ClassSyncData.java b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/ClassSyncData.java index d6127c3..b50d3a6 100644 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/meta/ClassSyncData.java +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/ClassSyncData.java @@ -48,6 +48,12 @@ public static ClassSyncData getClassData(Class cls) { private final Set serverSyncFields = new ObjectOpenHashSet<>(); @Getter private final Set bothSyncFields = new ObjectOpenHashSet<>(); + @Getter + private final Set serverUpdateFields = new ObjectOpenHashSet<>(); + @Getter + private FieldSyncData[] orderedClientSyncFields = new FieldSyncData[0]; + @Getter + private FieldSyncData[] orderedServerUpdateFields = new FieldSyncData[0]; private ClassSyncData(Class clazz) { MethodHandles.Lookup privateLookup; @@ -60,6 +66,7 @@ private ClassSyncData(Class clazz) { } Map> changeListeners = new HashMap<>(); + Set clientListenerTargets = new HashSet<>(); for (Method method : clazz.getDeclaredMethods()) { ClientFieldChangeListener listener = method.getAnnotation(ClientFieldChangeListener.class); @@ -78,9 +85,20 @@ private ClassSyncData(Class clazz) { SyncedData.LOGGER.error(e.getMessage()); continue; } + if (listener.fieldName().isBlank()) { + throw new IllegalArgumentException("@ClientFieldChangeListener requires a non-blank fieldName: %s.%s" + .formatted(clazz.getName(), method.getName())); + } changeListeners.computeIfAbsent(listener.fieldName(), $ -> new ArrayList<>()).add(handle); + clientListenerTargets.add(listener.fieldName()); } + Map localFieldsByName = new HashMap<>(); + Set localSaveKeys = new HashSet<>(); + Set localItemKeys = new HashSet<>(); + Set localClientSyncKeys = new HashSet<>(); + Set localServerSyncKeys = new HashSet<>(); + for (Field field : clazz.getDeclaredFields()) { boolean hasSave = field.isAnnotationPresent(SaveField.class); boolean hasItem = field.isAnnotationPresent(ItemSave.class); @@ -106,16 +124,36 @@ private ClassSyncData(Class clazz) { FieldSyncData syncData = new FieldSyncData(field, handle, changeListeners.getOrDefault(field.getName(), List.of())); + if (localFieldsByName.put(syncData.fieldName, syncData) != null) { + throw new IllegalArgumentException("Duplicate managed field name in %s: %s" + .formatted(clazz.getName(), syncData.fieldName)); + } managedFields.add(syncData); - if (hasSave) worldSaveFields.add(syncData); - if (hasItem) itemSaveFields.add(syncData); - if (hasS2C) clientSyncFields.add(syncData); - if (hasC2S) serverSyncFields.add(syncData); + if (hasSave) { + checkDuplicateKey(localSaveKeys, syncData.nbtSaveKey, clazz, "save"); + worldSaveFields.add(syncData); + } + if (hasItem) { + checkDuplicateKey(localItemKeys, syncData.itemNbtKey, clazz, "item"); + itemSaveFields.add(syncData); + } + if (hasS2C) { + checkDuplicateKey(localClientSyncKeys, syncData.nbtSaveKey, clazz, "client sync"); + clientSyncFields.add(syncData); + } + if (hasC2S) { + checkDuplicateKey(localServerSyncKeys, syncData.fieldName, clazz, "server sync"); + serverSyncFields.add(syncData); + serverUpdateFields.add(syncData); + } if (hasBoth) { + checkDuplicateKey(localClientSyncKeys, syncData.nbtSaveKey, clazz, "client sync"); + checkDuplicateKey(localServerSyncKeys, syncData.fieldName, clazz, "server sync"); bothSyncFields.add(syncData); clientSyncFields.add(syncData); serverSyncFields.add(syncData); + serverUpdateFields.add(syncData); } } @@ -128,6 +166,33 @@ private ClassSyncData(Class clazz) { clientSyncFields.addAll(parentData.clientSyncFields); serverSyncFields.addAll(parentData.serverSyncFields); bothSyncFields.addAll(parentData.bothSyncFields); + serverUpdateFields.addAll(parentData.serverUpdateFields); + } + + for (String fieldName : clientListenerTargets) { + FieldSyncData localField = localFieldsByName.get(fieldName); + if (localField != null && !localField.hasSyncToClient && !localField.hasSyncBoth) { + throw new IllegalArgumentException("@ClientFieldChangeListener targets a field that never syncs to client: %s.%s" + .formatted(clazz.getName(), fieldName)); + } + if (localField == null && clientSyncFields.stream().noneMatch(field -> field.fieldName.equals(fieldName))) { + throw new IllegalArgumentException("@ClientFieldChangeListener targets unknown field: %s.%s" + .formatted(clazz.getName(), fieldName)); + } + } + + orderedClientSyncFields = clientSyncFields.stream() + .sorted(Comparator.comparing(field -> field.nbtSaveKey)) + .toArray(FieldSyncData[]::new); + orderedServerUpdateFields = serverUpdateFields.stream() + .sorted(Comparator.comparing(field -> field.fieldName)) + .toArray(FieldSyncData[]::new); + } + + private static void checkDuplicateKey(Set keys, String key, Class owner, String kind) { + if (!keys.add(key)) { + throw new IllegalArgumentException("Duplicate %s key in %s: %s" + .formatted(kind, owner.getName(), key)); } } } diff --git a/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java b/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java index 8e9bad1..d696562 100644 --- a/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java +++ b/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java @@ -77,6 +77,30 @@ public static void noChange(GameTestHelper helper) { helper.succeed(); } + @TestHolder(value = "sync_invalid_listener") + @EmptyTemplate("3") + @GameTest + public static void invalidListener(GameTestHelper helper) { + try { + ClassSyncData.getClassData(SyncTestFixtures.InvalidListenerBlockEntity.class); + helper.fail("invalid listener target should throw"); + } catch (IllegalArgumentException expected) { + helper.succeed(); + } + } + + @TestHolder(value = "sync_duplicate_client_key") + @EmptyTemplate("3") + @GameTest + public static void duplicateClientKey(GameTestHelper helper) { + try { + ClassSyncData.getClassData(SyncTestFixtures.DuplicateClientKeyBlockEntity.class); + helper.fail("duplicate client key should throw"); + } catch (IllegalArgumentException expected) { + helper.succeed(); + } + } + @TestHolder(value = "sync_item") @EmptyTemplate("3") @GameTest @@ -91,4 +115,5 @@ public static void item(GameTestHelper helper) { check(helper, restored.e == 500, "item rt"); helper.succeed(); } + } diff --git a/src/test/java/com/modularmc/synceddata/test/SyncTestFixtures.java b/src/test/java/com/modularmc/synceddata/test/SyncTestFixtures.java index bed09e0..85270db 100644 --- a/src/test/java/com/modularmc/synceddata/test/SyncTestFixtures.java +++ b/src/test/java/com/modularmc/synceddata/test/SyncTestFixtures.java @@ -196,4 +196,22 @@ public void scheduleRenderUpdate() {} @Override public void markAsChanged() {} } + + static class InvalidListenerBlockEntity extends MockSyncManaged { + + @SaveField + int energy; + + @ClientFieldChangeListener(fieldName = "energy") + void onEnergy() {} + } + + static class DuplicateClientKeyBlockEntity extends MockSyncManaged { + + @SyncToClient + int first; + @SyncToClient + @SaveField(nbtKey = "first") + int second; + } } From 1ad3e1b22cccc630a99fb37d2bc9a855447cdf08 Mon Sep 17 00:00:00 2001 From: qiuye2024github Date: Thu, 30 Apr 2026 15:10:00 +0800 Subject: [PATCH 2/3] feat(sync): add lightweight explicit block entity sync --- .../com/modularmc/synceddata/SyncedData.java | 13 ++ .../sync_system/ManagedSyncBlockEntity.java | 26 ++- .../sync_system/holder/SyncDataHolder.java | 168 ++++++++++++++++-- .../network/ClientBlockEntitySyncPayload.java | 48 +++++ .../network/ServerBlockEntitySyncPayload.java | 53 ++++++ .../synceddata/test/SyncAnnotationTest.java | 45 +++++ 6 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/modularmc/synceddata/api/sync_system/network/ClientBlockEntitySyncPayload.java create mode 100644 src/main/java/com/modularmc/synceddata/api/sync_system/network/ServerBlockEntitySyncPayload.java diff --git a/src/main/java/com/modularmc/synceddata/SyncedData.java b/src/main/java/com/modularmc/synceddata/SyncedData.java index fee5cec..a74b4c2 100644 --- a/src/main/java/com/modularmc/synceddata/SyncedData.java +++ b/src/main/java/com/modularmc/synceddata/SyncedData.java @@ -1,6 +1,8 @@ package com.modularmc.synceddata; import com.modularmc.synceddata.api.sync_system.SyncedComponents; +import com.modularmc.synceddata.api.sync_system.network.ClientBlockEntitySyncPayload; +import com.modularmc.synceddata.api.sync_system.network.ServerBlockEntitySyncPayload; import com.modularmc.synceddata.utils.FormattingUtil; import net.minecraft.client.Minecraft; @@ -12,6 +14,8 @@ import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.neoforge.data.loading.DatagenModLoader; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; import net.neoforged.neoforge.server.ServerLifecycleHooks; import com.mojang.serialization.Codec; @@ -34,6 +38,15 @@ public class SyncedData { public SyncedData(IEventBus bus) { SyncedComponents.COMPONENTS.register(bus); + bus.addListener(SyncedData::registerPayloadHandlers); + } + + private static void registerPayloadHandlers(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(MOD_ID); + registrar.playToClient(ServerBlockEntitySyncPayload.TYPE, ServerBlockEntitySyncPayload.CODEC, + ServerBlockEntitySyncPayload::execute); + registrar.playToServer(ClientBlockEntitySyncPayload.TYPE, ClientBlockEntitySyncPayload.CODEC, + ClientBlockEntitySyncPayload::execute); } public static Identifier id(String path) { diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java b/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java index cc27fd2..d7410af 100644 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java @@ -2,6 +2,8 @@ import com.modularmc.synceddata.api.blockentity.BlockEntityCreationInfo; import com.modularmc.synceddata.api.sync_system.holder.SyncDataHolder; +import com.modularmc.synceddata.api.sync_system.network.ClientBlockEntitySyncPayload; +import com.modularmc.synceddata.api.sync_system.network.ServerBlockEntitySyncPayload; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; @@ -13,12 +15,15 @@ import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; import com.mojang.serialization.MapCodec; import lombok.Getter; @@ -105,13 +110,26 @@ public final void updateTick() { setChanged(); if (getLevel() instanceof ServerLevel serverLevel) { if (syncDataHolder.scanAndMarkChanges(serverLevel.registryAccess())) { - serverLevel.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), - Block.UPDATE_CLIENTS); + byte[] data = syncDataHolder.collectClientNetworkChanges(serverLevel.registryAccess(), false); + if (data.length > 0) { + PacketDistributor.sendToPlayersTrackingChunk(serverLevel, + new ChunkPos(getBlockPos().getX() >> 4, getBlockPos().getZ() >> 4), + ServerBlockEntitySyncPayload.of(this, data)); + } } } } - public final void handleClientUpdate(HolderLookup.Provider registries, CompoundTag tag) { - syncDataHolder.applyServerUpdate(registries, tag); + public final void handleClientUpdate(net.minecraft.core.RegistryAccess registries, byte[] data) { + syncDataHolder.applyServerNetworkUpdate(registries, data); + } + + public final void pushClientChangesToServer() { + if (getLevel() instanceof ClientLevel clientLevel) { + byte[] changes = syncDataHolder.collectServerNetworkChanges(clientLevel.registryAccess()); + if (changes.length > 0) { + ClientPacketDistributor.sendToServer(new ClientBlockEntitySyncPayload(getBlockPos().asLong(), changes)); + } + } } } diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java index f369996..7d2ae68 100644 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java @@ -7,13 +7,18 @@ import com.modularmc.synceddata.api.sync_system.meta.FieldSyncData; import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.item.ItemStack; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; import java.util.*; @@ -22,7 +27,7 @@ public class SyncDataHolder { private final ClassSyncData syncData; private final ISyncManaged holder; - private final Map cachedValues = new HashMap<>(); + private final Map cachedValues = new Reference2ReferenceOpenHashMap<>(); private CompoundTag pendingClientChanges = null; private boolean fullSyncPending = true; @@ -30,7 +35,7 @@ public SyncDataHolder(ISyncManaged o) { holder = o; syncData = ClassSyncData.getClassData(o.getClass()); for (FieldSyncData field : syncData.getClientSyncFields()) { - cachedValues.put(field.fieldName, field.handle.get(holder)); + cachedValues.put(field, field.handle.get(holder)); } } @@ -44,12 +49,12 @@ public boolean scanAndMarkChanges(HolderLookup.Provider registries) { for (FieldSyncData field : syncData.getClientSyncFields()) { Object currentValue = field.handle.get(holder); - Object previousValue = cachedValues.get(field.fieldName); + Object previousValue = cachedValues.get(field); boolean changed = fullSyncPending || !Objects.equals(currentValue, previousValue); if (changed) { Tag serialized = encodeField(field, currentValue, registries); changes.put(field.nbtSaveKey, serialized); - cachedValues.put(field.fieldName, currentValue); + cachedValues.put(field, currentValue); hasChanges = true; } } @@ -67,6 +72,84 @@ public CompoundTag getPendingChanges() { return changes != null ? changes : new CompoundTag(); } + public byte[] collectClientNetworkChanges(RegistryAccess registries, boolean force) { + if (force) { + CompoundTag forcedChanges = new CompoundTag(); + for (FieldSyncData field : syncData.getClientSyncFields()) { + Object currentValue = field.handle.get(holder); + forcedChanges.put(field.nbtSaveKey, encodeField(field, currentValue, registries)); + cachedValues.put(field, currentValue); + } + pendingClientChanges = forcedChanges; + fullSyncPending = false; + } + + CompoundTag pendingChanges = getPendingChanges(); + if (pendingChanges.isEmpty()) { + return new byte[0]; + } + + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), registries); + try { + FieldSyncData[] fields = syncData.getOrderedClientSyncFields(); + for (int i = 0; i < fields.length; i++) { + FieldSyncData field = fields[i]; + Tag value = pendingChanges.get(field.nbtSaveKey); + if (value == null) { + continue; + } + buf.writeVarInt(i); + buf.writeNbt(value); + } + byte[] data = new byte[buf.readableBytes()]; + buf.getBytes(0, data); + return data; + } finally { + buf.release(); + } + } + + public CompoundTag collectServerChanges(HolderLookup.Provider registries) { + CompoundTag changes = new CompoundTag(); + for (FieldSyncData field : syncData.getServerUpdateFields()) { + Object currentValue = field.handle.get(holder); + Object previousValue = cachedValues.get(field); + if (!Objects.equals(currentValue, previousValue)) { + changes.put(field.fieldName, encodeField(field, currentValue, registries)); + cachedValues.put(field, currentValue); + } + } + return changes; + } + + public byte[] collectServerNetworkChanges(RegistryAccess registries) { + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), registries); + try { + boolean wroteAny = false; + FieldSyncData[] fields = syncData.getOrderedServerUpdateFields(); + for (int i = 0; i < fields.length; i++) { + FieldSyncData field = fields[i]; + Object currentValue = field.handle.get(holder); + Object previousValue = cachedValues.get(field); + if (Objects.equals(currentValue, previousValue)) { + continue; + } + buf.writeVarInt(i); + buf.writeNbt(encodeField(field, currentValue, registries)); + cachedValues.put(field, currentValue); + wroteAny = true; + } + if (!wroteAny) { + return new byte[0]; + } + byte[] data = new byte[buf.readableBytes()]; + buf.getBytes(0, data); + return data; + } finally { + buf.release(); + } + } + public CompoundTag serializeToSaveNBT(HolderLookup.Provider registries) { CompoundTag tag = new CompoundTag(); for (var field : syncData.getWorldSaveFields()) { @@ -93,7 +176,7 @@ public CompoundTag serializeFullClientSyncNBT(HolderLookup.Provider registries) Object value = field.handle.get(holder); Tag serialized = encodeField(field, value, registries); tag.put(field.nbtSaveKey, serialized); - cachedValues.put(field.fieldName, value); + cachedValues.put(field, value); } fullSyncPending = false; return tag; @@ -113,7 +196,7 @@ public void deserializeNBT(HolderLookup.Provider registries, CompoundTag tag, bo } if (readingClientFields) { - cachedValues.put(field.fieldName, field.handle.get(holder)); + cachedValues.put(field, field.handle.get(holder)); for (var listener : field.changeListenerHandles) { try { listener.invoke(holder); @@ -140,11 +223,7 @@ public void deserializeItemNBT(HolderLookup.Provider registries, CompoundTag tag } public void applyServerUpdate(HolderLookup.Provider registries, CompoundTag tag) { - Set targetFields = new HashSet<>(); - targetFields.addAll(syncData.getServerSyncFields()); - targetFields.addAll(syncData.getBothSyncFields()); - - for (var field : targetFields) { + for (var field : syncData.getServerUpdateFields()) { Tag value = tag.get(field.fieldName); if (value != null) { Object decoded = decodeField(field, value, field.handle.get(holder), registries); @@ -155,6 +234,70 @@ public void applyServerUpdate(HolderLookup.Provider registries, CompoundTag tag) } } + public void applyServerNetworkUpdate(RegistryAccess registries, byte[] data) { + if (data.length == 0) { + return; + } + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(data), registries); + try { + FieldSyncData[] fields = syncData.getOrderedServerUpdateFields(); + while (buf.isReadable()) { + int index = buf.readVarInt(); + if (index < 0 || index >= fields.length) { + throw new IllegalArgumentException("Invalid server sync field index: " + index); + } + FieldSyncData field = fields[index]; + Tag value = buf.readNbt(NbtAccounter.unlimitedHeap()); + if (value != null) { + Object decoded = decodeField(field, value, field.handle.get(holder), registries); + if (decoded != null) { + field.handle.set(holder, decoded); + } + } + } + } finally { + buf.release(); + } + } + + public void applyClientNetworkUpdate(RegistryAccess registries, byte[] data) { + if (data.length == 0) { + return; + } + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(data), registries); + try { + FieldSyncData[] fields = syncData.getOrderedClientSyncFields(); + while (buf.isReadable()) { + int index = buf.readVarInt(); + if (index < 0 || index >= fields.length) { + throw new IllegalArgumentException("Invalid client sync field index: " + index); + } + FieldSyncData field = fields[index]; + Tag value = buf.readNbt(NbtAccounter.unlimitedHeap()); + if (value != null) { + Object decoded = decodeField(field, value, field.handle.get(holder), registries); + if (decoded != null) { + field.handle.set(holder, decoded); + } + cachedValues.put(field, field.handle.get(holder)); + for (var listener : field.changeListenerHandles) { + try { + listener.invoke(holder); + } catch (Throwable e) { + SyncedData.LOGGER.error("Sync: Error invoking change listener for field {}", field.fieldName); + SyncedData.LOGGER.error(e); + } + } + if (field.triggerClientRerender) { + holder.scheduleRenderUpdate(); + } + } + } + } finally { + buf.release(); + } + } + public void applyToItemStack(ItemStack stack, HolderLookup.Provider registries) { CompoundTag data = serializeToItemNBT(registries); if (!data.isEmpty()) { @@ -176,7 +319,6 @@ private Tag encodeField(FieldSyncData field, Object value, HolderLookup.Provider return nullTag; } if (field.codec != null) { - @SuppressWarnings("unchecked") Codec codec = field.codec; DataResult result = codec.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), value); return result.getOrThrow(); @@ -196,7 +338,6 @@ private Object decodeField(FieldSyncData field, Tag tag, Object currentValue, Ho return currentValue; } if (field.codec != null) { - @SuppressWarnings("unchecked") Codec codec = field.codec; DataResult result = codec.parse(registries.createSerializationContext(NbtOps.INSTANCE), tag); return result.getOrThrow(); @@ -210,4 +351,5 @@ private Object decodeField(FieldSyncData field, Tag tag, Object currentValue, Ho SyncedData.LOGGER.error("Sync: No codec for field {} in {}", field.fieldName, holder.getClass()); return currentValue; } + } diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/network/ClientBlockEntitySyncPayload.java b/src/main/java/com/modularmc/synceddata/api/sync_system/network/ClientBlockEntitySyncPayload.java new file mode 100644 index 0000000..28f85d8 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/network/ClientBlockEntitySyncPayload.java @@ -0,0 +1,48 @@ +package com.modularmc.synceddata.api.sync_system.network; + +import com.modularmc.synceddata.SyncedData; +import com.modularmc.synceddata.api.sync_system.ManagedSyncBlockEntity; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import org.jspecify.annotations.NonNull; + +public record ClientBlockEntitySyncPayload(long pos, byte[] data) implements CustomPacketPayload { + + public static final Identifier ID = SyncedData.id("client_be_sync"); + public static final Type TYPE = new Type<>(ID); + public static final StreamCodec CODEC = StreamCodec.ofMember( + ClientBlockEntitySyncPayload::write, + ClientBlockEntitySyncPayload::decode); + + @Override + public @NonNull Type type() { + return TYPE; + } + + private void write(RegistryFriendlyByteBuf buf) { + buf.writeLong(pos); + buf.writeByteArray(data); + } + + private static ClientBlockEntitySyncPayload decode(RegistryFriendlyByteBuf buf) { + return new ClientBlockEntitySyncPayload(buf.readLong(), buf.readByteArray()); + } + + public static void execute(ClientBlockEntitySyncPayload packet, IPayloadContext context) { + if (!(context.player() instanceof ServerPlayer serverPlayer)) { + return; + } + if (!(serverPlayer.level().getBlockEntity(BlockPos.of(packet.pos)) instanceof ManagedSyncBlockEntity blockEntity)) { + return; + } + blockEntity.handleClientUpdate(serverPlayer.level().registryAccess(), packet.data); + blockEntity.markAsChanged(); + } +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/network/ServerBlockEntitySyncPayload.java b/src/main/java/com/modularmc/synceddata/api/sync_system/network/ServerBlockEntitySyncPayload.java new file mode 100644 index 0000000..4357ae2 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/network/ServerBlockEntitySyncPayload.java @@ -0,0 +1,53 @@ +package com.modularmc.synceddata.api.sync_system.network; + +import com.modularmc.synceddata.SyncedData; +import com.modularmc.synceddata.api.sync_system.ManagedSyncBlockEntity; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import org.jspecify.annotations.NonNull; + +public record ServerBlockEntitySyncPayload(long pos, byte[] data) implements CustomPacketPayload { + + public static final Identifier ID = SyncedData.id("server_be_sync"); + public static final Type TYPE = new Type<>(ID); + public static final StreamCodec CODEC = StreamCodec.ofMember( + ServerBlockEntitySyncPayload::write, + ServerBlockEntitySyncPayload::decode); + + @Override + public @NonNull Type type() { + return TYPE; + } + + private void write(RegistryFriendlyByteBuf buf) { + buf.writeLong(pos); + buf.writeByteArray(data); + } + + private static ServerBlockEntitySyncPayload decode(RegistryFriendlyByteBuf buf) { + return new ServerBlockEntitySyncPayload(buf.readLong(), buf.readByteArray()); + } + + public static void execute(ServerBlockEntitySyncPayload packet, IPayloadContext context) { + var level = Minecraft.getInstance().level; + if (level == null) { + return; + } + if (!(level.getBlockEntity(BlockPos.of(packet.pos)) instanceof ManagedSyncBlockEntity blockEntity)) { + return; + } + blockEntity.getSyncDataHolder().applyClientNetworkUpdate(level.registryAccess(), packet.data); + } + + public static ServerBlockEntitySyncPayload of(BlockEntity blockEntity, byte[] data) { + return new ServerBlockEntitySyncPayload(blockEntity.getBlockPos().asLong(), data); + } +} diff --git a/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java b/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java index d696562..8fbe964 100644 --- a/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java +++ b/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java @@ -77,6 +77,51 @@ public static void noChange(GameTestHelper helper) { helper.succeed(); } + @TestHolder(value = "sync_server_update") + @EmptyTemplate("3") + @GameTest + public static void serverUpdate(GameTestHelper helper) { + var registries = helper.getLevel().registryAccess(); + var blockEntity = new SyncTestFixtures.TestBlockEntity(); + blockEntity.target = 77; + blockEntity.block = net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft", "stone"); + blockEntity.active = true; + + byte[] changes = blockEntity.getSyncDataHolder().collectServerNetworkChanges(registries); + check(helper, changes.length > 0, "server network change missing"); + + var restored = new SyncTestFixtures.TestBlockEntity(); + restored.getSyncDataHolder().applyServerNetworkUpdate(registries, changes); + check(helper, restored.target == 77, "target not applied"); + check(helper, net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft", "stone").equals(restored.block), + "block not applied"); + check(helper, !restored.active, "client field should stay untouched"); + helper.succeed(); + } + + @TestHolder(value = "sync_client_network_update") + @EmptyTemplate("3") + @GameTest + public static void clientNetworkUpdate(GameTestHelper helper) { + var registries = helper.getLevel().registryAccess(); + var blockEntity = new SyncTestFixtures.TestBlockEntity(); + blockEntity.active = true; + blockEntity.lastTime = 99L; + blockEntity.target = 12; + + check(helper, blockEntity.getSyncDataHolder().scanAndMarkChanges(registries), "scan should detect client changes"); + byte[] changes = blockEntity.getSyncDataHolder().collectClientNetworkChanges(registries, false); + check(helper, changes.length > 0, "client network change missing"); + + var restored = new SyncTestFixtures.TestBlockEntity(); + restored.getSyncDataHolder().applyClientNetworkUpdate(registries, changes); + check(helper, restored.active, "active not applied"); + check(helper, restored.lastTime == 99L, "lastTime not applied"); + check(helper, restored.target == 12, "target not applied"); + check(helper, restored.callbacks.size() == 3, "listeners not invoked"); + helper.succeed(); + } + @TestHolder(value = "sync_invalid_listener") @EmptyTemplate("3") @GameTest From 64584cd8864360bb32ed3fc7254e122965cf72a7 Mon Sep 17 00:00:00 2001 From: qiuye2024github Date: Thu, 30 Apr 2026 15:26:36 +0800 Subject: [PATCH 3/3] spotless --- .../synceddata/api/sync_system/ManagedSyncBlockEntity.java | 1 - .../synceddata/api/sync_system/holder/SyncDataHolder.java | 1 - .../java/com/modularmc/synceddata/test/SyncAnnotationTest.java | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java b/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java index d7410af..f258732 100644 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/ManagedSyncBlockEntity.java @@ -16,7 +16,6 @@ import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java index 7d2ae68..1c75d31 100644 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java @@ -351,5 +351,4 @@ private Object decodeField(FieldSyncData field, Tag tag, Object currentValue, Ho SyncedData.LOGGER.error("Sync: No codec for field {} in {}", field.fieldName, holder.getClass()); return currentValue; } - } diff --git a/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java b/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java index 8fbe964..ea8e318 100644 --- a/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java +++ b/src/test/java/com/modularmc/synceddata/test/SyncAnnotationTest.java @@ -160,5 +160,4 @@ public static void item(GameTestHelper helper) { check(helper, restored.e == 500, "item rt"); helper.succeed(); } - }