Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ItemStack> 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;
}

Expand All @@ -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
Expand All @@ -52,5 +64,6 @@ public boolean stillValid(Player player) {

@Override
public void clearContent() {
this.items.clear();
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearContent() calls this.items.clear(), which shrinks the backing list to size 0. This breaks the Container contract (and makes getContainerSize() change), likely leading to index errors in menus. Instead, keep the list size and set all entries to ItemStack.EMPTY (or reinitialize items to the original size).

Suggested change
this.items.clear();
for (int i = 0; i < this.items.size(); i++) {
this.items.set(i, ItemStack.EMPTY);
}

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
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;
import net.minecraft.client.gui.components.events.GuiEventListener;
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;

Expand All @@ -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));
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,44 @@
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;
import net.minecraft.client.Minecraft;
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();
Expand All @@ -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();
Expand All @@ -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++) {
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Loading