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
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'

- name: Setup Gradle
Expand All @@ -42,10 +42,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'

- name: Setup Gradle
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ bin/

### Mac OS ###
.DS_Store

### SpectatorPlus ###
fabric/sources
17 changes: 17 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Minecraft 26.1 Source Code

This repository contains an extracted copy of the decompiled Minecraft 26.1 source code under `fabric/sources/26.1/`.

## What is this?
When updating mods to newer versions of Minecraft (e.g., migrating to 26.1), the method signatures, fields, and class hierarchies in the underlying game code frequently change. This directory acts as a local, fully searchable repository of the vanilla Minecraft codebase (both common and client-side logic).

## How to use it
As an AI Agent or developer, you can use these sources to investigate API changes and resolve compilation errors.

**Best Practices for Agents:**
1. **Search for references:** If you encounter an `InvalidInjectionException` or a `NoSuchMethodError`, use the `grep_search` tool within `fabric/sources/26.1/` to find the target class and see how the method signature has changed.
* Example: `grep_search(SearchPath="fabric/sources/26.1", Query="slotClicked", MatchPerLine=true)`
2. **Examine classes:** Once you find the target file, use the `view_file` tool to inspect the exact parameters, fields, and implementations to properly configure your Mixins or API calls.
3. **Trace logic:** You can trace how vanilla Minecraft implements specific mechanics by searching interfaces or base classes within this directory to understand the intended usage in the current version.

*Note: The `fabric/sources/` directory is ignored by Git to prevent bloating the system repository.*
28 changes: 13 additions & 15 deletions fabric/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
plugins {
id("fabric-loom") version "1.14-SNAPSHOT"
id("net.fabricmc.fabric-loom") version "1.15.5"
id("spectatorplus.platform")
}

description = "A Fabric mod that improves spectator mode by showing the hotbar, health, and held item of the spectated player"

repositories {
maven("https://maven.parchmentmc.org")
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven("https://maven.shedaniel.me/")
maven("https://maven.terraformersmc.com/releases/")
Expand Down Expand Up @@ -41,20 +40,21 @@ loom {

dependencies {
minecraft("com.mojang:minecraft:${property("minecraft_version")}")
modImplementation("net.fabricmc:fabric-loader:${property("loader_version")}")
mappings(loom.layered {
officialMojangMappings()
parchment("org.parchmentmc.data:parchment-${property("parchment_minecraft_version")}:${property("parchment_version")}@zip")
})
modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_version")}")
implementation("net.fabricmc:fabric-loader:${property("loader_version")}")

include(modImplementation("me.lucko:fabric-permissions-api:${property("fabric_permissions_api_version")}")!!)
// Fabric API
implementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_version")}")

modImplementation("me.shedaniel.cloth:cloth-config-fabric:${property("cloth_config_version")}") {
exclude("net.fabricmc.fabric-api")
include(implementation("me.lucko:fabric-permissions-api:${property("fabric_permissions_api_version")}")!!)

implementation("me.shedaniel.cloth:cloth-config-fabric:${property("cloth_config_version")}") {
exclude(group = "net.fabricmc.fabric-api")
}

modImplementation("com.terraformersmc:modmenu:${property("modmenu_version")}")
implementation("com.terraformersmc:modmenu:${property("modmenu_version")}")

include(implementation("io.github.llamalad7:mixinextras-fabric:${property("mixinextras_version")}")!!)
annotationProcessor("io.github.llamalad7:mixinextras-fabric:${property("mixinextras_version")}")
}

tasks {
Expand All @@ -74,7 +74,5 @@ tasks {
from("../LICENSE")
}

remapJar {
archiveVersion = getByName<Jar>("jar").archiveVersion
}

}
17 changes: 7 additions & 10 deletions fabric/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
minecraft_version=1.21.11
yarn_mappings=1.21.11+build.3
loader_version=0.18.4
minecraft_version=26.1
loader_version=0.18.5

# Fabric API
fabric_version=0.140.2+1.21.11
fabric_version=0.144.4+26.1

parchment_minecraft_version=1.21.11
parchment_version=2025.12.20

fabric_permissions_api_version=0.6.1
cloth_config_version=21.11.153
modmenu_version=17.0.0-beta.1
fabric_permissions_api_version=0.7.0
cloth_config_version=26.1.154
modmenu_version=18.0.0-alpha.8
mixinextras_version=0.5.3
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.hpfxd.spectatorplus.fabric.client.mixin.SpectatorGuiAccessor;
import com.hpfxd.spectatorplus.fabric.client.mixin.SpectatorMenuAccessor;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.minecraft.ChatFormatting;
import net.minecraft.util.Util;
import net.minecraft.client.KeyMapping;
Expand Down Expand Up @@ -37,19 +37,19 @@ public class SpectatorKeybinds {
private static UUID targetCursorId;

public static void init() {
CLOSEST_PLAYER = KeyBindingHelper.registerKeyBinding(new KeyMapping(
CLOSEST_PLAYER = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.spectatorplus.closestPlayer",
GLFW.GLFW_KEY_UP,
KeyMapping.Category.MISC
));

NEXT_PLAYER = KeyBindingHelper.registerKeyBinding(new KeyMapping(
NEXT_PLAYER = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.spectatorplus.nextPlayer",
GLFW.GLFW_KEY_RIGHT,
KeyMapping.Category.MISC
));

PREVIOUS_PLAYER = KeyBindingHelper.registerKeyBinding(new KeyMapping(
PREVIOUS_PLAYER = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.spectatorplus.previousPlayer",
GLFW.GLFW_KEY_LEFT,
KeyMapping.Category.MISC
Expand All @@ -65,10 +65,10 @@ private static void tick(Minecraft mc) {

if (nearest != null) {
setTarget(mc, nearest.getUUID());
mc.player.displayClientMessage(Component.translatable("spectatorplus.target.now-spectating", Component.empty().append(nearest.getDisplayName())
.withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY), true);
mc.player.sendOverlayMessage(Component.translatable("spectatorplus.target.now-spectating", Component.empty().append(nearest.getDisplayName())
.withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY));
} else {
mc.player.displayClientMessage(Component.translatable("spectatorplus.target.no-closest-player").withStyle(ChatFormatting.RED), true);
mc.player.sendOverlayMessage(Component.translatable("spectatorplus.target.no-closest-player").withStyle(ChatFormatting.RED));
}
}

Expand All @@ -87,10 +87,10 @@ private static void targetNext(Minecraft mc, int shift) {

if (target != null && !target.getProfile().id().equals(mc.getCameraEntity().getUUID())) {
setTarget(mc, target.getProfile().id());
mc.player.displayClientMessage(Component.translatable("spectatorplus.target.now-spectating", Component.empty().append(mc.gui.getTabList().getNameForDisplay(target))
.withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY), true);
mc.player.sendOverlayMessage(Component.translatable("spectatorplus.target.now-spectating", Component.empty().append(mc.gui.getTabList().getNameForDisplay(target))
.withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY));
} else {
mc.player.displayClientMessage(Component.translatable("spectatorplus.target.no-player").withStyle(ChatFormatting.RED), true);
mc.player.sendOverlayMessage(Component.translatable("spectatorplus.target.no-player").withStyle(ChatFormatting.RED));
}
}

Expand Down
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 @@ -16,7 +16,7 @@
public class ExperienceBarRendererMixin {
@Shadow @Final private Minecraft minecraft;

@Redirect(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getXpNeededForNextLevel()I"))
@Redirect(method = "extractBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getXpNeededForNextLevel()I"))
private int spectatorplus$showSyncedExperienceBar(LocalPlayer instance) {
if (ClientSyncController.syncData != null && ClientSyncController.syncData.experienceLevel != -1 && SpecUtil.getCameraPlayer(this.minecraft) != null) {
// If experienceNeededForNextLevel is 0, return 1 to ensure the bar renders
Expand All @@ -26,7 +26,7 @@ public class ExperienceBarRendererMixin {
return instance.getXpNeededForNextLevel();
}

@Redirect(method = "renderBackground", at = @At(value = "FIELD", target = "Lnet/minecraft/client/player/LocalPlayer;experienceProgress:F", opcode = Opcodes.GETFIELD))
@Redirect(method = "extractBackground", at = @At(value = "FIELD", target = "Lnet/minecraft/client/player/LocalPlayer;experienceProgress:F", opcode = Opcodes.GETFIELD))
private float spectatorplus$showSyncedExperienceProgress(LocalPlayer instance) {
if (ClientSyncController.syncData != null && ClientSyncController.syncData.experienceLevel != -1 && SpecUtil.getCameraPlayer(this.minecraft) != null) {
return ClientSyncController.syncData.experienceProgress;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,23 @@
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.ClientAvatarState;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.InteractionHand;
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;
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;

@Mixin(GameRenderer.class)
Expand All @@ -37,9 +35,6 @@ public abstract class GameRendererMixin {
private Minecraft minecraft;
@Shadow
@Final
private LightTexture lightTexture;
@Shadow
@Final
private RenderBuffers renderBuffers;
@Shadow
@Final
Expand Down Expand Up @@ -80,7 +75,7 @@ public abstract class GameRendererMixin {
// }

@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,
public void spectatorplus$renderItemInHand(CameraRenderState cameraState, float partialTicks, Matrix4fc projectionMatrix,
CallbackInfo ci, @Local PoseStack poseStackIn) {
if (SpectatorClientMod.config.renderArms && this.minecraft.player != null
&& this.minecraft.options.getCameraType().isFirstPerson() && !this.minecraft.options.hideGui) {
Expand Down Expand Up @@ -201,18 +196,18 @@ private static ItemInHandRenderer.HandRenderSelection evaluateWhichHandsToRender
}
}

@Redirect(method = "bobView", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/entity/ClientAvatarState;getBackwardsInterpolatedWalkDistance(F)F"))
float spectatorplus$modifyBobWalkDist(ClientAvatarState instance, float partialTick) {
@ModifyExpressionValue(method = "bobView", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/state/level/CameraEntityRenderState;backwardsInterpolatedWalkDistance:F"))
float spectatorplus$modifyBobWalkDist(float original) {
if (minecraft.getCameraEntity() == this.minecraft.player)
return instance.getBackwardsInterpolatedWalkDistance(partialTick);
return original;
float f = this.walkDist - this.walkDistO;
return -(this.walkDist + f * partialTick);
return -(this.walkDist + f);
}

@Redirect(method = "bobView", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/entity/ClientAvatarState;getInterpolatedBob(F)F"))
float spectatorplus$modifyBobValue(ClientAvatarState instance, float partialTick) {
@ModifyExpressionValue(method = "bobView", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/state/level/CameraEntityRenderState;bob:F"))
float spectatorplus$modifyBobValue(float original) {
if (minecraft.getCameraEntity() == this.minecraft.player)
return instance.getInterpolatedBob(partialTick);
return Mth.lerp(partialTick, this.bobO, this.bob);
return original;
return Mth.lerp(1.0F, this.bobO, this.bob);
}
}
Loading