diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/DummyContainer.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/DummyContainer.java index 48a6400..16a9a12 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/DummyContainer.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/DummyContainer.java @@ -1,29 +1,38 @@ package com.hpfxd.spectatorplus.fabric.client.gui.screens; +import net.minecraft.core.NonNullList; import net.minecraft.world.Container; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.NotNull; public class DummyContainer implements Container { - private final int size; + private final NonNullList items; public DummyContainer(int size) { - this.size = size; + this.items = NonNullList.withSize(size, ItemStack.EMPTY); } @Override public int getContainerSize() { - return this.size; + return this.items.size(); } @Override public boolean isEmpty() { + for (ItemStack item : this.items) { + if (!item.isEmpty()) { + return false; + } + } return true; } @Override public @NotNull ItemStack getItem(int slot) { + if (slot >= 0 && slot < this.items.size()) { + return this.items.get(slot); + } return ItemStack.EMPTY; } @@ -39,6 +48,9 @@ public boolean isEmpty() { @Override public void setItem(int slot, ItemStack stack) { + if (slot >= 0 && slot < this.items.size()) { + this.items.set(slot, stack); + } } @Override @@ -52,5 +64,6 @@ public boolean stillValid(Player player) { @Override public void clearContent() { + this.items.clear(); } } diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/SyncedInventoryScreen.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/SyncedInventoryScreen.java index 0ca8ea0..e0ae3db 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/SyncedInventoryScreen.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/gui/screens/SyncedInventoryScreen.java @@ -1,8 +1,8 @@ package com.hpfxd.spectatorplus.fabric.client.gui.screens; -import com.hpfxd.spectatorplus.fabric.client.mixin.InventoryAccessor; import com.hpfxd.spectatorplus.fabric.client.mixin.screen.AbstractRecipeBookScreenAccessor; import com.hpfxd.spectatorplus.fabric.client.mixin.screen.ImageButtonAccessor; +import com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController; import net.minecraft.client.gui.components.ImageButton; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.components.WidgetSprites; @@ -10,7 +10,6 @@ import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.screens.inventory.InventoryScreen; import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; -import net.minecraft.world.entity.EntityEquipment; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; @@ -33,15 +32,17 @@ public void containerTick() { } private void syncOtherItems() { - final SyncedInventoryMenu menu = (SyncedInventoryMenu) this.menu; - final Inventory fakeInventory = menu.getInventory(); - - // Use synced inventory data for all slots (main, armor, offhand) - var syncData = com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController.syncData; - if (syncData != null && syncData.screen != null && syncData.screen.inventoryItems != null) { - var items = syncData.screen.inventoryItems; - for (int i = 0; i < items.size(); i++) { - fakeInventory.setItem(i, items.get(i)); + // If the mixin worked correctly, this.menu should be a SyncedInventoryMenu + if (this.menu instanceof SyncedInventoryMenu syncedMenu) { + final Inventory inventory = syncedMenu.getInventory(); + + // Use synced inventory data for all slots (main, armor, offhand) + var syncData = ClientSyncController.syncData; + if (syncData != null && syncData.screen != null && syncData.screen.inventoryItems != null) { + var items = syncData.screen.inventoryItems; + for (int i = 0; i < items.size() && i < 41; i++) { + inventory.setItem(i, items.get(i)); + } } } } 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 e0cdcc2..b243423 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 @@ -24,7 +24,7 @@ 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 && (viewingEntity == null || !ClientSyncController.syncData.playerId.equals(viewingEntity.getUUID()))) { - ClientSyncController.setSyncData(null); + ClientSyncController.createSyncDataIfNull(null); } } @@ -64,7 +64,7 @@ public abstract class MinecraftMixin { if (ClientPlayNetworking.canSend(ServerboundOpenedInventorySyncPacket.TYPE)) { // just let the server we've opened our inventory. this is used to sync with other users spectating this client - ClientPlayNetworking.send(new ServerboundOpenedInventorySyncPacket()); + ClientPlayNetworking.send(new ServerboundOpenedInventorySyncPacket(true)); } return true; } diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/AbstractContainerScreenMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/AbstractContainerScreenMixin.java index 248ceb6..52cae8e 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/AbstractContainerScreenMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/AbstractContainerScreenMixin.java @@ -5,6 +5,7 @@ import com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController; import com.hpfxd.spectatorplus.fabric.client.sync.screen.ScreenSyncController; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.renderer.RenderPipelines; @@ -73,7 +74,7 @@ public abstract class AbstractContainerScreenMixin { ) ) private void spectatorplus$renderSyncedCursorItem(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, CallbackInfo ci) { - if (!this.spectatorplus$isSyncedScreen()) { + if (!this.spectatorplus$isSyncedScreen() || ClientSyncController.syncData == null || ClientSyncController.syncData.screen == null) { return; } @@ -143,6 +144,51 @@ public abstract class AbstractContainerScreenMixin { this.animations.removeIf(animation -> ++animation.tick >= MOVE_ANIMATION_TICKS); } + @Inject(method = "containerTick", at = @At("TAIL")) + private void spectatorplus$syncContainerItems(CallbackInfo ci) { + if (!this.spectatorplus$isSyncedScreen()) { + return; + } + + this.spectatorplus$syncContainerItems(); + } + + + @Unique + private void spectatorplus$syncContainerItems() { + AbstractContainerScreen self = (AbstractContainerScreen) (Object) this; + var minecraft = Minecraft.getInstance(); + + if (minecraft == null || minecraft.player == null) { + return; + } + + var syncData = ClientSyncController.syncData; + if (syncData == null || syncData.screen == null || syncData.screen.containerItems == null) { + return; + } + + var containerItems = syncData.screen.containerItems; + + // Find container slots and sync items + int containerItemIndex = 0; + for (int slotIndex = 0; slotIndex < self.getMenu().slots.size() && containerItemIndex < containerItems.size(); slotIndex++) { + Slot slot = self.getMenu().slots.get(slotIndex); + + // Only sync container slots, not player inventory + if (slot.container != minecraft.player.getInventory()) { + ItemStack syncedItem = containerItems.get(containerItemIndex); + slot.set(syncedItem); + containerItemIndex++; + } + } + + // Sync cursor item + if (syncData.screen.cursorItem != null) { + minecraft.player.containerMenu.setCarried(syncData.screen.cursorItem); + } + } + @ModifyExpressionValue( method = "renderTooltip(Lnet/minecraft/client/gui/GuiGraphics;II)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/inventory/Slot;hasItem()Z") diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/MenuScreensMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/MenuScreensMixin.java index ddf08d7..e3624cd 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/MenuScreensMixin.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/MenuScreensMixin.java @@ -50,18 +50,28 @@ public abstract class MenuScreensMixin { // if no inventory could be created, we close the inventory } else { - final Inventory inventory; - if (hasInventory) { - inventory = ScreenSyncController.syncedInventory; + // Handle container screens + if (ClientSyncController.syncData.screen.containerType != null) { + // Open container screen with proper type and size + ScreenSyncController.openContainerScreen(mc, + ClientSyncController.syncData.screen.containerType, + ClientSyncController.syncData.screen.containerSize); + return; } else { - inventory = mc.player.getInventory(); - } + // Fallback to original screen creation for unknown container types + final Inventory inventory; + if (hasInventory) { + inventory = ScreenSyncController.syncedInventory; + } else { + inventory = mc.player.getInventory(); + } - final M menu = type.create(windowId, inventory); - final S screen = screenConstructor.create(menu, inventory, title); + final M menu = type.create(windowId, inventory); + final S screen = screenConstructor.create(menu, inventory, title); - ScreenSyncController.handleNewSyncedScreen(mc, screen); - return; + ScreenSyncController.handleNewSyncedScreen(mc, screen); + return; + } } } } diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/ScreenMixin.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/ScreenMixin.java new file mode 100644 index 0000000..57236ee --- /dev/null +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/mixin/screen/ScreenMixin.java @@ -0,0 +1,26 @@ +package com.hpfxd.spectatorplus.fabric.client.mixin.screen; + +import com.hpfxd.spectatorplus.fabric.sync.packet.ServerboundOpenedInventorySyncPacket; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +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(Screen.class) +public class ScreenMixin { + + @Inject(method = "onClose()V", at = @At("HEAD")) + private void spectatorplus$onScreenClose(CallbackInfo ci) { + // Check if this is an inventory or container screen being closed + if ((Object) this instanceof InventoryScreen || (Object) this instanceof AbstractContainerScreen) { + // Notify server that we closed our inventory/container + if (ClientPlayNetworking.canSend(ServerboundOpenedInventorySyncPacket.TYPE)) { + ClientPlayNetworking.send(new ServerboundOpenedInventorySyncPacket(false)); + } + } + } +} 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 7b1b89c..97aa86d 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 @@ -5,16 +5,8 @@ import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundExperienceSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundFoodSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundHotbarSyncPacket; -import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundInventorySyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundSelectedSlotSyncPacket; -import java.util.List; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundEffectsSyncPacket; -import com.hpfxd.spectatorplus.fabric.sync.SyncedEffect; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.Holder; -import net.minecraft.resources.Identifier; -import net.minecraft.world.effect.MobEffect; -import net.minecraft.world.effect.MobEffectInstance; import net.fabricmc.fabric.api.client.networking.v1.ClientLoginConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; @@ -22,38 +14,35 @@ import net.minecraft.world.food.FoodData; import net.minecraft.world.item.ItemStack; -import java.util.ArrayList; import java.util.UUID; public class ClientSyncController { public static ClientSyncData syncData; - private static Minecraft minecraft = Minecraft.getInstance(); + private static final Minecraft minecraft = Minecraft.getInstance(); public static void init() { ClientPlayNetworking.registerGlobalReceiver(ClientboundExperienceSyncPacket.TYPE, ClientSyncController::handle); ClientPlayNetworking.registerGlobalReceiver(ClientboundFoodSyncPacket.TYPE, ClientSyncController::handle); ClientPlayNetworking.registerGlobalReceiver(ClientboundHotbarSyncPacket.TYPE, ClientSyncController::handle); - ClientPlayNetworking.registerGlobalReceiver(ClientboundSelectedSlotSyncPacket.TYPE, - ClientSyncController::handle); + ClientPlayNetworking.registerGlobalReceiver(ClientboundSelectedSlotSyncPacket.TYPE, ClientSyncController::handle); ClientPlayNetworking.registerGlobalReceiver(ClientboundEffectsSyncPacket.TYPE, ClientSyncController::handle); - ClientLoginConnectionEvents.INIT.register((handler, client) -> setSyncData(null)); - ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> setSyncData(null)); + ClientLoginConnectionEvents.INIT.register((handler, client) -> createSyncDataIfNull(null)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> createSyncDataIfNull(null)); ScreenSyncController.init(); } private static void handle(ClientboundEffectsSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); + createSyncDataIfNull(packet.playerId()); syncData.effects = packet.effects(); EffectUtil.updateEffectInstances(packet.effects()); } private static void handle(ClientboundExperienceSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); + createSyncDataIfNull(packet.playerId()); var player = minecraft.player; - if (player != null - && (packet.progress() != player.experienceProgress || packet.level() != player.experienceLevel)) + if (player != null && (packet.progress() != player.experienceProgress || packet.level() != player.experienceLevel)) player.experienceDisplayStartTick = player.tickCount; syncData.experienceProgress = packet.progress(); @@ -62,7 +51,7 @@ private static void handle(ClientboundExperienceSyncPacket packet, ClientPlayNet } private static void handle(ClientboundFoodSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); + createSyncDataIfNull(packet.playerId()); if (syncData.foodData == null) { syncData.foodData = new FoodData(); @@ -72,7 +61,7 @@ private static void handle(ClientboundFoodSyncPacket packet, ClientPlayNetworkin } private static void handle(ClientboundHotbarSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); + createSyncDataIfNull(packet.playerId()); final ItemStack[] items = packet.items(); for (int slot = 0; slot < items.length; slot++) { @@ -85,12 +74,12 @@ private static void handle(ClientboundHotbarSyncPacket packet, ClientPlayNetwork } private static void handle(ClientboundSelectedSlotSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); + createSyncDataIfNull(packet.playerId()); syncData.selectedHotbarSlot = packet.selectedSlot(); } - public static void setSyncData(UUID playerId) { + public static void createSyncDataIfNull(UUID playerId) { if (playerId == null) { syncData = null; } else if (syncData == null || !syncData.playerId.equals(playerId)) { diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncData.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncData.java index ec15e27..ecb009f 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncData.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/ClientSyncData.java @@ -28,7 +28,7 @@ public ClientSyncData(UUID playerId) { this.playerId = playerId; } - public void setScreen() { + public void createScreenIfNull() { if (this.screen == null) { this.screen = new ScreenSyncData(); } diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncController.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncController.java index 19fc69d..e4b53b2 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncController.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncController.java @@ -1,33 +1,34 @@ package com.hpfxd.spectatorplus.fabric.client.sync.screen; +import com.hpfxd.spectatorplus.fabric.client.gui.screens.DummyContainer; import com.hpfxd.spectatorplus.fabric.client.gui.screens.SyncedInventoryScreen; import com.hpfxd.spectatorplus.fabric.client.mixin.InventoryAccessor; import com.hpfxd.spectatorplus.fabric.client.util.SpecUtil; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundContainerSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundInventorySyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundScreenCursorSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundScreenSyncPacket; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; -import net.fabricmc.fabric.api.networking.v1.PacketSender; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.MenuAccess; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.core.NonNullList; +import net.minecraft.client.gui.screens.inventory.*; +import net.minecraft.network.chat.Component; import net.minecraft.world.entity.EntityEquipment; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; import net.minecraft.world.item.ItemStack; -import static com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController.setSyncData; +import static com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController.createSyncDataIfNull; import static com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController.syncData; public class ScreenSyncController { public static boolean isPendingOpen = false; public static int syncedWindowId = Integer.MIN_VALUE; - public static Inventory syncedInventory; + public static Inventory syncedInventory;//dummy inventory public static Screen syncedScreen; public static void init() { @@ -37,77 +38,242 @@ public static void init() { } }); - ClientPlayNetworking.registerGlobalReceiver(ClientboundScreenSyncPacket.TYPE, ScreenSyncController::handle); ClientPlayNetworking.registerGlobalReceiver(ClientboundInventorySyncPacket.TYPE, ScreenSyncController::handle); + ClientPlayNetworking.registerGlobalReceiver(ClientboundContainerSyncPacket.TYPE, ScreenSyncController::handle); + ClientPlayNetworking.registerGlobalReceiver(ClientboundScreenSyncPacket.TYPE, ScreenSyncController::handle); ClientPlayNetworking.registerGlobalReceiver(ClientboundScreenCursorSyncPacket.TYPE, ScreenSyncController::handle); } + private static void handle(ClientboundContainerSyncPacket packet, ClientPlayNetworking.Context context) { + createSyncDataIfNull(packet.playerId()); + syncData.createScreenIfNull(); + + // Set container data + syncData.screen.containerType = packet.containerType(); + syncData.screen.containerSize = packet.containerSize(); + + // Initialize container items if not already done + syncData.screen.initializeContainerItems(packet.containerSize()); + + // Update container items + // Support incremental updates: null items in the array mean "don't update this slot" + syncData.screen.updateContainerItems(packet.items()); + } + private static void handle(ClientboundScreenSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); - syncData.setScreen(); + createSyncDataIfNull(packet.playerId()); + syncData.createScreenIfNull(); + + // If the packet indicates the screen is closed, close it on the client + if (packet.isScreenClosed()) { + final Minecraft mc = Minecraft.getInstance(); + mc.execute(ScreenSyncController::closeSyncedInventory); + return; + } isPendingOpen = true; syncData.screen.isSurvivalInventory = packet.isSurvivalInventory(); syncData.screen.isClientRequested = packet.isClientRequested(); syncData.screen.hasDummySlots = packet.hasDummySlots(); + + // If this is a survival inventory screen sync, try to open it immediately + if (syncData.screen.isSurvivalInventory) { + final Minecraft mc = Minecraft.getInstance(); + mc.execute(() -> { + if (isPendingOpen) { + // Create the synced inventory first, so the mixin can use it + final Player spectated = SpecUtil.getCameraPlayer(mc); + if (spectated != null && createInventory(spectated)) { + openPlayerInventory(mc); + } + } + }); + } } private static void handle(ClientboundInventorySyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); - syncData.setScreen(); + createSyncDataIfNull(packet.playerId()); + syncData.createScreenIfNull(); - if (syncData.screen.inventoryItems == null || syncData.screen.inventoryItems.size() != ClientboundInventorySyncPacket.ITEMS_LENGTH) { - syncData.screen.inventoryItems = NonNullList.withSize(ClientboundInventorySyncPacket.ITEMS_LENGTH, ItemStack.EMPTY); - } + syncData.screen.initializeInventoryItems(ClientboundInventorySyncPacket.ITEMS_LENGTH); final ItemStack[] items = packet.items(); - for (int slot = 0; slot < items.length; slot++) { - final ItemStack item = items[slot]; - if (item != null) { - syncData.screen.inventoryItems.set(slot, item); - if (syncedInventory != null) { - syncedInventory.setItem(slot, item); - } - } - } - // Also update global armorItems for convenience - if (syncData.armorItems != null && items.length >= 40) { - for (int slot = 36; slot < 40; slot++) { - final ItemStack item = items[slot]; - if (item != null) { - syncData.armorItems.set(slot - 36, item); - } - } - } + syncData.screen.updateInventoryItems(items); } private static void handle(ClientboundScreenCursorSyncPacket packet, ClientPlayNetworking.Context context) { - setSyncData(packet.playerId()); - syncData.setScreen(); + createSyncDataIfNull(packet.playerId()); + syncData.createScreenIfNull(); syncData.screen.cursorItem = packet.cursor(); syncData.screen.cursorItemSlot = packet.originSlot(); } public static void closeSyncedInventory() { - if (syncedScreen != null) { - syncedScreen.onClose(); - syncedInventory = null; + final Minecraft mc = Minecraft.getInstance(); + mc.setScreen(null); + syncedScreen = null; + + syncedInventory = null; + syncedWindowId = Integer.MIN_VALUE; + isPendingOpen = false; + if (syncData != null) { + syncData.screen = null; } } public static void openPlayerInventory(Minecraft mc) { - final Player player = SpecUtil.getCameraPlayer(mc); - final SyncedInventoryScreen screen = new SyncedInventoryScreen(player); - + final Player spectated = SpecUtil.getCameraPlayer(mc); + final SyncedInventoryScreen screen = new SyncedInventoryScreen(spectated); handleNewSyncedScreen(mc, screen); } + public static void openContainerScreen(Minecraft mc, MenuType containerType, int containerSize) { + final Player spectated = SpecUtil.getCameraPlayer(mc); + if (spectated != null && mc.player != null && syncData != null && syncData.screen != null) { + // Create dummy container for the synced screen + DummyContainer containerInventory = new DummyContainer(containerSize); + + // Populate the container with synced items using helper method + syncData.screen.populateContainer(containerInventory); + + Inventory playerInventory = syncedInventory != null ? syncedInventory : mc.player.getInventory(); + Screen screen = createVanillaContainerScreen(containerType, syncedWindowId, playerInventory, containerInventory); + + if (screen instanceof MenuAccess) { + handleNewSyncedScreen(mc, (Screen & MenuAccess) screen); + } + } + } + + private static Screen createVanillaContainerScreen(MenuType type, int windowId, Inventory playerInventory, DummyContainer containerInventory) { + Component title = Component.literal("Container"); + + // For furnace-like containers + if (type == MenuType.FURNACE) { + FurnaceMenu menu = new FurnaceMenu(windowId, playerInventory, containerInventory, new SimpleContainerData(4)); + return new FurnaceScreen(menu, playerInventory, title); + } else if (type == MenuType.BLAST_FURNACE) { + BlastFurnaceMenu menu = new BlastFurnaceMenu(windowId, playerInventory, containerInventory, new SimpleContainerData(4)); + return new BlastFurnaceScreen(menu, playerInventory, title); + } else if (type == MenuType.SMOKER) { + SmokerMenu menu = new SmokerMenu(windowId, playerInventory, containerInventory, new SimpleContainerData(4)); + return new SmokerScreen(menu, playerInventory, title); + } + + // For brewing stand + else if (type == MenuType.BREWING_STAND) { + BrewingStandMenu menu = new BrewingStandMenu(windowId, playerInventory, containerInventory, new SimpleContainerData(2)); + return new BrewingStandScreen(menu, playerInventory, title); + } + + // For anvil + else if (type == MenuType.ANVIL) { + AnvilMenu menu = new AnvilMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new AnvilScreen(menu, playerInventory, title); + } + + // For enchanting table + else if (type == MenuType.ENCHANTMENT) { + EnchantmentMenu menu = new EnchantmentMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new EnchantmentScreen(menu, playerInventory, title); + } + + // For crafting table + else if (type == MenuType.CRAFTING) { + CraftingMenu menu = new CraftingMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new CraftingScreen(menu, playerInventory, title); + } + + // For grindstone + else if (type == MenuType.GRINDSTONE) { + GrindstoneMenu menu = new GrindstoneMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new GrindstoneScreen(menu, playerInventory, title); + } + + // For loom + else if (type == MenuType.LOOM) { + LoomMenu menu = new LoomMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new LoomScreen(menu, playerInventory, title); + } + + // For stonecutter + else if (type == MenuType.STONECUTTER) { + StonecutterMenu menu = new StonecutterMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new StonecutterScreen(menu, playerInventory, title); + } + + // For cartography table + else if (type == MenuType.CARTOGRAPHY_TABLE) { + CartographyTableMenu menu = new CartographyTableMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new CartographyTableScreen(menu, playerInventory, title); + } + + // For smithing table + else if (type == MenuType.SMITHING) { + SmithingMenu menu = new SmithingMenu(windowId, playerInventory, ContainerLevelAccess.NULL); + return new SmithingScreen(menu, playerInventory, title); + } + + // For generic containers (chests, etc.) + else if (type == MenuType.GENERIC_9x1) { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x1, windowId, playerInventory, containerInventory, 1); + return new ContainerScreen(menu, playerInventory, title); + } else if (type == MenuType.GENERIC_9x2) { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x2, windowId, playerInventory, containerInventory, 2); + return new ContainerScreen(menu, playerInventory, title); + } else if (type == MenuType.GENERIC_9x3) { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x3, windowId, playerInventory, containerInventory, 3); + return new ContainerScreen(menu, playerInventory, title); + } else if (type == MenuType.GENERIC_9x4) { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x4, windowId, playerInventory, containerInventory, 4); + return new ContainerScreen(menu, playerInventory, title); + } else if (type == MenuType.GENERIC_9x5) { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x5, windowId, playerInventory, containerInventory, 5); + return new ContainerScreen(menu, playerInventory, title); + } else if (type == MenuType.GENERIC_9x6) { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x6, windowId, playerInventory, containerInventory, 6); + return new ContainerScreen(menu, playerInventory, title); + } + + // For dispenser and dropper + else if (type == MenuType.GENERIC_3x3) { + DispenserMenu menu = new DispenserMenu(windowId, playerInventory, containerInventory); + return new DispenserScreen(menu, playerInventory, title); + } + + // For hopper + else if (type == MenuType.HOPPER) { + HopperMenu menu = new HopperMenu(windowId, playerInventory, containerInventory); + return new HopperScreen(menu, playerInventory, title); + } + + // For shulker box + else if (type == MenuType.SHULKER_BOX) { + ShulkerBoxMenu menu = new ShulkerBoxMenu(windowId, playerInventory, containerInventory); + return new ShulkerBoxScreen(menu, playerInventory, title); + } + + // For beacon + else if (type == MenuType.BEACON) { + BeaconMenu menu = new BeaconMenu(windowId, containerInventory, new SimpleContainerData(3), ContainerLevelAccess.NULL); + return new BeaconScreen(menu, playerInventory, title); + } + + // Fallback to generic 3x9 chest for unknown types (including lectern which has no GUI) + else { + ChestMenu menu = new ChestMenu(MenuType.GENERIC_9x3, windowId, playerInventory, containerInventory, 3); + return new ContainerScreen(menu, playerInventory, title); + } + } + public static > void handleNewSyncedScreen(Minecraft mc, S screen) { isPendingOpen = false; + if(mc.player == null) return; mc.player.containerMenu = screen.getMenu(); mc.setScreen(screen); + // sanity check to avoid registering events on the wrong screen if (mc.screen != screen) { syncedInventory = null; syncData.screen = null; @@ -125,18 +291,13 @@ public static > void handleNewSyncedScreen(Mine } public static boolean createInventory(Player spectated) { - if (syncData.screen.inventoryItems == null) { + if (!syncData.screen.hasInventoryItems()) { return false; } EntityEquipment equipment = ((InventoryAccessor)spectated.getInventory()).getEquipment(); syncedInventory = new Inventory(spectated, equipment); - - // Set all inventory slots (0-39) - for (int i = 0; i < syncData.screen.inventoryItems.size(); i++) { - syncedInventory.setItem(i, syncData.screen.inventoryItems.get(i)); - } - // No direct access to armor list; setting slots 36-39 is sufficient for GUI + syncData.screen.populateInventory(syncedInventory); return true; } } diff --git a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncData.java b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncData.java index 8e69bac..933f88d 100644 --- a/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncData.java +++ b/fabric/src/client/java/com/hpfxd/spectatorplus/fabric/client/sync/screen/ScreenSyncData.java @@ -1,8 +1,13 @@ package com.hpfxd.spectatorplus.fabric.client.sync.screen; import net.minecraft.core.NonNullList; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.MenuType; import net.minecraft.world.item.ItemStack; +import static com.hpfxd.spectatorplus.fabric.client.sync.ClientSyncController.syncData; + public class ScreenSyncData { /** * inventoryItems layout: @@ -11,10 +16,139 @@ public class ScreenSyncData { */ public NonNullList inventoryItems; + /** + * Container items for non-inventory screens + */ + public NonNullList containerItems; + + /** + * Container type for determining proper GUI rendering + */ + public MenuType containerType; + + /** + * Container size for dynamic containers like chests + */ + public int containerSize; + public boolean isSurvivalInventory; public boolean isClientRequested; public boolean hasDummySlots; public ItemStack cursorItem = ItemStack.EMPTY; public int cursorItemSlot = -1; + + /** + * Initialize or resize container items to the specified size + * @param size the desired size of the container + */ + public void initializeContainerItems(int size) { + if (containerItems == null || containerItems.size() != size) { + containerItems = NonNullList.withSize(size, ItemStack.EMPTY); + } + } + + /** + * Update container item at the specified slot + * @param slot the slot index + * @param item the item to set (null means skip this slot) + */ + public void updateContainerItem(int slot, ItemStack item) { + if (containerItems != null && slot >= 0 && slot < containerItems.size() && item != null) { + containerItems.set(slot, item.isEmpty() ? ItemStack.EMPTY : item); + } + } + + /** + * Update multiple container items from an array + * @param items array of items to update (null items are skipped) + */ + public void updateContainerItems(ItemStack[] items) { + if (containerItems == null || items == null) { + return; + } + for (int i = 0; i < items.length && i < containerItems.size(); i++) { + updateContainerItem(i, items[i]); + } + } + + /** + * Populate a Container with the synced container items + * @param container the container to populate + */ + public void populateContainer(Container container) { + if (containerItems == null || container == null) { + return; + } + for (int i = 0; i < containerItems.size() && i < container.getContainerSize(); i++) { + container.setItem(i, containerItems.get(i)); + } + } + + /** + * Check if container items are initialized + * @return true if containerItems is not null + */ + public boolean hasContainerItems() { + return containerItems != null; + } + + /** + * Initialize or resize inventory items to the specified size + * @param size the desired size of the inventory + */ + public void initializeInventoryItems(int size) { + if (inventoryItems == null || inventoryItems.size() != size) { + inventoryItems = NonNullList.withSize(size, ItemStack.EMPTY); + } + } + + /** + * Update inventory item at the specified slot + * @param slot the slot index + * @param item the item to set + */ + public void updateInventoryItem(int slot, ItemStack item) { + if (inventoryItems != null && slot >= 0 && slot < inventoryItems.size() && item != null) { + inventoryItems.set(slot, item); + } + } + + public void updateInventoryItems(ItemStack[] items) { + if (inventoryItems == null) return; + + for (int i = 0; i < items.length && i < inventoryItems.size(); i++) { + updateInventoryItem(i, items[i]); + } + //update armor items + if (syncData != null) { + for (int i = 36; i < 40 && i < items.length; i++) { + final ItemStack item = items[i]; + if (item != null) { + syncData.armorItems.set(i - 36, item); + } + } + } + } + + /** + * Populate an Inventory with the synced inventory items + * @param inventory the inventory to populate + */ + public void populateInventory(Inventory inventory) { + if (inventoryItems == null || inventory == null) { + return; + } + for (int i = 0; i < inventoryItems.size(); i++) { + inventory.setItem(i, inventoryItems.get(i)); + } + } + + /** + * Check if inventory items are initialized + * @return true if inventoryItems is not null + */ + public boolean hasInventoryItems() { + return inventoryItems != null; + } } 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 index 3119807..ebcb05d 100644 --- 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 @@ -16,7 +16,7 @@ public class EffectUtil { private static final Map, MobEffectInstance> activeEffects = new HashMap<>(); public static void updateEffectInstances(List effects) { - // 收集新的效果 + // collecting new effects Set> newEffects = new HashSet<>(); for (SyncedEffect syncedEffect : effects) { @@ -25,10 +25,10 @@ public static void updateEffectInstances(List effects) { newEffects.add(effect); } - // 移除不再存在的效果 + // remove effects that are no longer present activeEffects.entrySet().removeIf(entry -> !newEffects.contains(entry.getKey())); - // 添加新效果(保持现有实例的BlendState) + // add new effects (keep existing instances' BlendState) for (SyncedEffect syncedEffect : effects) { Holder effect = BuiltInRegistries.MOB_EFFECT.get(Identifier.parse(syncedEffect.effectKey)) .orElseThrow(() -> new IllegalArgumentException("Unknown effect: " + syncedEffect.effectKey)); @@ -53,7 +53,6 @@ public static boolean shouldUseSpectatorData() { hasValidSyncData(); } - // 直接返回原版格式的activeEffects public static Map, MobEffectInstance> getActiveEffectsMap() { return activeEffects; } diff --git a/fabric/src/client/resources/spectatorplus.client.mixins.json b/fabric/src/client/resources/spectatorplus.client.mixins.json index d302c21..b650ae5 100644 --- a/fabric/src/client/resources/spectatorplus.client.mixins.json +++ b/fabric/src/client/resources/spectatorplus.client.mixins.json @@ -31,7 +31,8 @@ "screen.ImageButtonAccessor", "screen.InventoryMenuMixin", "screen.InventoryScreenMixin", - "screen.MenuScreensMixin" + "screen.MenuScreensMixin", + "screen.ScreenMixin" ], "injectors": { "defaultRequire": 1 diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerGamePacketListenerImplMixin.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerGamePacketListenerImplMixin.java index 15d8e13..316ad4c 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerGamePacketListenerImplMixin.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/mixin/ServerGamePacketListenerImplMixin.java @@ -1,7 +1,9 @@ package com.hpfxd.spectatorplus.fabric.mixin; import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.handler.CursorSyncHandler; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundSelectedSlotSyncPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClickPacket; import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.ServerGamePacketListenerImpl; @@ -20,4 +22,10 @@ public abstract class ServerGamePacketListenerImplMixin { private void spectatorplus$syncSelectedSlot(ServerboundSetCarriedItemPacket packet, CallbackInfo ci) { ServerSyncController.broadcastPacketToSpectators(this.player, new ClientboundSelectedSlotSyncPacket(this.player.getUUID(), packet.getSlot())); } + + @Inject(method = "handleContainerClick(Lnet/minecraft/network/protocol/game/ServerboundContainerClickPacket;)V", at = @At("TAIL")) + private void spectatorplus$syncCursorAfterClick(ServerboundContainerClickPacket packet, CallbackInfo ci) { + // After handling container click, check if cursor item changed + CursorSyncHandler.onCursorChanged(this.player, this.player.containerMenu.getCarried(), packet.slotNum()); + } } 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 15c7b1f..a4d07ef 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,14 +3,13 @@ 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.handler.*; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundExperienceSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundFoodSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundHotbarSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundSelectedSlotSyncPacket; import com.llamalad7.mixinextras.sugar.Local; import com.mojang.authlib.GameProfile; -import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponents; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket; @@ -35,7 +34,9 @@ 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.OptionalInt; import java.util.Set; @Mixin(ServerPlayer.class) @@ -58,6 +59,16 @@ public ServerPlayerMixin(Level level, GameProfile gameProfile) { ServerSyncController.broadcastPacketToSpectators((ServerPlayer) (Object) this, new ClientboundExperienceSyncPacket(this.getUUID(), this.experienceProgress, this.getXpNeededForNextLevel(), this.experienceLevel)); } + @Inject(method = "setCamera(Lnet/minecraft/world/entity/Entity;)V", at = @At("HEAD")) + private void spectatorplus$beforeCameraChange(Entity entityToSpectate, CallbackInfo ci) { + final ServerPlayer spectator = (ServerPlayer) (Object) this; + + // If we're changing from spectating a player to something else, clean up + if (spectator.getCamera() instanceof ServerPlayer oldTarget && spectator.getCamera() != entityToSpectate) { + // Clean up any container listeners when stopping spectating a player + ContainerSyncHandler.unsubscribeFromContainer(spectator, oldTarget); + } + } @Inject(method = "setCamera(Lnet/minecraft/world/entity/Entity;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerGamePacketListenerImpl;send(Lnet/minecraft/network/protocol/Packet;)V")) private void spectatorplus$syncToNewSpectator(Entity entityToSpectate, CallbackInfo ci) { if (entityToSpectate instanceof final ServerPlayer target) { @@ -67,6 +78,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)); + InventorySyncHandler.sendPacket(spectator, target); EffectsSyncHandler.onStartSpectating(spectator, target); // Send initial map data patch packet if the target has a map in inventory @@ -130,4 +142,23 @@ private static ClientboundMapItemDataPacket getInitialMapDataPacket(MapId mapId, this.setCamera(entity); } } + + @Inject(method = "openMenu(Lnet/minecraft/world/MenuProvider;)Ljava/util/OptionalInt;", at = @At("RETURN")) + private void spectatorplus$onOpenMenu(CallbackInfoReturnable cir) { + + final ServerPlayer player = (ServerPlayer) (Object) this; + + if (cir.getReturnValue().isPresent() && player.containerMenu != player.inventoryMenu) { + ScreenSyncHandler.syncScreenOpened(player); + } + } + + @Inject(method = "doCloseContainer()V", at = @At("HEAD")) + private void spectatorplus$onCloseContainer(CallbackInfo ci) { + final ServerPlayer player = (ServerPlayer) (Object) this; + + if (player.containerMenu != player.inventoryMenu) { + ScreenSyncHandler.syncScreenClosed(player); + } + } } 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 587735e..80ca8d2 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,8 +1,12 @@ package com.hpfxd.spectatorplus.fabric.sync; +import com.hpfxd.spectatorplus.fabric.sync.handler.ContainerSyncHandler; +import com.hpfxd.spectatorplus.fabric.sync.handler.CursorSyncHandler; import com.hpfxd.spectatorplus.fabric.sync.handler.EffectsSyncHandler; import com.hpfxd.spectatorplus.fabric.sync.handler.HotbarSyncHandler; +import com.hpfxd.spectatorplus.fabric.sync.handler.InventorySyncHandler; import com.hpfxd.spectatorplus.fabric.sync.handler.ScreenSyncHandler; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -16,7 +20,10 @@ public static void init() { HotbarSyncHandler.init(); ScreenSyncHandler.init(); + ContainerSyncHandler.init(); EffectsSyncHandler.init(); + InventorySyncHandler.init(); + CursorSyncHandler.init(); } public static void sendPacket(ServerPlayer serverPlayer, ClientboundSyncPacket packet) { diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/SyncPackets.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/SyncPackets.java index eee14b5..c66eea4 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/SyncPackets.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/SyncPackets.java @@ -1,5 +1,6 @@ package com.hpfxd.spectatorplus.fabric.sync; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundContainerSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundEffectsSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundExperienceSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundFoodSyncPacket; @@ -17,6 +18,7 @@ public static void registerAll() { PayloadTypeRegistry.playC2S().register(ServerboundOpenedInventorySyncPacket.TYPE, ServerboundOpenedInventorySyncPacket.STREAM_CODEC); PayloadTypeRegistry.playC2S().register(ServerboundRequestInventoryOpenPacket.TYPE, ServerboundRequestInventoryOpenPacket.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(ClientboundContainerSyncPacket.TYPE, ClientboundContainerSyncPacket.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(ClientboundExperienceSyncPacket.TYPE, ClientboundExperienceSyncPacket.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(ClientboundFoodSyncPacket.TYPE, ClientboundFoodSyncPacket.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(ClientboundHotbarSyncPacket.TYPE, ClientboundHotbarSyncPacket.STREAM_CODEC); diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ContainerSyncHandler.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ContainerSyncHandler.java new file mode 100644 index 0000000..2adee86 --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ContainerSyncHandler.java @@ -0,0 +1,117 @@ +package com.hpfxd.spectatorplus.fabric.sync.handler; + +import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundContainerSyncPacket; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundScreenSyncPacket; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ContainerSyncHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(ContainerSyncHandler.class); + + // Map to track container listeners for each spectator + // Key: spectator UUID, Value: listener instance + private static final Map containerListeners = new HashMap<>(); + + public static void init() { + // Clean up listeners when a player disconnects. + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + containerListeners.remove(handler.getPlayer().getUUID()); + }); + } + + /** + * Unsubscribe a spectator from a target player's container. + * This removes the container listener to prevent memory leaks and stale updates. + */ + public static void unsubscribeFromContainer(ServerPlayer spectator, ServerPlayer target) { + ContainerListener oldListener = containerListeners.remove(spectator.getUUID()); + if (oldListener != null) { + target.containerMenu.removeSlotListener(oldListener); + } + } + + public static void sendPacket(ServerPlayer spectator, ServerPlayer target) { + InventorySyncHandler.sendPacket(spectator, target); + var containerMenu = target.containerMenu; + if (containerMenu == target.inventoryMenu) { + return; // No container open, just player inventory + } + ItemStack[] containerItems = extractContainerItems(containerMenu); + ServerSyncController.broadcastPacketToSpectators(target, new ClientboundContainerSyncPacket( + target.getUUID(), + containerMenu.getType(), + containerItems.length, + containerItems + )); + + int flags = 0; // Container screen (not survival inventory) + flags |= (1 << 2); // Has dummy slots flag for container screens + ServerSyncController.broadcastPacketToSpectators(target, new ClientboundScreenSyncPacket(target.getUUID(), flags)); + + containerMenu.addSlotListener(createContainerListener(spectator, target, containerListeners)); + + spectator.connection.send(new ClientboundOpenScreenPacket( + containerMenu.containerId, + containerMenu.getType(), + target.getDisplayName() + )); + } + + private static ItemStack[] extractContainerItems(AbstractContainerMenu menu) { + int containerSize = menu.slots.size() - 36; // Subtract player inventory slots + ItemStack[] containerItems = new ItemStack[containerSize]; + + for (int i = 0; i < containerSize; i++) { + var slot = menu.getSlot(i); + containerItems[i] = slot.getItem().copy(); + } + + return containerItems; + } + + public static ContainerListener createContainerListener(ServerPlayer spectator, ServerPlayer target, Map listenerMap) { + ContainerListener listener = new ContainerListener() { + @Override + public void slotChanged(@NotNull AbstractContainerMenu menu, int slotIndex, @NotNull ItemStack stack) { + // Only sync container slots, not player inventory slots + if (slotIndex < menu.slots.size() - 36 && ServerSyncController.getSpectators(target).contains(spectator)) { + ItemStack[] update = new ItemStack[menu.slots.size() - 36]; + for (int i = 0; i < update.length; i++) { + if (i == slotIndex) { + update[i] = stack.copy(); + } else { + // For efficiency, only send changed slot + update[i] = null; + } + } + ServerSyncController.sendPacket(spectator, new ClientboundContainerSyncPacket( + target.getUUID(), + menu.getType(), + update.length, + update + )); + } + } + + @Override + public void dataChanged(@NotNull AbstractContainerMenu menu, int dataSlot, int value) { + // Handle furnace progress, brewing stand progress, etc. + // For now, we don't need to sync this separately + } + }; + listenerMap.put(spectator.getUUID(), listener); + return listener; + } +} diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/CursorSyncHandler.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/CursorSyncHandler.java new file mode 100644 index 0000000..370cac3 --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/CursorSyncHandler.java @@ -0,0 +1,75 @@ +package com.hpfxd.spectatorplus.fabric.sync.handler; + +import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundScreenCursorSyncPacket; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Handles synchronization of cursor items (items being dragged) to spectators + */ +public class CursorSyncHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(CursorSyncHandler.class); + private static final Map playerCursors = new HashMap<>(); + + public static void init() { + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> + playerCursors.remove(handler.getPlayer().getUUID())); + } + + /** + * Called when a player's cursor item changes + */ + public static void onCursorChanged(ServerPlayer player, ItemStack newCursor) { + onCursorChanged(player, newCursor, -1); + } + + /** + * Called when a player's cursor item changes + */ + public static void onCursorChanged(ServerPlayer player, ItemStack newCursor, int originSlot) { + ItemStack oldCursor = playerCursors.get(player.getUUID()); + if (oldCursor == null) { + oldCursor = ItemStack.EMPTY; + } + if (newCursor == null) { + newCursor = ItemStack.EMPTY; + } + + if (!ItemStack.isSameItem(oldCursor, newCursor)) { + playerCursors.put(player.getUUID(), newCursor.copy()); + + ClientboundScreenCursorSyncPacket packet = new ClientboundScreenCursorSyncPacket( + player.getUUID(), + newCursor, + originSlot + ); + + ServerSyncController.broadcastPacketToSpectators(player, packet); + } + } + + /** + * Send initial cursor data to a spectator + */ + public static void sendPacket(ServerPlayer spectator, ServerPlayer target) { + ItemStack cursor = playerCursors.getOrDefault(target.getUUID(), ItemStack.EMPTY); + + if (!cursor.isEmpty()) { + ClientboundScreenCursorSyncPacket packet = new ClientboundScreenCursorSyncPacket( + target.getUUID(), + cursor, + -1 + ); + + ServerSyncController.sendPacket(spectator, packet); + } + } +} diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/InventorySyncHandler.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/InventorySyncHandler.java new file mode 100644 index 0000000..52081b8 --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/InventorySyncHandler.java @@ -0,0 +1,126 @@ +package com.hpfxd.spectatorplus.fabric.sync.handler; + +import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundHotbarSyncPacket; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundInventorySyncPacket; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.IntStream; + +public class InventorySyncHandler { + private static final Map playerInventories = new HashMap<>(); + private static int tickCounter = 0; + private static final int SYNC_INTERVAL = 5; // Sync every 5 ticks (4 times per second) + + public static void init() { + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> + playerInventories.remove(handler.getPlayer().getUUID())); + ServerTickEvents.END_SERVER_TICK.register(InventorySyncHandler::tick); + } + + public static void tick(MinecraftServer server) { + tickCounter++; + if (tickCounter % SYNC_INTERVAL != 0) { + return; // Skip this tick + } + + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (ServerSyncController.getSpectators(player).isEmpty()) { + continue; + } + syncPlayerInventory(player); + } + } + + private static void syncPlayerInventory(ServerPlayer player) { + final ItemStack[] slots = playerInventories.computeIfAbsent(player.getUUID(), k -> { + final ItemStack[] arr = new ItemStack[ClientboundInventorySyncPacket.ITEMS_LENGTH]; + Arrays.fill(arr, ItemStack.EMPTY); + return arr; + }); + + //as hotbar will change more than rest of inventory, we track it separately to avoid sending full inventory when only hotbar changes + final ItemStack[] currentSlots = extractPlayerInventory(player); + final ItemStack[] inventorySendSlots = new ItemStack[ClientboundInventorySyncPacket.ITEMS_LENGTH]; + final ItemStack[] hotbarSendSlots = new ItemStack[ClientboundHotbarSyncPacket.ITEMS_LENGTH]; + + boolean updatedHotbar = false; + boolean updatedInventory = false; + + // Main inventory (0-35) including hotbar (0-8) + for (int i = 0; i < currentSlots.length; i++) { + if (!ItemStack.matches(currentSlots[i], slots[i])) { + slots[i] = currentSlots[i].copy(); + inventorySendSlots[i] = currentSlots[i]; + updatedInventory = true; + + // 热键栏是槽位 0-8 + if (i < ClientboundHotbarSyncPacket.ITEMS_LENGTH) { + hotbarSendSlots[i] = currentSlots[i]; + updatedHotbar = true; + } + } + } + + if (updatedInventory) { + ScreenSyncHandler.updatePlayerInventory(player, inventorySendSlots); + } + + if (updatedHotbar) { + ServerSyncController.broadcastPacketToSpectators(player, + new ClientboundHotbarSyncPacket(player.getUUID(), hotbarSendSlots)); + } + } + + //this for send full inventory packet immediately + public static void sendPacket(ServerPlayer spectator, ServerPlayer target) { + final ItemStack[] slots = extractPlayerInventory(target); + ServerSyncController.broadcastPacketToSpectators(target, new ClientboundInventorySyncPacket(target.getUUID(), slots)); + } + + private static ItemStack[] extractPlayerInventory(ServerPlayer player) { + final Inventory inventory = player.getInventory(); + final ItemStack[] slots = new ItemStack[ClientboundInventorySyncPacket.ITEMS_LENGTH]; + + // Main inventory (0-35) + IntStream.range(0, 36).forEach(i -> { + ItemStack item = inventory.getItem(i); + slots[i] = item; + }); + + // Armor (36-39) + IntStream.range(36, 40).forEach(i -> { + ItemStack item = inventory.getItem(i); + slots[i] = item; + }); + + // Offhand slot (40) + ItemStack offhand = inventory.getItem(40); + slots[40] = offhand; + + return slots; + } + + public static void onPlayerDisconnect(UUID playerId) { + playerInventories.remove(playerId); + } + + /** + * Force an immediate inventory sync for a player, bypassing the tick counter + * Used for critical inventory changes that need immediate synchronization + */ + public static void forceSyncPlayer(ServerPlayer player) { + if (!ServerSyncController.getSpectators(player).isEmpty()) { + syncPlayerInventory(player); + } + } +} diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ScreenSyncHandler.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ScreenSyncHandler.java index 4fa0e46..cd3f162 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ScreenSyncHandler.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/handler/ScreenSyncHandler.java @@ -1,18 +1,113 @@ package com.hpfxd.spectatorplus.fabric.sync.handler; +import com.hpfxd.spectatorplus.fabric.sync.ServerSyncController; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundInventorySyncPacket; +import com.hpfxd.spectatorplus.fabric.sync.packet.ClientboundScreenSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ServerboundOpenedInventorySyncPacket; import com.hpfxd.spectatorplus.fabric.sync.packet.ServerboundRequestInventoryOpenPacket; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ScreenSyncHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(ScreenSyncHandler.class); + public static void init() { ServerPlayNetworking.registerGlobalReceiver(ServerboundRequestInventoryOpenPacket.TYPE, ScreenSyncHandler::handle); ServerPlayNetworking.registerGlobalReceiver(ServerboundOpenedInventorySyncPacket.TYPE, ScreenSyncHandler::handle); } private static void handle(ServerboundRequestInventoryOpenPacket packet, ServerPlayNetworking.Context ctx) { + final var spectator = ctx.player(); + final var target = ctx.server().getPlayerList().getPlayer(packet.playerId()); + if (target == null) { + return; + } + // Send screen sync packet to indicate survival inventory + int flags = 1; // Survival inventory flag + flags |= (1 << 1); // Client requested flag + // Send full inventory data + InventorySyncHandler.sendPacket(spectator, target); + + ServerSyncController.broadcastPacketToSpectators(target, new ClientboundScreenSyncPacket(target.getUUID(), flags)); + } private static void handle(ServerboundOpenedInventorySyncPacket packet, ServerPlayNetworking.Context ctx) { + final var player = ctx.player(); + if (packet.isOpened()) { + syncScreenOpened(player); + } else { + syncScreenClosed(player); + } + } + + /** + * Called when a player opens their inventory or any container screen. + * This method syncs the appropriate screen to all spectators who are viewing this player. + * + * @param target The player who opened their inventory/container + */ + public static void syncScreenOpened(ServerPlayer target) { + var spectators = ServerSyncController.getSpectators(target); + if (spectators.isEmpty()) { + return; + } + + for (ServerPlayer spectator : spectators) { + // Determine what type of screen the target player has open + if (target.containerMenu != target.inventoryMenu) { + // Player has a container open (chest, crafting table, furnace, etc.) + ContainerSyncHandler.sendPacket(spectator, target); + + } else { + // Player has their regular inventory open (survival/creative inventory) + syncPlayerInventoryScreen(spectator, target); + } + } + } + + /** + * Called when a player closes their inventory or any container screen. + * This method closes the synced screen for all spectators who are viewing this player. + * + * @param target The player who closed their inventory/container + */ + public static void syncScreenClosed(ServerPlayer target) { + var spectators = ServerSyncController.getSpectators(target); + if (spectators.isEmpty()) { + return; + } + + for (ServerPlayer spectator : spectators) { + ContainerSyncHandler.unsubscribeFromContainer(spectator, target); + + // Send screen sync packet to indicate inventory/container was closed + int flags = 0; // No flags means close screen + flags |= (1 << 3); // Screen closed flag + ServerSyncController.broadcastPacketToSpectators(target, new ClientboundScreenSyncPacket(target.getUUID(), flags)); + } + } + + public static void updatePlayerInventory(ServerPlayer player, ItemStack[] inventorySendSlots) { + ServerSyncController.broadcastPacketToSpectators(player, new ClientboundInventorySyncPacket(player.getUUID(), inventorySendSlots)); + } + + /** + * Syncs a player's regular inventory screen (survival/creative inventory) to a spectator. + * This is used when the target player has opened their own inventory. + * + * @param spectator The spectator to sync the screen to + * @param target The player whose inventory screen should be synced + */ + private static void syncPlayerInventoryScreen(ServerPlayer spectator, ServerPlayer target) { + // Send screen sync packet for player inventory (survival inventory) + int flags = 1; // Survival inventory flag + // Send the target's inventory data to the spectator + InventorySyncHandler.sendPacket(spectator, target); + + ServerSyncController.broadcastPacketToSpectators(target, new ClientboundScreenSyncPacket(target.getUUID(), flags)); } } diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundContainerSyncPacket.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundContainerSyncPacket.java new file mode 100644 index 0000000..f28225d --- /dev/null +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundContainerSyncPacket.java @@ -0,0 +1,58 @@ +package com.hpfxd.spectatorplus.fabric.sync.packet; + +import com.hpfxd.spectatorplus.fabric.sync.ClientboundSyncPacket; +import com.hpfxd.spectatorplus.fabric.sync.CustomPacketCodecs; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public record ClientboundContainerSyncPacket( + UUID playerId, + MenuType containerType, + int containerSize, + ItemStack[] items +) implements ClientboundSyncPacket { + public static final StreamCodec STREAM_CODEC = + CustomPacketPayload.codec(ClientboundContainerSyncPacket::write, ClientboundContainerSyncPacket::new); + public static final CustomPacketPayload.Type TYPE = + new CustomPacketPayload.Type<>(Identifier.parse("spectatorplus:container_sync")); + private static final String PERMISSION = "spectatorplus.sync.screen"; + + public ClientboundContainerSyncPacket(RegistryFriendlyByteBuf buf) { + this( + buf.readUUID(), + buf.readById(id -> { + MenuType type = BuiltInRegistries.MENU.byId(id); + return type != null ? type : MenuType.GENERIC_9x3; + }), + buf.readVarInt(), + CustomPacketCodecs.readItems(buf) + ); + } + + public void write(RegistryFriendlyByteBuf buf) { + buf.writeUUID(this.playerId); + buf.writeById(BuiltInRegistries.MENU::getId, this.containerType); + buf.writeVarInt(this.containerSize); + CustomPacketCodecs.writeItems(buf, this.items); + } + + @Override + public @NotNull Type type() { + return TYPE; + } + + @Override + public boolean canSend(ServerPlayer receiver) { + return Permissions.check(receiver, PERMISSION, true); + } +} diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundHotbarSyncPacket.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundHotbarSyncPacket.java index e39b7d0..db3a2d1 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundHotbarSyncPacket.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundHotbarSyncPacket.java @@ -20,6 +20,7 @@ public record ClientboundHotbarSyncPacket( public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ClientboundHotbarSyncPacket::write, ClientboundHotbarSyncPacket::new); public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.parse("spectatorplus:hotbar_sync")); static final String PERMISSION = "spectatorplus.sync.hotbar"; + public static final int ITEMS_LENGTH = 9; public static ClientboundHotbarSyncPacket initializing(ServerPlayer target) { final ItemStack[] items = new ItemStack[9]; diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenCursorSyncPacket.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenCursorSyncPacket.java index 157a972..958ad88 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenCursorSyncPacket.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenCursorSyncPacket.java @@ -2,6 +2,7 @@ import com.hpfxd.spectatorplus.fabric.sync.ClientboundSyncPacket; import com.hpfxd.spectatorplus.fabric.sync.CustomPacketCodecs; +import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; @@ -19,6 +20,7 @@ public record ClientboundScreenCursorSyncPacket( ) implements ClientboundSyncPacket { public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ClientboundScreenCursorSyncPacket::write, ClientboundScreenCursorSyncPacket::new); public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.parse("spectatorplus:screen_cursor_sync")); + private static final String PERMISSION = "spectatorplus.sync.screen"; public ClientboundScreenCursorSyncPacket(RegistryFriendlyByteBuf buf) { this(buf.readUUID(), CustomPacketCodecs.readItem(buf), buf.readByte()); @@ -37,6 +39,6 @@ public void write(RegistryFriendlyByteBuf buf) { @Override public boolean canSend(ServerPlayer receiver) { - return true; + return Permissions.check(receiver, PERMISSION, true); } } diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenSyncPacket.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenSyncPacket.java index cb0464d..be7f51b 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenSyncPacket.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ClientboundScreenSyncPacket.java @@ -1,6 +1,7 @@ package com.hpfxd.spectatorplus.fabric.sync.packet; import com.hpfxd.spectatorplus.fabric.sync.ClientboundSyncPacket; +import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; @@ -16,6 +17,7 @@ public record ClientboundScreenSyncPacket( ) implements ClientboundSyncPacket { public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ClientboundScreenSyncPacket::write, ClientboundScreenSyncPacket::new); public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.parse("spectatorplus:screen_sync")); + private static final String PERMISSION = "spectatorplus.sync.screen"; public ClientboundScreenSyncPacket(FriendlyByteBuf buf) { this(buf.readUUID(), buf.readByte()); @@ -38,6 +40,10 @@ public boolean hasDummySlots() { return (this.flags >> 2 & 1) == 1; } + public boolean isScreenClosed() { + return (this.flags >> 3 & 1) == 1; + } + @Override public @NotNull Type type() { return TYPE; @@ -45,6 +51,6 @@ public boolean hasDummySlots() { @Override public boolean canSend(ServerPlayer receiver) { - return true; + return Permissions.check(receiver, PERMISSION, true); } } diff --git a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ServerboundOpenedInventorySyncPacket.java b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ServerboundOpenedInventorySyncPacket.java index e6aea33..087d6e6 100644 --- a/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ServerboundOpenedInventorySyncPacket.java +++ b/fabric/src/main/java/com/hpfxd/spectatorplus/fabric/sync/packet/ServerboundOpenedInventorySyncPacket.java @@ -5,18 +5,29 @@ import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.Identifier; - +/* + * Sent by the client to the server to synchronize the opened inventory state of the player. + */ public final class ServerboundOpenedInventorySyncPacket implements ServerboundSyncPacket { public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ServerboundOpenedInventorySyncPacket::write, ServerboundOpenedInventorySyncPacket::new); public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.parse("spectatorplus:opened_inventory_sync")); - public ServerboundOpenedInventorySyncPacket() { + private final boolean isOpened; + + public ServerboundOpenedInventorySyncPacket(boolean isOpened) { + this.isOpened = isOpened; } public ServerboundOpenedInventorySyncPacket(FriendlyByteBuf buf) { + this.isOpened = buf.readBoolean(); } public void write(FriendlyByteBuf buf) { + buf.writeBoolean(this.isOpened); + } + + public boolean isOpened() { + return this.isOpened; } @Override