From afe59292c208151b8a0bac570e0d46e44ae05141 Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:13:04 +0800 Subject: [PATCH 1/8] update cloth config version to 20.0.149 to fix crash bug while click LAN settings in multiplayer game --- fabric/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fabric/gradle.properties b/fabric/gradle.properties index 29facc9..fe56e70 100644 --- a/fabric/gradle.properties +++ b/fabric/gradle.properties @@ -9,5 +9,5 @@ parchment_minecraft_version=1.21.10 parchment_version=2025.10.12 fabric_permissions_api_version=0.4.0 -cloth_config_version=19.0.147 +cloth_config_version=20.0.149 modmenu_version=16.0.0-rc.1 From 51f7266fd6478b2b40fa5bcc1da10418ed54fa42 Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:32:40 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix=EF=BC=9ARefactor=20CustomPacketCodecs?= =?UTF-8?q?=20to=20use=20OPTIONAL=5FSTREAM=5FCODEC=20for=20ItemStack=20ser?= =?UTF-8?q?ialization=20for=20fixing=20HotBar=20item=20decode=20fail.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fabric/sync/CustomPacketCodecs.java | 62 +++---------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java index 141c452..3f92c3e 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java @@ -1,17 +1,10 @@ package com.hpfxd.spectatorplus.fabric.sync; +import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtAccounter; -import net.minecraft.nbt.NbtIo; -import net.minecraft.nbt.NbtOps; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.item.ItemStack; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - public final class CustomPacketCodecs { private CustomPacketCodecs() { } @@ -21,11 +14,7 @@ public static ItemStack[] readItems(RegistryFriendlyByteBuf buf) { final ItemStack[] items = new ItemStack[len]; for (int slot = 0; slot < len; slot++) { - if (buf.readBoolean()) { - final ItemStack stack = readItem(buf); - - items[slot] = stack; - } + items[slot] = ItemStack.OPTIONAL_STREAM_CODEC.decode(buf); } return items; @@ -35,54 +24,23 @@ public static void writeItems(RegistryFriendlyByteBuf buf, ItemStack[] items) { buf.writeInt(items.length); for (final ItemStack item : items) { - buf.writeBoolean(item != null); - - if (item != null) { - writeItem(buf, item); - } + ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item != null ? item : ItemStack.EMPTY); } } public static ItemStack readItem(RegistryFriendlyByteBuf buf) { - final int len = buf.readInt(); - if (len == 0) { - return ItemStack.EMPTY; - } - try { - final byte[] in = new byte[len]; - buf.readBytes(in); - - final CompoundTag tag = NbtIo.readCompressed(new ByteArrayInputStream(in), NbtAccounter.unlimitedHeap()); - - var registryOps = buf.registryAccess().createSerializationContext(NbtOps.INSTANCE); - return ItemStack.CODEC.parse(registryOps, tag).resultOrPartial().orElse(ItemStack.EMPTY); - } catch (IOException e) { - throw new EncoderException(e); + return ItemStack.OPTIONAL_STREAM_CODEC.decode(buf); + } catch (Exception e) { + throw new DecoderException("Failed to read ItemStack", e); } } public static void writeItem(RegistryFriendlyByteBuf buf, ItemStack item) { - if (item.isEmpty()) { - buf.writeInt(0); - return; - } - - final byte[] bytes; try { - final CompoundTag tag = new CompoundTag(); - var registryOps = buf.registryAccess().createSerializationContext(NbtOps.INSTANCE); - ItemStack.CODEC.encode(item, registryOps, tag).getOrThrow(); - - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - NbtIo.writeCompressed(tag, out); - - bytes = out.toByteArray(); - } catch (IOException e) { - throw new EncoderException(e); + ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item != null ? item : ItemStack.EMPTY); + } catch (Exception e) { + throw new EncoderException("Failed to write ItemStack", e); } - - buf.writeInt(bytes.length); - buf.writeBytes(bytes); } -} +} \ No newline at end of file From 0af75b7712678ff61171a03710a76a50847c232a Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:24:00 +0800 Subject: [PATCH 3/8] feat: Implement effects synchronization for players in SpectatorPlus --- .../client/sync/ClientSyncController.java | 50 +++++++------- .../fabric/sync/ServerSyncController.java | 2 + .../sync/handler/EffectsSyncHandler.java | 68 +++++++++++++++++++ 3 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java index de56cb7..717ca99 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java @@ -42,32 +42,30 @@ public static void init() { private static void handle(ClientboundEffectsSyncPacket packet, ClientPlayNetworking.Context context) { setSyncData(packet.playerId()); - syncData.effects = packet.effects(); // Now List - System.out.println("[SpectatorPlus] Synced effects: " + syncData.effects); - - // var client = Minecraft.getInstance(); - // if (client.player != null) { - // // Remove all current effects from the client player - // List> toRemove = new ArrayList<>(client.player.getActiveEffectsMap().keySet()); - // for (Holder effect : toRemove) { - // System.out.println("[SpectatorPlus] Removing effect: " + BuiltInRegistries.MOB_EFFECT.getKey(effect.value())); - // client.player.removeEffect(effect); - // } - // // Add all synced effects to the client player - // for (SyncedEffect synced : syncData.effects) { - // System.out.println("[SpectatorPlus] Syncing effect: " + synced.effectKey + " duration=" + synced.duration + " amplifier=" + synced.amplifier); - // java.util.Optional> optHolder = BuiltInRegistries.MOB_EFFECT.get(ResourceLocation.tryParse(synced.effectKey)); - // if (optHolder.isPresent()) { - // MobEffect effect = optHolder.get().value(); - // Holder holder = Holder.direct(effect); - // MobEffectInstance instance = new MobEffectInstance(holder, synced.duration, synced.amplifier); - // client.player.forceAddEffect(instance, client.player); - // System.out.println("[SpectatorPlus] Added effect: " + synced.effectKey); - // } else { - // System.out.println("[SpectatorPlus] Effect not found in registry: " + synced.effectKey); - // } - // } - // } + syncData.effects = packet.effects(); + + var client = Minecraft.getInstance(); + if (client.player != null) { + // 移除客户端玩家当前的所有效果 + List> toRemove = new ArrayList<>(client.player.getActiveEffectsMap().keySet()); + for (Holder effect : toRemove) { + client.player.removeEffect(effect); + } + + // 添加所有同步的效果到客户端玩家 + for (SyncedEffect synced : syncData.effects) { + ResourceLocation effectLocation = ResourceLocation.tryParse(synced.effectKey); + if (effectLocation != null) { + java.util.Optional> optHolder = BuiltInRegistries.MOB_EFFECT.get(effectLocation); + if (optHolder.isPresent()) { + MobEffect effect = optHolder.get().value(); + Holder holder = optHolder.get(); + MobEffectInstance instance = new MobEffectInstance(holder, synced.duration, synced.amplifier); + client.player.forceAddEffect(instance, client.player); + } + } + } + } } private static void handle(ClientboundExperienceSyncPacket packet, ClientPlayNetworking.Context context) { diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/ServerSyncController.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/ServerSyncController.java index a60b466..587735e 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/ServerSyncController.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/ServerSyncController.java @@ -1,5 +1,6 @@ package com.hpfxd.spectatorplus.fabric.sync; +import com.hpfxd.spectatorplus.fabric.sync.handler.EffectsSyncHandler; import com.hpfxd.spectatorplus.fabric.sync.handler.HotbarSyncHandler; import com.hpfxd.spectatorplus.fabric.sync.handler.ScreenSyncHandler; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; @@ -15,6 +16,7 @@ public static void init() { HotbarSyncHandler.init(); ScreenSyncHandler.init(); + EffectsSyncHandler.init(); } public static void sendPacket(ServerPlayer serverPlayer, ClientboundSyncPacket packet) { diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java new file mode 100644 index 0000000..baddb7b --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java @@ -0,0 +1,68 @@ +package com.hpfxd.spectatorplus.fabric.sync.handler; + +import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.SyncedEffect; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundEffectsSyncPacket; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffectInstance; + +import java.util.*; + +public class EffectsSyncHandler { + private static final Map> EFFECTS = new HashMap<>(); + + public static void init() { + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> EFFECTS.remove(handler.getPlayer().getUUID())); + + ServerLifecycleEvents.SERVER_STOPPING.register(server -> EFFECTS.clear()); + + ServerTickEvents.END_WORLD_TICK.register(EffectsSyncHandler::tick); + } + + private static void tick(ServerLevel level) { + for (final ServerPlayer player : level.players()) { + final List cachedEffects = EFFECTS.computeIfAbsent(player.getUUID(), k -> new ArrayList<>()); + + final List currentEffects = new ArrayList<>(); + for (MobEffectInstance effectInstance : player.getActiveEffects()) { + String effectKey = BuiltInRegistries.MOB_EFFECT.getKey(effectInstance.getEffect().value()).toString(); + currentEffects.add(new SyncedEffect( + effectKey, + effectInstance.getAmplifier(), + effectInstance.getDuration() + )); + } + + if (!effectsEqual(currentEffects, cachedEffects)) { + cachedEffects.clear(); + cachedEffects.addAll(currentEffects); + + ServerSyncController.broadcastPacketToSpectators(player, new ClientboundEffectsSyncPacket(player.getUUID(), new ArrayList<>(currentEffects))); + } + } + } + + private static boolean effectsEqual(List list1, List list2) { + if (list1.size() != list2.size()) { + return false; + } + + final Map map1 = new HashMap<>(); + final Map map2 = new HashMap<>(); + + for (SyncedEffect effect : list1) { + map1.put(effect.effectKey, effect); + } + + for (SyncedEffect effect : list2) { + map2.put(effect.effectKey, effect); + } + + return map1.equals(map2); + } +} \ No newline at end of file From b93d50f468da9c0ed0784fd94b08ae75c7f2b017 Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:06:36 +0800 Subject: [PATCH 4/8] fix: effect sync and render --- .../fabric/client/mixin/GuiMixin.java | 11 ++-- .../client/mixin/LivingEntityAccessor.java | 16 +++++ .../client/mixin/LivingEntityMixin.java | 21 +++++-- .../client/sync/ClientSyncController.java | 25 +------- .../fabric/client/util/EffectUtil.java | 60 +++++++++++++++++++ .../spectatorplus.client.mixins.json | 5 +- .../fabric/mixin/LivingEntityMixin.java | 40 +++++++++++++ .../fabric/mixin/ServerPlayerMixin.java | 2 + .../sync/handler/EffectsSyncHandler.java | 51 +++++++++------- .../main/resources/spectatorplus.mixins.json | 1 + 10 files changed, 176 insertions(+), 56 deletions(-) create mode 100644 fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityAccessor.java create mode 100644 fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/util/EffectUtil.java create mode 100644 fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/LivingEntityMixin.java diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GuiMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GuiMixin.java index cb37db1..6703c46 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GuiMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GuiMixin.java @@ -178,20 +178,19 @@ public abstract class GuiMixin { int effectBaseY = baseY + slots.length * (itemHeight + spacing) + spacing; // start below armor // Render all active effect icons down the right side below armor - LocalPlayer player = this.minecraft.player; - if (player != null && player.getActiveEffects() != null && !player.getActiveEffects().isEmpty()) { + if (ClientSyncController.syncData.effects != null && !ClientSyncController.syncData.effects.isEmpty()) { int effectIndex = 0; - for (var effectInstance : player.getActiveEffects()) { + for (var effectInstance : ClientSyncController.syncData.effects) { int y = effectBaseY + effectIndex * (itemWidth + spacing); // Draw vanilla effect background guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, EFFECT_BACKGROUND_SPRITE, baseX, y, itemWidth, itemHeight); - ResourceLocation effectIcon = Gui.getMobEffectSprite(effectInstance.getEffect()); + ResourceLocation effectIcon = GuiMixin.getEffectIcon(effectInstance.effectKey); guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, effectIcon, baseX + 2, y + 2, itemWidth - 4, itemHeight - 4); // Draw effect level as a small white number on the top right of the icon - int level = effectInstance.getAmplifier() + 1; + int level = effectInstance.amplifier + 1; String levelText = String.valueOf(level); int levelTextWidth = this.minecraft.font.width(levelText); int levelTextX = baseX + itemWidth - (int)(levelTextWidth * 0.4F) - 3; // right-align inside top-right corner @@ -202,7 +201,7 @@ public abstract class GuiMixin { guiGraphics.pose().popMatrix(); // Draw duration bar (1px wide) to the left of the effect icon, color changes with percent - int duration = effectInstance.getDuration(); + int duration = effectInstance.duration; int maxDuration = 3600; // 3 minutes, adjust as needed float percent = maxDuration > 0 ? (duration / (float)maxDuration) : 1.0F; int maxBarHeight = itemHeight - 2; diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityAccessor.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityAccessor.java new file mode 100644 index 0000000..e318705 --- /dev/null +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityAccessor.java @@ -0,0 +1,16 @@ +package com.hpfxd.spectatorplus.fabric.client.mixin; + +import net.minecraft.core.Holder; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(LivingEntity.class) +public interface LivingEntityAccessor { + @Accessor("activeEffects") + Map, MobEffectInstance> spectatorplus$getActiveEffects(); +} diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityMixin.java index 2da419c..a69714c 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/LivingEntityMixin.java @@ -1,5 +1,6 @@ package com.hpfxd.spectatorplus.fabric.client.mixin; +import com.hpfxd.spectatorplus.fabric.client.util.EffectUtil; import com.hpfxd.spectatorplus.fabric.client.util.SpecUtil; import net.minecraft.client.Minecraft; import net.minecraft.client.player.AbstractClientPlayer; @@ -7,23 +8,20 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.phys.HitResult; - -import java.util.Collection; - -import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; @Mixin(LivingEntity.class) public abstract class LivingEntityMixin extends Entity { @@ -54,4 +52,15 @@ private boolean isLookingAtBlock() { return ((GameRendererAccessor) Minecraft.getInstance().gameRenderer).invokePick(this, player.blockInteractionRange(), player.entityInteractionRange(), 1F).getType() == HitResult.Type.BLOCK; } + + @Redirect(method = {"hasEffect", "getEffect", "getActiveEffects", "tickEffects"}, + at = @At(value = "FIELD", target = "Lnet/minecraft/world/entity/LivingEntity;activeEffects:Ljava/util/Map;")) + private Map, MobEffectInstance> spectatorplus$redirectActiveEffects(LivingEntity instance) { + // 只对玩家且满足条件时才重定向 + if (instance instanceof Player && EffectUtil.shouldUseSpectatorData()) { + return EffectUtil.getActiveEffectsMap(); + } + return ((LivingEntityAccessor) instance).spectatorplus$getActiveEffects(); + } + } diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java index 717ca99..00d78de 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncController.java @@ -1,6 +1,7 @@ package com.hpfxd.spectatorplus.fabric.client.sync; import com.hpfxd.spectatorplus.fabric.client.sync.screen.ScreenSyncController; +import com.hpfxd.spectatorplus.fabric.client.util.EffectUtil; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundExperienceSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundFoodSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundHotbarSyncPacket; @@ -43,29 +44,7 @@ public static void init() { private static void handle(ClientboundEffectsSyncPacket packet, ClientPlayNetworking.Context context) { setSyncData(packet.playerId()); syncData.effects = packet.effects(); - - var client = Minecraft.getInstance(); - if (client.player != null) { - // 移除客户端玩家当前的所有效果 - List> toRemove = new ArrayList<>(client.player.getActiveEffectsMap().keySet()); - for (Holder effect : toRemove) { - client.player.removeEffect(effect); - } - - // 添加所有同步的效果到客户端玩家 - for (SyncedEffect synced : syncData.effects) { - ResourceLocation effectLocation = ResourceLocation.tryParse(synced.effectKey); - if (effectLocation != null) { - java.util.Optional> optHolder = BuiltInRegistries.MOB_EFFECT.get(effectLocation); - if (optHolder.isPresent()) { - MobEffect effect = optHolder.get().value(); - Holder holder = optHolder.get(); - MobEffectInstance instance = new MobEffectInstance(holder, synced.duration, synced.amplifier); - client.player.forceAddEffect(instance, client.player); - } - } - } - } + EffectUtil.updateEffectInstances(packet.effects()); } private static void handle(ClientboundExperienceSyncPacket packet, ClientPlayNetworking.Context context) { diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/util/EffectUtil.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/util/EffectUtil.java new file mode 100644 index 0000000..dbc5796 --- /dev/null +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/util/EffectUtil.java @@ -0,0 +1,60 @@ +package com.hpfxd.spectatorplus.fabric.client.util; + + +import com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController; +import com.hpfxd.spectatorplus.fabric.sync.SyncedEffect; +import net.minecraft.client.Minecraft; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; + +import java.util.*; + +public class EffectUtil { + private static final Map, MobEffectInstance> activeEffects = new HashMap<>(); + + public static void updateEffectInstances(List effects) { + // 收集新的效果 + Set> newEffects = new HashSet<>(); + + for (SyncedEffect syncedEffect : effects) { + Holder effect = BuiltInRegistries.MOB_EFFECT.get(ResourceLocation.parse(syncedEffect.effectKey)) + .orElseThrow(() -> new IllegalArgumentException("Unknown effect: " + syncedEffect.effectKey)); + newEffects.add(effect); + } + + // 移除不再存在的效果 + activeEffects.entrySet().removeIf(entry -> !newEffects.contains(entry.getKey())); + + // 添加新效果(保持现有实例的BlendState) + for (SyncedEffect syncedEffect : effects) { + Holder effect = BuiltInRegistries.MOB_EFFECT.get(ResourceLocation.parse(syncedEffect.effectKey)) + .orElseThrow(() -> new IllegalArgumentException("Unknown effect: " + syncedEffect.effectKey)); + + if (!activeEffects.containsKey(effect)) { + MobEffectInstance instance = new MobEffectInstance(effect, syncedEffect.duration, + syncedEffect.amplifier, false, true, true); + activeEffects.put(effect, instance); + } + } + } + + public static boolean hasValidSyncData() { + return ClientSyncController.syncData != null && + ClientSyncController.syncData.effects != null; + } + + public static boolean shouldUseSpectatorData() { + Minecraft mc = Minecraft.getInstance(); + return mc.player != null && + SpecUtil.getCameraPlayer(mc) != null && + hasValidSyncData(); + } + + // 直接返回原版格式的activeEffects + public static Map, MobEffectInstance> getActiveEffectsMap() { + return activeEffects; + } +} \ No newline at end of file diff --git a/fabric/src/client/resources/spectatorplus.client.mixins.json b/fabric/src/client/resources/spectatorplus.client.mixins.json index 59c3082..f39dd46 100644 --- a/fabric/src/client/resources/spectatorplus.client.mixins.json +++ b/fabric/src/client/resources/spectatorplus.client.mixins.json @@ -34,5 +34,8 @@ ], "injectors": { "defaultRequire": 1 - } + }, + "mixins": [ + "LivingEntityAccessor" + ] } diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/LivingEntityMixin.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..4a263c5 --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/LivingEntityMixin.java @@ -0,0 +1,40 @@ +package com.hpfxd.spectatorplus.fabric.mixin; + +import com.hpfxd.spectatorplus.fabric.sync.handler.EffectsSyncHandler; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import org.jetbrains.annotations.Nullable; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Collection; + +@Mixin(LivingEntity.class) +public class LivingEntityMixin { + + @Inject(method = "onEffectAdded", at = @At("TAIL")) + private void onEffectAdded(MobEffectInstance effectInstance, @Nullable Entity entity, CallbackInfo ci) { + if ((Object) this instanceof ServerPlayer player) { + EffectsSyncHandler.onEffectChanged(player); + } + } + + @Inject(method = "onEffectUpdated", at = @At("TAIL")) + private void onEffectUpdated(MobEffectInstance effectInstance, boolean forced, @Nullable Entity entity, CallbackInfo ci) { + if ((Object) this instanceof ServerPlayer player) { + EffectsSyncHandler.onEffectChanged(player); + } + } + + @Inject(method = "onEffectsRemoved", at = @At("TAIL")) + private void onEffectsRemoved(Collection effects, CallbackInfo ci) { + if ((Object) this instanceof ServerPlayer player) { + EffectsSyncHandler.onEffectChanged(player); + } + } +} \ No newline at end of file diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerPlayerMixin.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerPlayerMixin.java index d03126d..15c7b1f 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerPlayerMixin.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerPlayerMixin.java @@ -3,6 +3,7 @@ import com.google.common.collect.Lists; import com.hpfxd.spectatorplus.fabric.SpectatorMod; import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.handler.EffectsSyncHandler; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundExperienceSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundFoodSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundHotbarSyncPacket; @@ -66,6 +67,7 @@ public ServerPlayerMixin(Level level, GameProfile gameProfile) { ServerSyncController.sendPacket(spectator, ClientboundFoodSyncPacket.initializing(target)); ServerSyncController.sendPacket(spectator, ClientboundHotbarSyncPacket.initializing(target)); ServerSyncController.sendPacket(spectator, ClientboundSelectedSlotSyncPacket.initializing(target)); + EffectsSyncHandler.onStartSpectating(spectator, target); // Send initial map data patch packet if the target has a map in inventory for (final ItemStack stack : target.getInventory().getNonEquipmentItems()) { diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java index baddb7b..ac878d4 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java @@ -4,10 +4,9 @@ import com.hpfxd.spectatorplus.fabric.sync.SyncedEffect; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundEffectsSyncPacket; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.effect.MobEffectInstance; @@ -18,31 +17,43 @@ public class EffectsSyncHandler { public static void init() { ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> EFFECTS.remove(handler.getPlayer().getUUID())); - ServerLifecycleEvents.SERVER_STOPPING.register(server -> EFFECTS.clear()); + } - ServerTickEvents.END_WORLD_TICK.register(EffectsSyncHandler::tick); + // 当玩家开始旁观另一个玩家时调用 + public static void onStartSpectating(ServerPlayer spectator, ServerPlayer target) { + syncPlayerEffects(spectator, target); } - private static void tick(ServerLevel level) { - for (final ServerPlayer player : level.players()) { - final List cachedEffects = EFFECTS.computeIfAbsent(player.getUUID(), k -> new ArrayList<>()); + // 当玩家的药水效果改变时调用 + public static void onEffectChanged(ServerPlayer player) { + // 获取所有正在旁观此玩家的观察者 + for (ServerPlayer spectator : ServerSyncController.getSpectators(player)) { + syncPlayerEffects(spectator, player); + } + } - final List currentEffects = new ArrayList<>(); - for (MobEffectInstance effectInstance : player.getActiveEffects()) { - String effectKey = BuiltInRegistries.MOB_EFFECT.getKey(effectInstance.getEffect().value()).toString(); - currentEffects.add(new SyncedEffect( - effectKey, - effectInstance.getAmplifier(), - effectInstance.getDuration() - )); - } + private static void syncPlayerEffects(ServerPlayer spectator, ServerPlayer target) { + final List currentEffects = new ArrayList<>(); + for (MobEffectInstance effectInstance : target.getActiveEffects()) { + String effectKey = BuiltInRegistries.MOB_EFFECT.getKey(effectInstance.getEffect().value()).toString(); + currentEffects.add(new SyncedEffect( + effectKey, + effectInstance.getAmplifier(), + effectInstance.getDuration() + )); + } + + final List cachedEffects = EFFECTS.computeIfAbsent(spectator.getUUID(), k -> new ArrayList<>()); - if (!effectsEqual(currentEffects, cachedEffects)) { - cachedEffects.clear(); - cachedEffects.addAll(currentEffects); + if (!effectsEqual(currentEffects, cachedEffects)) { + cachedEffects.clear(); + cachedEffects.addAll(currentEffects); - ServerSyncController.broadcastPacketToSpectators(player, new ClientboundEffectsSyncPacket(player.getUUID(), new ArrayList<>(currentEffects))); + // 使用ServerPlayNetworking发送包 + ClientboundEffectsSyncPacket packet = new ClientboundEffectsSyncPacket(target.getUUID(), new ArrayList<>(currentEffects)); + if (packet.canSend(spectator)) { + ServerPlayNetworking.send(spectator, packet); } } } diff --git a/fabric/src/main/resources/spectatorplus.mixins.json b/fabric/src/main/resources/spectatorplus.mixins.json index 3499f2b..780cf8a 100644 --- a/fabric/src/main/resources/spectatorplus.mixins.json +++ b/fabric/src/main/resources/spectatorplus.mixins.json @@ -4,6 +4,7 @@ "compatibilityLevel": "JAVA_21", "mixins": [ "ChunkMapAccessor", + "LivingEntityMixin", "ServerGamePacketListenerImplMixin", "ServerPlayerMixin", "TrackedEntityMixin" From 3751651ced59692ace2bc811e586dadbb03c1bba Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Sun, 14 Dec 2025 02:39:32 +0800 Subject: [PATCH 5/8] fix: hand height calculations bug --- .../fabric/client/mixin/ItemInHandRendererMixin.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/ItemInHandRendererMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/ItemInHandRendererMixin.java index 04e4c23..96e0817 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/ItemInHandRendererMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/ItemInHandRendererMixin.java @@ -55,8 +55,10 @@ public abstract class ItemInHandRendererMixin { if (this.spectated == spectated) { float f = spectated.getAttackStrengthScale(1.0F); - this.mainHandHeight += Mth.clamp((this.mainHandItem == mainHandItem ? f * f * f : 0.0F) - this.mainHandHeight, -0.4F, 0.4F); - this.offHandHeight += Mth.clamp((float) (this.offHandItem == offHandItem ? 1 : 0) - this.offHandHeight, -0.4F, 0.4F); + float g = this.mainHandItem != mainHandItem ? 0.0F : f * f * f; + float h = this.offHandItem != offHandItem ? 0.0F : 1.0F; + this.mainHandHeight = this.mainHandHeight + Mth.clamp(g - this.mainHandHeight, -0.4F, 0.4F); + this.offHandHeight = this.offHandHeight + Mth.clamp(h - this.offHandHeight, -0.4F, 0.4F); if (this.mainHandHeight < 0.1F) { this.mainHandItem = mainHandItem; From d243f9c6d14bdfb0d8aef499bca59166d58f2a02 Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:01:54 +0800 Subject: [PATCH 6/8] fix: handle null viewingEntity in sync data reset, which case a client crash --- .../spectatorplus/fabric/client/mixin/MinecraftMixin.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/MinecraftMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/MinecraftMixin.java index 6c1d158..1e084e9 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/MinecraftMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/MinecraftMixin.java @@ -23,8 +23,10 @@ public abstract class MinecraftMixin { @Inject(method = "setCameraEntity(Lnet/minecraft/world/entity/Entity;)V", at = @At(value = "TAIL")) private void spectatorplus$resetSyncDataOnCameraSwitch(Entity viewingEntity, CallbackInfo ci) { - if (ClientSyncController.syncData != null && !ClientSyncController.syncData.playerId.equals(viewingEntity.getUUID())) { - ClientSyncController.setSyncData(null); + if (ClientSyncController.syncData != null) { + if (viewingEntity == null || !ClientSyncController.syncData.playerId.equals(viewingEntity.getUUID())) { + ClientSyncController.setSyncData(null); + } } } From 532e1fcb34297ae57a3fca77f68885dbe9f6946d Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:53:10 +0800 Subject: [PATCH 7/8] fix: handle null ItemStack serialization in CustomPacketCodecs,which case handler cant recognize null item --- .../fabric/sync/CustomPacketCodecs.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java index 3f92c3e..a1a7dfa 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java @@ -4,6 +4,7 @@ import io.netty.handler.codec.EncoderException; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; public final class CustomPacketCodecs { private CustomPacketCodecs() { @@ -14,7 +15,7 @@ public static ItemStack[] readItems(RegistryFriendlyByteBuf buf) { final ItemStack[] items = new ItemStack[len]; for (int slot = 0; slot < len; slot++) { - items[slot] = ItemStack.OPTIONAL_STREAM_CODEC.decode(buf); + items[slot] = buf.readBoolean() ? ItemStack.OPTIONAL_STREAM_CODEC.decode(buf) : null; } return items; @@ -24,7 +25,10 @@ public static void writeItems(RegistryFriendlyByteBuf buf, ItemStack[] items) { buf.writeInt(items.length); for (final ItemStack item : items) { - ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item != null ? item : ItemStack.EMPTY); + buf.writeBoolean(item != null); + if (item != null) { + ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item); + } } } @@ -36,9 +40,10 @@ public static ItemStack readItem(RegistryFriendlyByteBuf buf) { } } - public static void writeItem(RegistryFriendlyByteBuf buf, ItemStack item) { + + public static void writeItem(RegistryFriendlyByteBuf buf, @NotNull ItemStack item) { try { - ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item != null ? item : ItemStack.EMPTY); + ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item); } catch (Exception e) { throw new EncoderException("Failed to write ItemStack", e); } From 6db7fdf33c7c6e3d4a2f1d8525d1c15990b4d627 Mon Sep 17 00:00:00 2001 From: RCUTANF <110910565+RCUTANF@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:14:33 +0800 Subject: [PATCH 8/8] fix: prevent incorrect arm orientation when spectating by skipping mulPose in renderItemInHand --- .../fabric/client/mixin/GameRendererMixin.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GameRendererMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GameRendererMixin.java index de281b2..a6f1873 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GameRendererMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/GameRendererMixin.java @@ -14,10 +14,14 @@ import net.minecraft.core.Direction; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.animal.CowVariant; +import net.minecraft.world.entity.player.PlayerModelType; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.level.GameType; import net.minecraft.world.phys.Vec3; import org.joml.Matrix4f; +import org.joml.Matrix4fc; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -45,6 +49,18 @@ public abstract class GameRendererMixin { @Unique private float xBobO; @Unique private float yBobO; + @Redirect(method = "renderItemInHand", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;mulPose(Lorg/joml/Matrix4fc;)V", ordinal = 0)) + private void redirectMulPose(PoseStack poseStack, Matrix4fc matrix) { + // In spectator mode, the projection matrix contains rotation data from the spectated player, + // while arm rendering calculations are based on the localPlayer's viewpoint. + // We must skip this mulPose operation when spectating to avoid rendering arms with incorrect orientation. + if (this.minecraft.player == null + || this.minecraft.player.gameMode() != GameType.SPECTATOR + || !this.minecraft.options.getCameraType().isFirstPerson()) { + poseStack.mulPose(matrix); + } + } + @Inject(method = "renderItemInHand", at = @At(value = "INVOKE", target = "Lorg/joml/Matrix4fStack;popMatrix()Lorg/joml/Matrix4fStack;", remap = false)) public void spectatorplus$renderItemInHand(float partialTicks, boolean sleeping, Matrix4f projectionMatrix, CallbackInfo ci, @Local PoseStack poseStackIn) { if (SpectatorClientMod.config.renderArms && this.minecraft.player != null && this.minecraft.options.getCameraType().isFirstPerson() && !this.minecraft.options.hideGui) {