diff --git a/src/main/java/org/embeddedt/modernfix/ModernFix.java b/src/main/java/org/embeddedt/modernfix/ModernFix.java index 27ed116c..bf73fc45 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFix.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFix.java @@ -16,6 +16,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.embeddedt.modernfix.core.config.ModernFixConfig; +import org.embeddedt.modernfix.entity.EntityDataIDSyncHandler; +import org.embeddedt.modernfix.packet.PacketHandler; import org.embeddedt.modernfix.structure.AsyncLocator; import org.embeddedt.modernfix.util.KubeUtil; @@ -70,6 +72,8 @@ public class ModernFix { ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ModernFixConfig.COMMON_CONFIG); if(ModList.get().isLoaded("kubejs")) MinecraftForge.EVENT_BUS.register(KubeUtil.class); + MinecraftForge.EVENT_BUS.register(EntityDataIDSyncHandler.class); + PacketHandler.register(); } private static boolean dfuModPresent() { diff --git a/src/main/java/org/embeddedt/modernfix/ModernFixClient.java b/src/main/java/org/embeddedt/modernfix/ModernFixClient.java index 90779e01..7b97709a 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFixClient.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFixClient.java @@ -1,8 +1,12 @@ package org.embeddedt.modernfix; +import com.mojang.datafixers.util.Pair; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.ConnectScreen; import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.entity.Entity; import net.minecraftforge.client.event.GuiOpenEvent; import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.common.MinecraftForge; @@ -11,12 +15,17 @@ import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.network.NetworkEvent; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.load.LoadEvents; +import org.embeddedt.modernfix.packet.EntityIDSyncPacket; import org.embeddedt.modernfix.screen.DeferredLevelLoadingScreen; import java.lang.management.ManagementFactory; -import java.util.Optional; +import java.lang.reflect.Field; +import java.sql.Ref; +import java.util.*; +import java.util.function.Supplier; public class ModernFixClient { public static long worldLoadStartTime; @@ -71,4 +80,78 @@ public class ModernFixClient { event.getLeft().add(brandingString); } } + + /** + * Check if the IDs match and remap them if not. + * @return true if ID remap was needed + */ + private static boolean compareAndSwitchIds(Class eClass, String fieldName, EntityDataAccessor accessor, int newId) { + if(accessor.id != newId) { + ModernFix.LOGGER.warn("Corrected ID mismatch on {} field {}. Client had {} but server wants {}.", + eClass, + fieldName, + accessor.id, + newId); + accessor.id = newId; + return true; + } else { + ModernFix.LOGGER.debug("{} {} ID fine: {}", eClass, fieldName, newId); + return false; + } + } + + /** + * Horrendous hack to allow tracking every synced entity data manager. + * + * This is to ensure we can perform ID fixup on already constructed managers. + */ + public static Set allEntityDatas = Collections.newSetFromMap(new WeakHashMap<>()); + + /** + * Extremely hacky method to detect and correct mismatched entity data parameter IDs on the client and server. + * + * The technique is far from ideal, but it should detect reliably and also not break already constructed entities. + */ + public static void handleEntityIDSync(EntityIDSyncPacket packet, Supplier context) { + Map, List>> info = packet.getFieldInfo(); + context.get().enqueueWork(() -> { + boolean fixNeeded = false; + for(Map.Entry, List>> entry : info.entrySet()) { + Class eClass = entry.getKey(); + for(Pair field : entry.getValue()) { + String fieldName = field.getFirst(); + int newId = field.getSecond(); + try { + Field f = eClass.getDeclaredField(fieldName); + f.setAccessible(true); + EntityDataAccessor accessor = (EntityDataAccessor)f.get(null); + if(compareAndSwitchIds(eClass, fieldName, accessor, newId)) + fixNeeded = true; + } catch(NoSuchFieldException e) { + ModernFix.LOGGER.warn("Couldn't find field on {}: {}", eClass, fieldName); + } catch(ReflectiveOperationException e) { + throw new RuntimeException("Unexpected exception", e); + } + } + } + /* Now the ID mappings on synced entity data instances are probably all wrong. Fix that. */ + List dataEntries; + synchronized (allEntityDatas) { + if(fixNeeded) { + dataEntries = new ArrayList<>(allEntityDatas); + for(SynchedEntityData manager : dataEntries) { + Map> fixedMap = new HashMap<>(); + List> items = new ArrayList<>(manager.itemsById.values()); + for(SynchedEntityData.DataItem item : items) { + fixedMap.put(item.getAccessor().id, item); + } + manager.itemsById = fixedMap; + } + } + allEntityDatas.clear(); + } + }); + + context.get().setPacketHandled(true); + } } diff --git a/src/main/java/org/embeddedt/modernfix/entity/EntityDataIDSyncHandler.java b/src/main/java/org/embeddedt/modernfix/entity/EntityDataIDSyncHandler.java new file mode 100644 index 00000000..e1124721 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/entity/EntityDataIDSyncHandler.java @@ -0,0 +1,66 @@ +package org.embeddedt.modernfix.entity; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.entity.Entity; +import net.minecraftforge.event.OnDatapackSyncEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraftforge.fml.network.PacketDistributor; +import net.minecraftforge.fml.server.ServerLifecycleHooks; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.packet.EntityIDSyncPacket; +import org.embeddedt.modernfix.packet.PacketHandler; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EntityDataIDSyncHandler { + private static Map, List>> fieldsToSyncMap; + @SubscribeEvent(priority = EventPriority.HIGHEST) + @SuppressWarnings("unchecked") + public static void onDatapackSyncEvent(OnDatapackSyncEvent event) { + if(event.getPlayer() != null) { + /* Compute the current set of serializer IDs in use and send them */ + try { + if(fieldsToSyncMap == null) { + fieldsToSyncMap = new HashMap<>(); + Field entityPoolField = ObfuscationReflectionHelper.findField(SynchedEntityData.class, "field_187232_a"); + Map, Integer> entityPoolMap = (Map, Integer>)entityPoolField.get(null); + List fieldsToSync = new ArrayList<>(); + for(Class eClass : entityPoolMap.keySet()) { + fieldsToSync.clear(); + Field[] classFields = eClass.getDeclaredFields(); + for(Field field : classFields) { + if(!Modifier.isStatic(field.getModifiers())) + continue; + field.setAccessible(true); + Object o = field.get(null); + if(o != null && EntityDataAccessor.class.isAssignableFrom(o.getClass())) { + fieldsToSync.add(field); + } + } + for(Field field : fieldsToSync) { + int id = ((EntityDataAccessor)field.get(null)).id; + fieldsToSyncMap.computeIfAbsent(eClass, k -> new ArrayList<>()).add(Pair.of(field.getName(), id)); + } + } + } + EntityIDSyncPacket packet = new EntityIDSyncPacket(fieldsToSyncMap); + ModernFix.LOGGER.debug("Sending ID correction packet to client with " + fieldsToSyncMap.size() + " classes"); + PacketHandler.INSTANCE.send(PacketDistributor.PLAYER.with(event::getPlayer), packet); + } catch(ObfuscationReflectionHelper.UnableToFindFieldException | ReflectiveOperationException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/core/SynchedEntityDataMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/core/SynchedEntityDataMixin.java new file mode 100644 index 00000000..67133e3f --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/core/SynchedEntityDataMixin.java @@ -0,0 +1,24 @@ +package org.embeddedt.modernfix.mixin.core; + +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.entity.Entity; +import org.embeddedt.modernfix.ModernFixClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(SynchedEntityData.class) +public class SynchedEntityDataMixin { + /** + * Store this in our set of all entity data objects. + * + * Not an ideal solution, but it should guarantee compatibility with mods. + */ + @Inject(method = "", at = @At("RETURN")) + private void storeInSet(Entity arg, CallbackInfo ci) { + synchronized (ModernFixClient.allEntityDatas) { + ModernFixClient.allEntityDatas.add((SynchedEntityData)(Object)this); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/packet/EntityIDSyncPacket.java b/src/main/java/org/embeddedt/modernfix/packet/EntityIDSyncPacket.java new file mode 100644 index 00000000..6fe2c6b1 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/packet/EntityIDSyncPacket.java @@ -0,0 +1,78 @@ +package org.embeddedt.modernfix.packet; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.world.entity.Entity; +import org.embeddedt.modernfix.ModernFix; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; + +public class EntityIDSyncPacket { + private Map, List>> map; + + public EntityIDSyncPacket(Map, List>> map) { + this.map = map; + } + + public Map, List>> getFieldInfo() { + return this.map; + } + + public EntityIDSyncPacket() { + this.map = new HashMap<>(); + } + + public void serialize(FriendlyByteBuf buf) { + buf.writeVarInt(map.keySet().size()); + for(Map.Entry, List>> entry : map.entrySet()) { + buf.writeUtf(entry.getKey().getName()); + buf.writeVarInt(entry.getValue().size()); + for(Pair field : entry.getValue()) { + buf.writeUtf(field.getFirst()); + buf.writeVarInt(field.getSecond()); + } + } + } + + @SuppressWarnings("unchecked") + public static EntityIDSyncPacket deserialize(FriendlyByteBuf buf) { + EntityIDSyncPacket self = new EntityIDSyncPacket(); + int numEntityClasses = buf.readVarInt(); + for(int i = 0; i < numEntityClasses; i++) { + String clzName = buf.readUtf(); + try { + Class clz; + try { + clz = Class.forName(clzName); + } catch(ClassNotFoundException e) { + ModernFix.LOGGER.warn("Entity class not found: {}", clzName); + break; + } + if(!Entity.class.isAssignableFrom(clz)) { + ModernFix.LOGGER.error("Not an entity: " + clzName); + break; + } + int numFields = buf.readVarInt(); + for(int j = 0; j < numFields; j++) { + String fieldName = buf.readUtf(); + int id = buf.readVarInt(); + Field f = clz.getDeclaredField(fieldName); + if(!Modifier.isStatic(f.getModifiers())) + continue; + f.setAccessible(true); + if(!EntityDataAccessor.class.isAssignableFrom(f.get(null).getClass())) { + ModernFix.LOGGER.error("Not a data accessor field: " + clz + "." + fieldName); + continue; + } + self.map.computeIfAbsent((Class)clz, k -> new ArrayList<>()).add(Pair.of(fieldName, id)); + } + } catch(ReflectiveOperationException e) { + ModernFix.LOGGER.error("Error deserializing packet", e); + } + } + return self; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/packet/PacketHandler.java b/src/main/java/org/embeddedt/modernfix/packet/PacketHandler.java new file mode 100644 index 00000000..7471e8fa --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/packet/PacketHandler.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.packet; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.fml.network.NetworkRegistry; +import net.minecraftforge.fml.network.simple.SimpleChannel; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.ModernFixClient; + +public class PacketHandler { + private static final String PROTOCOL_VERSION = "1"; + public static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel( + new ResourceLocation(ModernFix.MODID, "main"), + () -> PROTOCOL_VERSION, + NetworkRegistry.acceptMissingOr(PROTOCOL_VERSION), + NetworkRegistry.acceptMissingOr(PROTOCOL_VERSION) + ); + + public static void register() { + int id = 1; + INSTANCE.registerMessage(id++, EntityIDSyncPacket.class, EntityIDSyncPacket::serialize, EntityIDSyncPacket::deserialize, ModernFixClient::handleEntityIDSync); + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 47aa894e..a8145c22 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -8,4 +8,6 @@ public net.minecraft.client.renderer.model.ModelBakery field_217849_F # unbakedC public net.minecraft.client.renderer.texture.Stitcher$Holder public net.minecraft.util.concurrent.ThreadTaskExecutor func_213160_bf()V # runAllTasks public net.minecraft.server.MinecraftServer field_211151_aa # nextTickTime -public net.minecraft.client.Minecraft field_213277_ad # progressListener \ No newline at end of file +public net.minecraft.client.Minecraft field_213277_ad # progressListener +public-f net.minecraft.network.datasync.DataParameter field_187157_a # id +public-f net.minecraft.network.datasync.EntityDataManager field_187234_c # itemsById \ No newline at end of file diff --git a/src/main/resources/modernfix.mixins.json b/src/main/resources/modernfix.mixins.json index 4650ceeb..8dc0bbf7 100644 --- a/src/main/resources/modernfix.mixins.json +++ b/src/main/resources/modernfix.mixins.json @@ -57,6 +57,7 @@ ], "client": [ "core.MinecraftMixin", + "core.SynchedEntityDataMixin", "feature.measure_time.MinecraftMixin", "feature.reduce_loading_screen_freezes.ModelBakeryMixin", "perf.skip_first_datapack_reload.MinecraftMixin",