feat: Add interactive component movers + fix: move coordinates

This commit is contained in:
Linnea Gräf
2025-06-22 14:51:38 +02:00
parent db0174ca8c
commit 4cfba87783
7 changed files with 261 additions and 109 deletions

View File

@@ -2,7 +2,8 @@ package moe.nea.firmament.features.texturepack
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.minecraft.client.MinecraftClient
import kotlinx.serialization.Transient
import net.minecraft.client.font.TextRenderer
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.HandledScreen
@@ -17,7 +18,9 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.features.texturepack.CustomTextColors.cache
import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.CENTER
import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.LEFT
import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.RIGHT
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ErrorUtil.intoCatch
import moe.nea.firmament.util.IdentifierSerializer
@@ -29,8 +32,18 @@ object CustomScreenLayouts : SinglePreparationResourceReloader<List<CustomScreen
val predicates: Preds,
val background: BackgroundReplacer? = null,
val slots: List<SlotReplacer> = listOf(),
val playerTitle: TitleReplacer = TitleReplacer(),
val containerTitle: TitleReplacer = TitleReplacer()
val playerTitle: TitleReplacer? = null,
val containerTitle: TitleReplacer? = null,
val repairCostTitle: TitleReplacer? = null,
val nameField: ComponentMover? = null,
)
@Serializable
data class ComponentMover(
val x: Int,
val y: Int,
val width: Int? = null,
val height: Int? = null,
)
@Serializable
@@ -97,49 +110,48 @@ object CustomScreenLayouts : SinglePreparationResourceReloader<List<CustomScreen
enum class Alignment {
@SerialName("left")
LEFT,
@SerialName("center")
CENTER,
@SerialName("right")
RIGHT
}
@Serializable
data class TitleReplacer(
val x: Int = 0,
val y: Int = 0,
val x: Int? = null,
val y: Int? = null,
val align: Alignment = Alignment.LEFT,
val replace: String? = null
)
) {
@Transient
val replacedText: Text? = replace?.let(Text::literal)
fun alignText(text: Text, x: Int, width: Int): Int {
var currentText = mapReplaceText(text)
val align = if (currentText.string == "Inventory") activeScreenOverride?.playerTitle?.align ?: Alignment.LEFT
else activeScreenOverride?.containerTitle?.align ?: Alignment.LEFT
fun replaceText(text: Text): Text {
if (replacedText != null) return replacedText
return text
}
val textWidth = MinecraftClient.getInstance().textRenderer.getWidth(Text.literal(currentText.string))
fun replaceY(y: Int): Int {
return this.y ?: y
}
return when (align) {
Alignment.LEFT -> x
Alignment.CENTER -> x + (width - textWidth) / 2
Alignment.RIGHT -> x + (width - textWidth)
fun replaceX(font: TextRenderer, text: Text, x: Int): Int {
val baseX = this.x ?: x
return baseX + when (this.align) {
LEFT -> 0
CENTER -> -font.getWidth(text) / 2
RIGHT -> -font.getWidth(text)
}
}
fun mapReplaceText(text: Text): Text {
val replaceText = if (text.string == "Inventory") activeScreenOverride?.playerTitle?.replace ?: null else activeScreenOverride?.containerTitle?.replace ?: null
if (replaceText == null) return text
return Text.literal(replaceText)
/**
* Not technically part of the package, but it does allow for us to later on seamlessly integrate a color option into this class as well
*/
fun replaceColor(text: Text, color: Int): Int {
return CustomTextColors.mapTextColor(text, color)
}
fun mapTextToX(text: Text, x: Int): Int {
return x + if (text.string == "Inventory") activeScreenOverride?.playerTitle?.x
?: 0 else activeScreenOverride?.containerTitle?.x ?: 0
}
fun mapTextToY(text: Text, y: Int): Int {
return y + if (text.string == "Inventory") activeScreenOverride?.playerTitle?.y
?: 0 else activeScreenOverride?.containerTitle?.y ?: 0
}
@@ -175,6 +187,16 @@ object CustomScreenLayouts : SinglePreparationResourceReloader<List<CustomScreen
@get:JvmStatic
var activeScreenOverride = null as CustomScreenLayout?
val DO_NOTHING_TEXT_REPLACER = TitleReplacer()
@JvmStatic
fun <T>getMover(selector: (CustomScreenLayout)-> (T?)) =
activeScreenOverride?.let(selector)
@JvmStatic
fun getTextMover(selector: (CustomScreenLayout) -> (TitleReplacer?)) =
getMover(selector) ?: DO_NOTHING_TEXT_REPLACER
@Subscribe
fun onScreenOpen(event: ScreenChangeEvent) {
if (!CustomSkyBlockTextures.TConfig.allowLayoutChanges) {

View File

@@ -1,51 +0,0 @@
package moe.nea.firmament.mixins.custommodels;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import moe.nea.firmament.features.texturepack.CustomTextColors;
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.AnvilScreen;
import net.minecraft.client.gui.screen.ingame.BeaconScreen;
import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.ingame.MerchantScreen;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin({HandledScreen.class, InventoryScreen.class, CreativeInventoryScreen.class, MerchantScreen.class,
AnvilScreen.class, BeaconScreen.class})
public class ReplaceTextColorInHandledScreen {
// To my future self: double check those mixins, but don't be too concerned about errors. Some of the wrapopertions
// only apply in some of the specified subclasses.
@WrapOperation(
method = "drawForeground",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
expect = 0,
require = 0)
private int replaceTextColorWithVariableShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
int width = ((AccessorHandledScreen) this).getBackgroundWidth_Firmament();
return original.call(instance, textRenderer, CustomScreenLayouts.INSTANCE.mapReplaceText(text), CustomScreenLayouts.INSTANCE.alignText(text, CustomScreenLayouts.INSTANCE.mapTextToX(text, x), width), CustomScreenLayouts.INSTANCE.mapTextToY(text, y), CustomTextColors.INSTANCE.mapTextColor(text, color), shadow);
}
@WrapOperation(
method = "drawForeground",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;III)I"),
expect = 0,
require = 0)
private int replaceTextColorWithShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation<Integer> original) {
int width = ((AccessorHandledScreen) this).getBackgroundWidth_Firmament();
return original.call(instance, textRenderer, CustomScreenLayouts.INSTANCE.mapReplaceText(text), CustomScreenLayouts.INSTANCE.alignText(text, CustomScreenLayouts.INSTANCE.mapTextToX(text, x), width), CustomScreenLayouts.INSTANCE.mapTextToY(text, y), CustomTextColors.INSTANCE.mapTextColor(text, color));
}
}

View File

@@ -0,0 +1,55 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.AnvilScreen;
import net.minecraft.client.gui.screen.ingame.ForgingScreen;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.AnvilScreenHandler;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(AnvilScreen.class)
public abstract class ReplaceAnvilScreen extends ForgingScreen<AnvilScreenHandler> {
@Shadow
private TextFieldWidget nameField;
public ReplaceAnvilScreen(AnvilScreenHandler handler, PlayerInventory playerInventory, Text title, Identifier texture) {
super(handler, playerInventory, title, texture);
}
@Inject(method = "setup", at = @At("TAIL"))
private void moveNameField(CallbackInfo ci) {
var override = CustomScreenLayouts.getMover(CustomScreenLayouts.CustomScreenLayout::getNameField);
if (override == null) return;
int baseX = (this.width - this.backgroundWidth) / 2;
int baseY = (this.height - this.backgroundHeight) / 2;
nameField.setX(baseX + override.getX());
nameField.setY(baseY + override.getY());
if (override.getWidth() != null)
nameField.setWidth(override.getWidth());
if (override.getHeight() != null)
nameField.setHeight(override.getHeight());
}
@WrapOperation(method = "drawForeground",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;III)I"),
allow = 1)
private int onDrawRepairCost(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation<Integer> original) {
var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getRepairCostTitle);
return original.call(instance, textRenderer,
textOverride.replaceText(text),
textOverride.replaceX(textRenderer, text, x),
textOverride.replaceY(y),
textOverride.replaceColor(text, color));
}
}

View File

@@ -0,0 +1,9 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import net.minecraft.client.gui.screen.ingame.ForgingScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Inject;
@Mixin(ForgingScreen.class)
public class ReplaceForgingScreen {
}

View File

@@ -1,7 +1,10 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
@@ -22,6 +25,20 @@ public abstract class ReplacePlayerBackgrounds extends RecipeBookScreen<PlayerSc
super(handler, recipeBook, inventory, title);
}
@WrapOperation(method = "drawForeground",
allow = 1,
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"))
private int onDrawForegroundText(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle);
return original.call(instance, textRenderer,
textOverride.replaceText(text),
textOverride.replaceX(textRenderer, text, x),
textOverride.replaceY(y),
textOverride.replaceColor(text, color),
shadow);
}
@WrapWithCondition(method = "drawBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTexture(Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIFFIIII)V"))
private boolean onDrawBackground(DrawContext instance, Function<Identifier, RenderLayer> renderLayers, Identifier sprite, int x, int y, float u, float v, int width, int height, int textureWidth, int textureHeight) {
final var override = CustomScreenLayouts.getActiveScreenOverride();

View File

@@ -0,0 +1,65 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.AnvilScreen;
import net.minecraft.client.gui.screen.ingame.BeaconScreen;
import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.ingame.MerchantScreen;
import net.minecraft.text.Text;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Slice;
@Mixin(HandledScreen.class)
// TODO: MerchantScreen.class, BeaconScreen.class
public class ReplaceTextColorInHandledScreen {
@WrapOperation(
method = "drawForeground",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
slice = @Slice(
from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;title:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD),
to = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;playerInventoryTitle:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD)
),
allow = 1,
require = 1)
private int replaceContainerTitle(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle);
return original.call(instance, textRenderer,
textOverride.replaceText(text),
textOverride.replaceX(textRenderer, text, x),
textOverride.replaceY(y),
textOverride.replaceColor(text, color),
shadow);
}
@WrapOperation(
method = "drawForeground",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
slice = @Slice(
from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;playerInventoryTitle:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD),
to = @At(value = "TAIL")
),
allow = 1,
require = 1)
private int replacePlayerShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle);
return original.call(instance, textRenderer,
textOverride.replaceText(text),
textOverride.replaceX(textRenderer, text, x),
textOverride.replaceY(y),
textOverride.replaceColor(text, color),
shadow);
}
}

View File

@@ -635,7 +635,7 @@ You need to specify an x and y offset relative to where the regular screen would
You can move slots around by a specific index. This is not the index in the inventory, but rather the index in the screen (so if you have a chest screen then all the player inventory slots would be a higher index since the chest slots move them down the list). The x and y are relative to where the regular screen top left would be. Set to large values to effectively "delete" a slot by moving it offscreen.
### Moving window title around
### Moving text around
```json
{
@@ -653,12 +653,46 @@ You can move slots around by a specific index. This is not the index in the inve
}
```
You can move the window title around. The x and y are relative to where the text normally is. Set to large values to effectively "delete" a slot by moving it offscreen. playerTitle is the Inventory text where containerTitle is the chest text at the top
You can move the window title around. The x and y are relative to the top left of the regular screen (like slots). Set to large values to effectively "delete" a slot by moving it offscreen.
The align only specifies the direction the text grows in, it does not the actual anchor point, so if you want right aligned text you will also need to move the origin of the text to the right (or it will just grow out of the left side of your screen).
You can replace the text with another text to render instead.
Available titles are
- `containerTitle` for the title of the open container, typically at the very top.
- `playerTitle` for the players inventory title. Note that in the player inventory without a chest or something open, the `containerTitle` is also used for the "Crafting" text.
- `repairCostTitle` for the repair cost label in anvils.
### Moving components around
```json
{
"predicates": {
"label": {
"regex": "Hyper Furnace"
}
},
"nameField": {
"x": 10,
"y": 10,
"width": 100,
"height": 12
}
}
```
Some other components can also be moved. These components might be buttons, text inputs or other things not fitting into any category. They can have a x, y (relative to the top left of the screen), as well as sometimes a width, height, and other properties. This is more of a wild card category, and which options work depends on the type of object.
Available options
- `nameField`: x, y, width & height are all available to move the field to set the name of the item in an anvil.
### All together
| Field | Required | Description |
|--------------------------|----------|--------------------------------------------------------------------------------------------|
|---------------------------|----------|--------------------------------------------------------------------------------------------------------------------------|
| `predicates` | true | A list of predicates that need to match in order to change the layout of a screen |
| `predicates.label` | true | A [string matcher](#string-matcher) for the screen title |
| `background` | false | Allows replacing the background texture |
@@ -671,16 +705,17 @@ You can move the window title around. The x and y are relative to where the text
| `slots[*].index` | true | The index in the array of all slots on the screen (not inventory). |
| `slots[*].x` | true | The x coordinate of the slot relative to the top left of the screen |
| `slots[*].y` | true | The y coordinate of the slot relative to the top left of the screen |
| `playerTitle` | false | The Inventory title. |
| `playerTitle.x` | false | The x coordinate of the slot relative to the text normally is |
| `playerTitle.y` | false | The y coordinate of the slot relative to the text normally is |
| `playerTitle.align` | false | How you want the text to align. "left", "center" or "right" |
| `playerTitle.replace` | false | Replace window text with your text |
| `containerTitle` | false | The container title (eg chest, ender chest, ect) |
| `containerTitle.x` | false | The x coordinate of the slot relative to the text normally is |
| `containerTitle.y` | false | The y coordinate of the slot relative to the text normally is |
| `containerTitle.align` | false | How you want the text to align. "left", "center" or "right" |
| `containerTitle.replace` | false | Replace window text with your text |
| `<element>Title` | false | The title mover (see above for valid options) |
| `<element>Title.x` | false | The x coordinate of text relative to the top left of the screen |
| `<element>Title.y` | false | The y coordinate of text relative to the top left of the screen |
| `<element>Title.align` | false | How you want the text to align. "left", "center" or "right". This only changes the text direction, not its anchor point. |
| `<element>Title.replace` | false | Replace the text with your own text |
| `<extraComponent>` | false | Allows you to move button components and similar around |
| `<extraComponent>.x` | true | The new x coordinate of the component relative to the top left of the screen |
| `<extraComponent>.x` | true | The new y coordinate of the component relative to the top left of the screen |
| `<extraComponent>.width` | false | The new width of the component |
| `<extraComponent>.height` | false | The new height of the component |
## Global Item Texture Replacement