diff --git a/src/main/java/com/modularmc/synceddata/SyncedData.java b/src/main/java/com/modularmc/synceddata/SyncedData.java index 8bd36d0..fee5cec 100644 --- a/src/main/java/com/modularmc/synceddata/SyncedData.java +++ b/src/main/java/com/modularmc/synceddata/SyncedData.java @@ -1,10 +1,12 @@ package com.modularmc.synceddata; +import com.modularmc.synceddata.api.sync_system.SyncedComponents; import com.modularmc.synceddata.utils.FormattingUtil; import net.minecraft.client.Minecraft; import net.minecraft.resources.Identifier; import net.minecraft.server.MinecraftServer; +import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModList; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLEnvironment; @@ -30,6 +32,10 @@ public class SyncedData { public static final Path SYNCED_FOLDER = getGameDir().resolve("synced"); private static final Identifier TEMPLATE_LOCATION = Identifier.fromNamespaceAndPath(MOD_ID, ""); + public SyncedData(IEventBus bus) { + SyncedComponents.COMPONENTS.register(bus); + } + public static Identifier id(String path) { if (path.isBlank()) { return TEMPLATE_LOCATION; diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/ISyncManaged.java b/src/main/java/com/modularmc/synceddata/api/sync_system/ISyncManaged.java index 8937124..282e441 100644 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/ISyncManaged.java +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/ISyncManaged.java @@ -1,16 +1,12 @@ package com.modularmc.synceddata.api.sync_system; +import com.modularmc.synceddata.api.sync_system.holder.SyncDataHolder; + public interface ISyncManaged { SyncDataHolder getSyncDataHolder(); - /** - * Function called when a synced field requests a rerender - */ void scheduleRenderUpdate(); - /** - * Function called to notify the server that this object has been updated and must be synced to clients - */ void markAsChanged(); } 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 255865e..cc27fd2 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 @@ -1,15 +1,28 @@ package com.modularmc.synceddata.api.sync_system; import com.modularmc.synceddata.api.blockentity.BlockEntityCreationInfo; +import com.modularmc.synceddata.api.sync_system.holder.SyncDataHolder; +import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.component.DataComponentGetter; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.Packet; +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.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 com.mojang.serialization.MapCodec; import lombok.Getter; -import lombok.Setter; +import org.jetbrains.annotations.Nullable; import java.util.Objects; @@ -17,9 +30,6 @@ public abstract class ManagedSyncBlockEntity extends BlockEntity implements ISyn @Getter protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); - @Getter - @Setter - private boolean isDirty; public ManagedSyncBlockEntity(BlockEntityCreationInfo info) { super(info.type(), info.pos(), info.state()); @@ -30,16 +40,78 @@ public ManagedSyncBlockEntity(BlockEntityType type, BlockPos pos, BlockState } @Override - public final void markAsChanged() { - isDirty = true; + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + var registries = Objects.requireNonNull(getLevel()).registryAccess(); + CompoundTag tag = getSyncDataHolder().serializeToSaveNBT(registries) + .merge(getSyncDataHolder().serializeToItemNBT(registries)); + if (!tag.isEmpty()) { + output.store("synced", CompoundTag.CODEC, tag); + } + } + + @Override + public void loadAdditional(ValueInput input) { + super.loadAdditional(input); + if (getLevel() == null) return; + var registries = getLevel().registryAccess(); + boolean clientSide = getLevel() instanceof ClientLevel; + input.read(MapCodec.assumeMapUnsafe(CompoundTag.CODEC)).ifPresent(fullTag -> { + var synced = fullTag.getCompound("synced").orElse(new CompoundTag()); + if (!synced.isEmpty()) { + getSyncDataHolder().deserializeNBT(registries, synced, clientSide); + if (!clientSide) { + getSyncDataHolder().deserializeItemNBT(registries, synced); + } + } + }); + } + + @Override + protected void collectImplicitComponents(DataComponentMap.Builder components) { + super.collectImplicitComponents(components); + var registries = Objects.requireNonNull(getLevel()).registryAccess(); + components.set(SyncedComponents.BLOCK_ITEM_DATA.get(), + getSyncDataHolder().serializeToItemNBT(registries)); + } + + @Override + protected void applyImplicitComponents(DataComponentGetter components) { + super.applyImplicitComponents(components); + var data = components.get(SyncedComponents.BLOCK_ITEM_DATA.get()); + if (data != null && getLevel() != null) { + getSyncDataHolder().deserializeItemNBT(getLevel().registryAccess(), data); + } + } + + @Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + getSyncDataHolder().resyncAllFields(); + return getSyncDataHolder().serializeFullClientSyncNBT(registries); + } + + @Override + public @Nullable Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this, + (be, r) -> ((ManagedSyncBlockEntity) be).syncDataHolder.getPendingChanges()); + } + + @Override + public void markAsChanged() { + setChanged(); } public final void updateTick() { setChanged(); - if (isDirty) { - Objects.requireNonNull(getLevel()).sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), - Block.UPDATE_CLIENTS); - isDirty = false; + if (getLevel() instanceof ServerLevel serverLevel) { + if (syncDataHolder.scanAndMarkChanges(serverLevel.registryAccess())) { + serverLevel.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), + Block.UPDATE_CLIENTS); + } } } + + public final void handleClientUpdate(HolderLookup.Provider registries, CompoundTag tag) { + syncDataHolder.applyServerUpdate(registries, tag); + } } diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/SyncDataHolder.java b/src/main/java/com/modularmc/synceddata/api/sync_system/SyncDataHolder.java deleted file mode 100644 index 643df44..0000000 --- a/src/main/java/com/modularmc/synceddata/api/sync_system/SyncDataHolder.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.modularmc.synceddata.api.sync_system; - -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import it.unimi.dsi.fastutil.objects.ObjectSet; - -public class SyncDataHolder { - - private final ISyncManaged holder; - - private final ObjectSet dirtySyncFields = new ObjectOpenHashSet<>(); - private boolean resyncAll = false; - - public SyncDataHolder(ISyncManaged o) { - holder = o; - } -} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/SyncedComponents.java b/src/main/java/com/modularmc/synceddata/api/sync_system/SyncedComponents.java new file mode 100644 index 0000000..b5f20e6 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/SyncedComponents.java @@ -0,0 +1,18 @@ +package com.modularmc.synceddata.api.sync_system; + +import com.modularmc.synceddata.SyncedData; + +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.neoforged.neoforge.registries.DeferredRegister; + +import java.util.function.Supplier; + +public class SyncedComponents { + + public static final DeferredRegister> COMPONENTS = DeferredRegister.create(Registries.DATA_COMPONENT_TYPE, SyncedData.MOD_ID); + + public static final Supplier> BLOCK_ITEM_DATA = COMPONENTS.register("block_item_data", + () -> DataComponentType.builder().persistent(CompoundTag.CODEC).build()); +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/ClientFieldChangeListener.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/ClientFieldChangeListener.java new file mode 100644 index 0000000..e741df2 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/ClientFieldChangeListener.java @@ -0,0 +1,13 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ClientFieldChangeListener { + + String fieldName(); +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/ItemSave.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/ItemSave.java new file mode 100644 index 0000000..2fcc8f4 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/ItemSave.java @@ -0,0 +1,13 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ItemSave { + + String nbtKey() default ""; +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/RerenderOnChanged.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/RerenderOnChanged.java new file mode 100644 index 0000000..6f32c19 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/RerenderOnChanged.java @@ -0,0 +1,10 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface RerenderOnChanged {} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SaveField.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SaveField.java new file mode 100644 index 0000000..713f9ec --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SaveField.java @@ -0,0 +1,13 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SaveField { + + String nbtKey() default ""; +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncBoth.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncBoth.java new file mode 100644 index 0000000..fa3a7c8 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncBoth.java @@ -0,0 +1,10 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SyncBoth {} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncToClient.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncToClient.java new file mode 100644 index 0000000..807b57f --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncToClient.java @@ -0,0 +1,10 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SyncToClient {} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncToServer.java b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncToServer.java new file mode 100644 index 0000000..a85a585 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/annotations/SyncToServer.java @@ -0,0 +1,10 @@ +package com.modularmc.synceddata.api.sync_system.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SyncToServer {} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/holder/ItemSyncHolder.java b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/ItemSyncHolder.java new file mode 100644 index 0000000..fd019a2 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/ItemSyncHolder.java @@ -0,0 +1,61 @@ +package com.modularmc.synceddata.api.sync_system.holder; + +import com.modularmc.synceddata.api.sync_system.ISyncManaged; +import com.modularmc.synceddata.api.sync_system.SyncedComponents; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; + +import lombok.Getter; + +/** + * Wraps an ItemStack with the sync annotation system. + * All annotated field data uses {@link SyncedComponents#BLOCK_ITEM_DATA} DataComponent. + */ +public final class ItemSyncHolder implements ISyncManaged { + + @Getter + private final SyncDataHolder syncDataHolder; + + public ItemSyncHolder(ISyncManaged owner) { + this.syncDataHolder = new SyncDataHolder(owner); + } + + public void saveToStack(ItemStack stack, HolderLookup.Provider registries) { + syncDataHolder.applyToItemStack(stack, registries); + } + + public void loadFromStack(ItemStack stack, HolderLookup.Provider registries, boolean clientSide) { + syncDataHolder.loadFromItemStack(stack, registries); + if (clientSide) { + var data = stack.get(SyncedComponents.BLOCK_ITEM_DATA.get()); + if (data != null) { + syncDataHolder.deserializeNBT(registries, data, true); + } + } + } + + public boolean scanChanges(HolderLookup.Provider registries) { + return syncDataHolder.scanAndMarkChanges(registries); + } + + public void flushToStack(ItemStack stack, HolderLookup.Provider registries) { + CompoundTag pending = syncDataHolder.getPendingChanges(); + if (!pending.isEmpty()) { + var existing = stack.get(SyncedComponents.BLOCK_ITEM_DATA.get()); + stack.set(SyncedComponents.BLOCK_ITEM_DATA.get(), + existing != null ? existing.merge(pending) : pending); + } + } + + public void applyServerUpdate(HolderLookup.Provider registries, CompoundTag tag) { + syncDataHolder.applyServerUpdate(registries, tag); + } + + @Override + public void scheduleRenderUpdate() {} + + @Override + public void markAsChanged() {} +} 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 new file mode 100644 index 0000000..f369996 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/holder/SyncDataHolder.java @@ -0,0 +1,213 @@ +package com.modularmc.synceddata.api.sync_system.holder; + +import com.modularmc.synceddata.SyncedData; +import com.modularmc.synceddata.api.sync_system.ISyncManaged; +import com.modularmc.synceddata.api.sync_system.SyncedComponents; +import com.modularmc.synceddata.api.sync_system.meta.ClassSyncData; +import com.modularmc.synceddata.api.sync_system.meta.FieldSyncData; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; + +import java.util.*; + +public class SyncDataHolder { + + private final ClassSyncData syncData; + private final ISyncManaged holder; + + private final Map cachedValues = new HashMap<>(); + private CompoundTag pendingClientChanges = null; + private boolean fullSyncPending = true; + + public SyncDataHolder(ISyncManaged o) { + holder = o; + syncData = ClassSyncData.getClassData(o.getClass()); + for (FieldSyncData field : syncData.getClientSyncFields()) { + cachedValues.put(field.fieldName, field.handle.get(holder)); + } + } + + public void resyncAllFields() { + fullSyncPending = true; + } + + public boolean scanAndMarkChanges(HolderLookup.Provider registries) { + CompoundTag changes = new CompoundTag(); + boolean hasChanges = fullSyncPending; + + for (FieldSyncData field : syncData.getClientSyncFields()) { + Object currentValue = field.handle.get(holder); + Object previousValue = cachedValues.get(field.fieldName); + 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); + hasChanges = true; + } + } + + if (hasChanges) { + pendingClientChanges = changes; + } + fullSyncPending = false; + return hasChanges; + } + + public CompoundTag getPendingChanges() { + CompoundTag changes = pendingClientChanges; + pendingClientChanges = null; + return changes != null ? changes : new CompoundTag(); + } + + public CompoundTag serializeToSaveNBT(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + for (var field : syncData.getWorldSaveFields()) { + Object value = field.handle.get(holder); + Tag serialized = encodeField(field, value, registries); + tag.put(field.nbtSaveKey, serialized); + } + return tag; + } + + public CompoundTag serializeToItemNBT(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + for (var field : syncData.getItemSaveFields()) { + Object value = field.handle.get(holder); + Tag serialized = encodeField(field, value, registries); + tag.put(field.itemNbtKey, serialized); + } + return tag; + } + + public CompoundTag serializeFullClientSyncNBT(HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + for (var field : syncData.getClientSyncFields()) { + Object value = field.handle.get(holder); + Tag serialized = encodeField(field, value, registries); + tag.put(field.nbtSaveKey, serialized); + cachedValues.put(field.fieldName, value); + } + fullSyncPending = false; + return tag; + } + + public void deserializeNBT(HolderLookup.Provider registries, CompoundTag tag, boolean readingClientFields) { + Set fields = readingClientFields ? syncData.getClientSyncFields() : + syncData.getWorldSaveFields(); + + for (var field : fields) { + Tag savedValue = tag.get(field.nbtSaveKey); + if (savedValue != null) { + Object decoded = decodeField(field, savedValue, field.handle.get(holder), registries); + if (decoded != null) { + field.handle.set(holder, decoded); + } + } + + if (readingClientFields) { + cachedValues.put(field.fieldName, 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(); + } + } + } + + public void deserializeItemNBT(HolderLookup.Provider registries, CompoundTag tag) { + for (var field : syncData.getItemSaveFields()) { + Tag savedValue = tag.get(field.itemNbtKey); + if (savedValue != null) { + Object decoded = decodeField(field, savedValue, field.handle.get(holder), registries); + if (decoded != null) { + field.handle.set(holder, decoded); + } + } + } + } + + public void applyServerUpdate(HolderLookup.Provider registries, CompoundTag tag) { + Set targetFields = new HashSet<>(); + targetFields.addAll(syncData.getServerSyncFields()); + targetFields.addAll(syncData.getBothSyncFields()); + + for (var field : targetFields) { + Tag value = tag.get(field.fieldName); + if (value != null) { + Object decoded = decodeField(field, value, field.handle.get(holder), registries); + if (decoded != null) { + field.handle.set(holder, decoded); + } + } + } + } + + public void applyToItemStack(ItemStack stack, HolderLookup.Provider registries) { + CompoundTag data = serializeToItemNBT(registries); + if (!data.isEmpty()) { + stack.set(SyncedComponents.BLOCK_ITEM_DATA.get(), data); + } + } + + public void loadFromItemStack(ItemStack stack, HolderLookup.Provider registries) { + CompoundTag data = stack.get(SyncedComponents.BLOCK_ITEM_DATA.get()); + if (data != null) { + deserializeItemNBT(registries, data); + } + } + + private Tag encodeField(FieldSyncData field, Object value, HolderLookup.Provider registries) { + if (value == null) { + var nullTag = new CompoundTag(); + nullTag.putBoolean("null", true); + return nullTag; + } + if (field.codec != null) { + @SuppressWarnings("unchecked") + Codec codec = field.codec; + DataResult result = codec.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), value); + return result.getOrThrow(); + } + if (field.isSyncManaged && value instanceof ISyncManaged syncObj) { + return syncObj.getSyncDataHolder().serializeToSaveNBT(registries); + } + SyncedData.LOGGER.error("Sync: No codec for field {} in {}", field.fieldName, holder.getClass()); + return new CompoundTag(); + } + + private Object decodeField(FieldSyncData field, Tag tag, Object currentValue, HolderLookup.Provider registries) { + if (tag instanceof CompoundTag compound && compound.getBoolean("null").orElse(false)) { + return null; + } + if (tag instanceof CompoundTag compound && compound.isEmpty()) { + return currentValue; + } + if (field.codec != null) { + @SuppressWarnings("unchecked") + Codec codec = field.codec; + DataResult result = codec.parse(registries.createSerializationContext(NbtOps.INSTANCE), tag); + return result.getOrThrow(); + } + if (field.isSyncManaged && tag instanceof CompoundTag compound) { + if (currentValue instanceof ISyncManaged syncObj) { + syncObj.getSyncDataHolder().deserializeNBT(registries, compound, false); + return currentValue; + } + } + 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/meta/ClassSyncData.java b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/ClassSyncData.java new file mode 100644 index 0000000..d6127c3 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/ClassSyncData.java @@ -0,0 +1,133 @@ +package com.modularmc.synceddata.api.sync_system.meta; + +import com.modularmc.synceddata.SyncedData; +import com.modularmc.synceddata.api.sync_system.annotations.ClientFieldChangeListener; +import com.modularmc.synceddata.api.sync_system.annotations.ItemSave; +import com.modularmc.synceddata.api.sync_system.annotations.SaveField; +import com.modularmc.synceddata.api.sync_system.annotations.SyncBoth; +import com.modularmc.synceddata.api.sync_system.annotations.SyncToClient; +import com.modularmc.synceddata.api.sync_system.annotations.SyncToServer; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import lombok.Getter; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +public final class ClassSyncData { + + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private static final ClassValue CACHE = new ClassValue<>() { + + @Override + protected ClassSyncData computeValue(Class type) { + return new ClassSyncData(type); + } + }; + + public static ClassSyncData getClassData(Class cls) { + return CACHE.get(cls); + } + + @Getter + private final List managedFields = new ObjectArrayList<>(); + + @Getter + private final Set worldSaveFields = new ObjectOpenHashSet<>(); + @Getter + private final Set itemSaveFields = new ObjectOpenHashSet<>(); + @Getter + private final Set clientSyncFields = new ObjectOpenHashSet<>(); + @Getter + private final Set serverSyncFields = new ObjectOpenHashSet<>(); + @Getter + private final Set bothSyncFields = new ObjectOpenHashSet<>(); + + private ClassSyncData(Class clazz) { + MethodHandles.Lookup privateLookup; + try { + privateLookup = MethodHandles.privateLookupIn(clazz, LOOKUP); + } catch (IllegalAccessException e) { + SyncedData.LOGGER.error("Sync: Failed to create method handle lookup for class {}", clazz); + SyncedData.LOGGER.error(e.getMessage()); + return; + } + + Map> changeListeners = new HashMap<>(); + + for (Method method : clazz.getDeclaredMethods()) { + ClientFieldChangeListener listener = method.getAnnotation(ClientFieldChangeListener.class); + if (listener == null) continue; + + if (Modifier.isStatic(method.getModifiers())) + throw new IllegalArgumentException("@ClientFieldChangeListener on static method: %s.%s" + .formatted(clazz.getName(), method.getName())); + + MethodHandle handle; + try { + handle = privateLookup.unreflect(method); + } catch (IllegalAccessException e) { + SyncedData.LOGGER.error("Sync: Failed to acquire method handle for method {} {}", + method.getName(), clazz.getName()); + SyncedData.LOGGER.error(e.getMessage()); + continue; + } + changeListeners.computeIfAbsent(listener.fieldName(), $ -> new ArrayList<>()).add(handle); + } + + for (Field field : clazz.getDeclaredFields()) { + boolean hasSave = field.isAnnotationPresent(SaveField.class); + boolean hasItem = field.isAnnotationPresent(ItemSave.class); + boolean hasS2C = field.isAnnotationPresent(SyncToClient.class); + boolean hasC2S = field.isAnnotationPresent(SyncToServer.class); + boolean hasBoth = field.isAnnotationPresent(SyncBoth.class); + + if (!hasSave && !hasItem && !hasS2C && !hasC2S && !hasBoth) continue; + + if (Modifier.isStatic(field.getModifiers())) + throw new IllegalArgumentException("Cannot apply sync annotations to static field: %s.%s" + .formatted(field.getDeclaringClass().getName(), field.getName())); + + VarHandle handle; + try { + handle = privateLookup.unreflectVarHandle(field); + } catch (IllegalAccessException e) { + SyncedData.LOGGER.error("Sync: Failed to acquire variable handle for field {} {}", + field.getName(), clazz.getName()); + SyncedData.LOGGER.error(e.getMessage()); + continue; + } + + FieldSyncData syncData = new FieldSyncData(field, handle, + changeListeners.getOrDefault(field.getName(), List.of())); + managedFields.add(syncData); + + if (hasSave) worldSaveFields.add(syncData); + if (hasItem) itemSaveFields.add(syncData); + if (hasS2C) clientSyncFields.add(syncData); + if (hasC2S) serverSyncFields.add(syncData); + if (hasBoth) { + bothSyncFields.add(syncData); + clientSyncFields.add(syncData); + serverSyncFields.add(syncData); + } + } + + Class parent = clazz.getSuperclass(); + if (parent != null && parent != Object.class) { + ClassSyncData parentData = CACHE.get(parent); + managedFields.addAll(parentData.managedFields); + worldSaveFields.addAll(parentData.worldSaveFields); + itemSaveFields.addAll(parentData.itemSaveFields); + clientSyncFields.addAll(parentData.clientSyncFields); + serverSyncFields.addAll(parentData.serverSyncFields); + bothSyncFields.addAll(parentData.bothSyncFields); + } + } +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/meta/FieldCodecs.java b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/FieldCodecs.java new file mode 100644 index 0000000..c1c6374 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/FieldCodecs.java @@ -0,0 +1,172 @@ +package com.modularmc.synceddata.api.sync_system.meta; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.fluids.FluidStack; + +import com.mojang.serialization.Codec; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.*; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +public class FieldCodecs { + + private static final Map> REGISTRY = new Reference2ReferenceOpenHashMap<>(); + private static final Map>> SUPPLIERS = new Reference2ReferenceOpenHashMap<>(); + + private static final Map PRIMITIVE_TO_BOXED = Map.of( + boolean.class, Boolean.class, + byte.class, Byte.class, + char.class, Character.class, + short.class, Short.class, + int.class, Integer.class, + long.class, Long.class, + float.class, Float.class, + double.class, Double.class, + void.class, Void.class); + + public static @Nullable Codec get(Type type) { + if (type instanceof Class cls && cls.isPrimitive()) type = PRIMITIVE_TO_BOXED.get(cls); + + Codec cached = REGISTRY.get(type); + if (cached != null) return cached; + + // 泛型类型直接解析 + if (type instanceof ParameterizedType pt) { + Class raw = (Class) pt.getRawType(); + if (List.class.isAssignableFrom(raw)) return makeListCodec(pt); + if (Set.class.isAssignableFrom(raw)) return makeSetCodec(pt); + if (Map.class.isAssignableFrom(raw)) return makeMapCodec(pt); + + for (var entry : SUPPLIERS.entrySet()) { + if (entry.getKey() instanceof Class sup && sup.isAssignableFrom(raw)) return entry.getValue().get(); + } + } + + var declaration = new TypeDeclaration(type); + Class clazz = declaration.getClassValue(); + if (clazz == null) return null; + + // 数组 + if (clazz.isArray()) return makeArrayCodec(clazz.getComponentType()); + + // 枚举 + if (clazz.isEnum()) return makeEnumCodec(clazz); + + // 接口/父类匹配 + for (var entry : SUPPLIERS.entrySet()) { + if (entry.getKey() instanceof Class sup && sup.isAssignableFrom(clazz)) return entry.getValue().get(); + } + + return null; + } + + public static void register(Type type, Codec codec) { + REGISTRY.putIfAbsent(type, codec); + } + + public static void registerSupplier(Class type, Supplier> supplier) { + SUPPLIERS.putIfAbsent(type, supplier); + } + + @SuppressWarnings("unchecked") + public static @Nullable Codec getTyped(Type type) { + return (Codec) get(type); + } + + // === 泛型集合构建 === + + @SuppressWarnings("unchecked") + private static @Nullable Codec makeListCodec(ParameterizedType pt) { + Codec elem = get(pt.getActualTypeArguments()[0]); + if (elem == null) return null; + return Codec.list((Codec) elem); + } + + @SuppressWarnings("unchecked") + private static @Nullable Codec makeSetCodec(ParameterizedType pt) { + Codec elem = get(pt.getActualTypeArguments()[0]); + if (elem == null) return null; + return Codec.list((Codec) elem).xmap(LinkedHashSet::new, ArrayList::new); + } + + @SuppressWarnings("unchecked") + private static @Nullable Codec makeMapCodec(ParameterizedType pt) { + Codec key = get(pt.getActualTypeArguments()[0]); + Codec val = get(pt.getActualTypeArguments()[1]); + if (key == null || val == null) return null; + return Codec.unboundedMap((Codec) key, (Codec) val); + } + + // === 数组 / 枚举 === + + @SuppressWarnings("unchecked") + private static @Nullable Codec makeArrayCodec(Class component) { + Codec elem = get(component); + if (elem == null) return null; + return Codec.list((Codec) elem).xmap( + l -> { + var arr = Array.newInstance(component, l.size()); + for (int i = 0; i < l.size(); i++) Array.set(arr, i, l.get(i)); + return arr; + }, + a -> { + var l = new ArrayList<>(); + for (int i = 0; i < Array.getLength(a); i++) l.add(Array.get(a, i)); + return l; + }); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Codec makeEnumCodec(Class clazz) { + return Codec.STRING.xmap( + s -> Enum.valueOf((Class) clazz, s), + e -> ((Enum) e).name()); + } + + // === 静态初始化 === + + static { + register(Integer.class, Codec.INT); + register(Long.class, Codec.LONG); + register(Float.class, Codec.FLOAT); + register(Double.class, Codec.DOUBLE); + register(Short.class, Codec.SHORT); + register(Byte.class, Codec.BYTE); + register(Boolean.class, Codec.BOOL); + register(Character.class, Codec.STRING.xmap(s -> s.charAt(0), String::valueOf)); + + register(String.class, Codec.STRING); + register(UUID.class, Codec.STRING.xmap(UUID::fromString, UUID::toString)); + register(CompoundTag.class, CompoundTag.CODEC); + + register(int[].class, Codec.INT_STREAM.xmap(IntStream::toArray, Arrays::stream)); + register(long[].class, Codec.LONG_STREAM.xmap(LongStream::toArray, Arrays::stream)); + register(byte[].class, Codec.list(Codec.BYTE).xmap( + l -> { + byte[] a = new byte[l.size()]; + for (int i = 0; i < l.size(); i++) a[i] = l.get(i); + return a; + }, + a -> { + var l = new ArrayList(); + for (byte b : a) l.add(b); + return l; + })); + + register(BlockPos.class, BlockPos.CODEC); + register(Identifier.class, Identifier.CODEC); + register(ItemStack.class, ItemStack.CODEC); + register(FluidStack.class, FluidStack.CODEC); + register(Component.class, ComponentSerialization.CODEC); + } +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/meta/FieldSyncData.java b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/FieldSyncData.java new file mode 100644 index 0000000..4493622 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/FieldSyncData.java @@ -0,0 +1,65 @@ +package com.modularmc.synceddata.api.sync_system.meta; + +import com.modularmc.synceddata.api.sync_system.ISyncManaged; +import com.modularmc.synceddata.api.sync_system.annotations.ItemSave; +import com.modularmc.synceddata.api.sync_system.annotations.RerenderOnChanged; +import com.modularmc.synceddata.api.sync_system.annotations.SaveField; +import com.modularmc.synceddata.api.sync_system.annotations.SyncBoth; +import com.modularmc.synceddata.api.sync_system.annotations.SyncToClient; +import com.modularmc.synceddata.api.sync_system.annotations.SyncToServer; + +import com.mojang.serialization.Codec; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.util.List; + +public final class FieldSyncData { + + public final String fieldName; + public final VarHandle handle; + public final boolean triggerClientRerender; + public final boolean isSyncManaged; + + public final boolean hasSaveField; + public final boolean hasItemSave; + public final boolean hasSyncToClient; + public final boolean hasSyncToServer; + public final boolean hasSyncBoth; + + public final String nbtSaveKey; + public final String itemNbtKey; + + @Getter + public final @Nullable Codec codec; + public final List changeListenerHandles; + public final TypeDeclaration type; + + @SuppressWarnings("unchecked") + public FieldSyncData(Field field, VarHandle handle, + List changeListenerHandles) { + this.fieldName = field.getName(); + this.isSyncManaged = ISyncManaged.class.isAssignableFrom(field.getType()); + this.handle = handle; + this.triggerClientRerender = field.isAnnotationPresent(RerenderOnChanged.class); + this.changeListenerHandles = changeListenerHandles; + this.type = new TypeDeclaration(field.getGenericType()); + + SaveField saveField = field.getAnnotation(SaveField.class); + ItemSave itemSave = field.getAnnotation(ItemSave.class); + this.hasSaveField = saveField != null; + this.hasItemSave = itemSave != null; + this.hasSyncToClient = field.isAnnotationPresent(SyncToClient.class); + this.hasSyncToServer = field.isAnnotationPresent(SyncToServer.class); + this.hasSyncBoth = field.isAnnotationPresent(SyncBoth.class); + + this.nbtSaveKey = (saveField != null && !saveField.nbtKey().isBlank()) ? saveField.nbtKey() : fieldName; + this.itemNbtKey = (itemSave != null && !itemSave.nbtKey().isBlank()) ? itemSave.nbtKey() : fieldName; + + Codec resolved = FieldCodecs.get(field.getGenericType()); + this.codec = (Codec) resolved; + } +} diff --git a/src/main/java/com/modularmc/synceddata/api/sync_system/meta/TypeDeclaration.java b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/TypeDeclaration.java new file mode 100644 index 0000000..0643977 --- /dev/null +++ b/src/main/java/com/modularmc/synceddata/api/sync_system/meta/TypeDeclaration.java @@ -0,0 +1,53 @@ +package com.modularmc.synceddata.api.sync_system.meta; + +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class TypeDeclaration { + + @Getter + private final Type rawType; + @Getter + private final @Nullable Class classValue; + @Getter + private final TypeDeclaration[] genericTypeArgs; + private final @Nullable TypeDeclaration arrayComponentType; + + public TypeDeclaration(Type type) { + this.rawType = type; + + if (type instanceof ParameterizedType parameterizedType) { + this.classValue = (Class) parameterizedType.getRawType(); + this.genericTypeArgs = java.util.Arrays.stream(parameterizedType.getActualTypeArguments()) + .map(TypeDeclaration::new).toArray(TypeDeclaration[]::new); + this.arrayComponentType = null; + } else if (type instanceof GenericArrayType genericArrayType) { + this.classValue = null; + this.arrayComponentType = new TypeDeclaration(genericArrayType.getGenericComponentType()); + this.genericTypeArgs = new TypeDeclaration[0]; + } else { + this.classValue = (Class) type; + this.genericTypeArgs = new TypeDeclaration[0]; + this.arrayComponentType = classValue.isArray() ? new TypeDeclaration(classValue.getComponentType()) : null; + } + } + + public boolean isArray() { + return (classValue != null && classValue.isArray()) || (rawType instanceof GenericArrayType); + } + + public TypeDeclaration getArrayComponentType() { + if (arrayComponentType == null) + throw new IllegalStateException("Not an array type: %s".formatted(rawType)); + return arrayComponentType; + } + + @Override + public String toString() { + return rawType.toString(); + } +}