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 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) { 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/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; 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/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); + } } } 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..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; @@ -42,32 +43,8 @@ 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(); + 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/CustomPacketCodecs.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/CustomPacketCodecs.java index 141c452..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 @@ -1,16 +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; +import org.jetbrains.annotations.NotNull; public final class CustomPacketCodecs { private CustomPacketCodecs() { @@ -21,11 +15,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] = buf.readBoolean() ? ItemStack.OPTIONAL_STREAM_CODEC.decode(buf) : null; } return items; @@ -36,53 +26,26 @@ public static void writeItems(RegistryFriendlyByteBuf buf, ItemStack[] items) { for (final ItemStack item : items) { buf.writeBoolean(item != null); - if (item != null) { - writeItem(buf, item); + ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, item); } } } 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; + public static void writeItem(RegistryFriendlyByteBuf buf, @NotNull ItemStack item) { 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); + } catch (Exception e) { + throw new EncoderException("Failed to write ItemStack", e); } - - buf.writeInt(bytes.length); - buf.writeBytes(bytes); } -} +} \ No newline at end of file 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..ac878d4 --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/EffectsSyncHandler.java @@ -0,0 +1,79 @@ +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.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.core.registries.BuiltInRegistries; +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()); + } + + // 当玩家开始旁观另一个玩家时调用 + public static void onStartSpectating(ServerPlayer spectator, ServerPlayer target) { + syncPlayerEffects(spectator, target); + } + + // 当玩家的药水效果改变时调用 + public static void onEffectChanged(ServerPlayer player) { + // 获取所有正在旁观此玩家的观察者 + for (ServerPlayer spectator : ServerSyncController.getSpectators(player)) { + syncPlayerEffects(spectator, player); + } + } + + 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); + + // 使用ServerPlayNetworking发送包 + ClientboundEffectsSyncPacket packet = new ClientboundEffectsSyncPacket(target.getUUID(), new ArrayList<>(currentEffects)); + if (packet.canSend(spectator)) { + ServerPlayNetworking.send(spectator, packet); + } + } + } + + 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 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"