feat: Add screen layout replacement feature for texture packs

This commit is contained in:
Linnea Gräf
2025-06-20 15:49:15 +02:00
parent 54a76553a7
commit 286691c54c
9 changed files with 338 additions and 1 deletions

View File

@@ -0,0 +1,142 @@
package moe.nea.firmament.features.texturepack
import kotlinx.serialization.Serializable
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.render.RenderLayer
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.screen.slot.Slot
import net.minecraft.util.Identifier
import net.minecraft.util.profiler.Profiler
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.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ErrorUtil.intoCatch
import moe.nea.firmament.util.IdentifierSerializer
object CustomScreenLayouts : SinglePreparationResourceReloader<List<CustomScreenLayouts.CustomScreenLayout>>() {
@Serializable
data class CustomScreenLayout(
val predicates: Preds,
val background: BackgroundReplacer? = null,
val slots: List<SlotReplacer> = listOf(),
)
@Serializable
data class Preds(
val label: StringMatcher,
) {
fun matches(screen: Screen): Boolean {
// TODO: does this deserve the restriction to handled screen
val s = screen as? HandledScreen<*>? ?: return false
return label.matches(s.title)
}
}
@Serializable
data class BackgroundReplacer(
@Serializable(with = IdentifierSerializer::class)
val texture: Identifier,
// TODO: allow selectively still rendering some components (recipe button, trade backgrounds, furnace flame progress, arrows)
val x: Int,
val y: Int,
val width: Int,
val height: Int,
) {
fun renderGeneric(context: DrawContext, screen: HandledScreen<*>) {
screen as AccessorHandledScreen
val originalX: Int = (screen.width - screen.backgroundWidth_Firmament) / 2
val originalY: Int = (screen.height - screen.backgroundHeight_Firmament) / 2
val modifiedX = originalX + this.x
val modifiedY = originalY + this.y
val textureWidth = this.width
val textureHeight = this.height
context.drawTexture(
RenderLayer::getGuiTextured,
this.texture,
modifiedX,
modifiedY,
0.0f,
0.0f,
textureWidth,
textureHeight,
textureWidth,
textureHeight
)
}
}
@Serializable
data class SlotReplacer(
// TODO: override getRecipeBookButtonPos as well
// TODO: is this index or id (i always forget which one is duplicated per inventory)
val index: Int,
val x: Int,
val y: Int,
) {
fun move(slots: List<Slot>) {
val slot = slots.getOrNull(index) ?: return
slot.x = x
slot.y = y
}
}
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.resourceManager.registerReloader(CustomScreenLayouts)
}
override fun prepare(
manager: ResourceManager,
profiler: Profiler
): List<CustomScreenLayout> {
val allScreenLayouts = manager.findResources(
"overrides/screen_layout",
{ it.path.endsWith(".json") && it.namespace == "firmskyblock" })
val allParsedLayouts = allScreenLayouts.mapNotNull { (path, stream) ->
Firmament.tryDecodeJsonFromStream<CustomScreenLayout>(stream.inputStream)
.intoCatch("Could not read custom screen layout from $path").orNull()
}
return allParsedLayouts
}
var customScreenLayouts = listOf<CustomScreenLayout>()
override fun apply(
prepared: List<CustomScreenLayout>,
manager: ResourceManager?,
profiler: Profiler?
) {
this.customScreenLayouts = prepared
}
@get:JvmStatic
var activeScreenOverride = null as CustomScreenLayout?
@Subscribe
fun onScreenOpen(event: ScreenChangeEvent) {
if (!CustomSkyBlockTextures.TConfig.allowLayoutChanges) {
activeScreenOverride = null
return
}
activeScreenOverride = event.new?.let { screen ->
customScreenLayouts.find { it.predicates.matches(screen) }
}
val screen = event.new as? HandledScreen<*> ?: return
val handler = screen.screenHandler
activeScreenOverride?.let { override ->
override.slots.forEach { slotReplacer ->
slotReplacer.move(handler.slots)
}
}
}
}

View File

@@ -36,6 +36,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
val enableLegacyMinecraftCompat by toggle("legacy-minecraft-path-support") { true }
val enableLegacyCIT by toggle("legacy-cit") { true }
val allowRecoloringUiText by toggle("recolor-text") { true }
val allowLayoutChanges by toggle("screen-layouts") { true }
}
override val config: ManagedConfig

View File

@@ -2,6 +2,7 @@ package moe.nea.firmament.features.texturepack
import java.util.Optional
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlin.jvm.optionals.getOrNull
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
@@ -18,12 +19,23 @@ object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.Tex
data class TextOverrides(
val defaultColor: Int,
val overrides: List<TextOverride> = listOf()
)
) {
/**
* Stub custom text color to allow always returning a text override
*/
@Transient
val baseOverride = TextOverride(
StringMatcher.Equals("", false),
defaultColor,
false
)
}
@Serializable
data class TextOverride(
val predicate: StringMatcher,
val override: Int,
val hidden: Boolean = false,
)
@Subscribe

View File

@@ -0,0 +1,32 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.AbstractFurnaceScreen;
import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.AbstractFurnaceScreenHandler;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.function.Function;
@Mixin(AbstractFurnaceScreen.class)
public abstract class ReplaceFurnaceBackgrounds<T extends AbstractFurnaceScreenHandler> extends RecipeBookScreen<T> {
public ReplaceFurnaceBackgrounds(T handler, RecipeBookWidget<?> recipeBook, PlayerInventory inventory, Text title) {
super(handler, recipeBook, inventory, title);
}
@WrapWithCondition(method = "drawBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTexture(Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIFFIIII)V"), allow = 1)
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();
if (override == null || override.getBackground() == null) return true;
override.getBackground().renderGeneric(instance, this);
return false;
}
}

View File

@@ -0,0 +1,29 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.*;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.text.Text;
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({CraftingScreen.class, CrafterScreen.class, Generic3x3ContainerScreen.class, GenericContainerScreen.class, HopperScreen.class, ShulkerBoxScreen.class,})
public abstract class ReplaceGenericBackgrounds extends HandledScreen<ScreenHandler> {
// TODO: split out screens with special background components like flames, arrows, etc. (maybe arrows deserve generic handling tho)
public ReplaceGenericBackgrounds(ScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
}
@Inject(method = "drawBackground", at = @At("HEAD"), cancellable = true)
private void replaceDrawBackground(DrawContext context, float deltaTicks, int mouseX, int mouseY, CallbackInfo ci) {
final var override = CustomScreenLayouts.getActiveScreenOverride();
if (override == null || override.getBackground() == null) return;
override.getBackground().renderGeneric(context, this);
ci.cancel();
}
}

View File

@@ -0,0 +1,34 @@
package moe.nea.firmament.mixins.custommodels.screenlayouts;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.function.Function;
@Mixin(InventoryScreen.class)
public abstract class ReplacePlayerBackgrounds extends RecipeBookScreen<PlayerScreenHandler> {
public ReplacePlayerBackgrounds(PlayerScreenHandler handler, RecipeBookWidget<?> recipeBook, PlayerInventory inventory, Text title) {
super(handler, recipeBook, inventory, title);
}
@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();
if (override == null || override.getBackground() == null) return true;
override.getBackground().renderGeneric(instance, this);
return false;
}
// TODO: allow moving the player
}