diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java index 8997df737..9dac7bb50 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -40,7 +40,6 @@ import net.minecraft.client.renderer.item.ClampedItemPropertyFunction; import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.ResourceProvider; import net.minecraft.util.FastColor; import net.minecraft.world.entity.LivingEntity; @@ -146,10 +145,6 @@ public final class ClientRegistry { void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property); } - public static void registerReloadListeners(Consumer register, Minecraft minecraft) { - register.accept(GuiSprites.initialise(minecraft.getTextureManager())); - } - private static final ResourceLocation[] EXTRA_MODELS = { TurtleOverlay.ELF_MODEL, TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL, diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java index b398da342..9220c298f 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/ComputerScreen.java @@ -6,9 +6,7 @@ package dan200.computercraft.client.gui; import dan200.computercraft.client.gui.widgets.ComputerSidebar; import dan200.computercraft.client.gui.widgets.TerminalWidget; -import dan200.computercraft.client.render.ComputerBorderRenderer; -import dan200.computercraft.client.render.RenderTypes; -import dan200.computercraft.client.render.SpriteRenderer; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.network.chat.Component; @@ -40,14 +38,15 @@ public final class ComputerScreen extends Abstra public void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) { // Draw a border around the terminal var terminal = getTerminal(); - var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES); var computerTextures = GuiSprites.getComputerTextures(family); - ComputerBorderRenderer.render( - spriteRenderer, computerTextures, - terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false + graphics.blitSprite( + computerTextures.border(), + terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2 + ); + graphics.blitSprite( + Nullability.assertNonNull(computerTextures.sidebar()), + leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT ); - ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset); - graphics.flush(); // Flush to ensure background textures are drawn before foreground. } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/GuiSprites.java b/projects/common/src/client/java/dan200/computercraft/client/gui/GuiSprites.java index 9da1a6120..f516fc5b6 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/GuiSprites.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/GuiSprites.java @@ -7,9 +7,6 @@ package dan200.computercraft.client.gui; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.client.render.ComputerBorderRenderer; import dan200.computercraft.shared.computer.core.ComputerFamily; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.TextureManager; -import net.minecraft.client.resources.TextureAtlasHolder; import net.minecraft.resources.ResourceLocation; import org.jspecify.annotations.Nullable; @@ -19,10 +16,7 @@ import java.util.stream.Stream; /** * Sprite sheet for all GUI texutres in the mod. */ -public final class GuiSprites extends TextureAtlasHolder { - public static final ResourceLocation SPRITE_SHEET = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui"); - public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png"); - +public final class GuiSprites { public static final ButtonTextures TURNED_OFF = button("turned_off"); public static final ButtonTextures TURNED_ON = button("turned_on"); public static final ButtonTextures TERMINATE = button("terminate"); @@ -32,6 +26,9 @@ public final class GuiSprites extends TextureAtlasHolder { public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true); public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false); + private GuiSprites() { + } + private static ButtonTextures button(String name) { return new ButtonTextures( ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name), @@ -47,34 +44,6 @@ public final class GuiSprites extends TextureAtlasHolder { ); } - private static @Nullable GuiSprites instance; - - private GuiSprites(TextureManager textureManager) { - super(textureManager, TEXTURE, SPRITE_SHEET); - } - - /** - * Initialise the singleton {@link GuiSprites} instance. - * - * @param textureManager The current texture manager. - * @return The singleton {@link GuiSprites} instance, to register as resource reload listener. - */ - public static GuiSprites initialise(TextureManager textureManager) { - if (instance != null) throw new IllegalStateException("GuiSprites has already been initialised"); - return instance = new GuiSprites(textureManager); - } - - /** - * Lookup a texture on the atlas. - * - * @param texture The texture to find. - * @return The sprite on the atlas. - */ - public static TextureAtlasSprite get(ResourceLocation texture) { - if (instance == null) throw new IllegalStateException("GuiSprites has not been initialised"); - return instance.getSprite(texture); - } - /** * Get the appropriate textures to use for a particular computer family. * diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java index 4ab08318c..d9cf7431d 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/TurtleScreen.java @@ -7,8 +7,7 @@ package dan200.computercraft.client.gui; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.client.gui.widgets.ComputerSidebar; import dan200.computercraft.client.gui.widgets.TerminalWidget; -import dan200.computercraft.client.render.RenderTypes; -import dan200.computercraft.client.render.SpriteRenderer; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.turtle.inventory.TurtleMenu; @@ -64,8 +63,9 @@ public class TurtleScreen extends AbstractComputerScreen { } // Render sidebar - var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES); - ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset); - graphics.flush(); // Flush to ensure background textures are drawn before foreground. + graphics.blitSprite( + Nullability.assertNonNull(GuiSprites.getComputerTextures(family).sidebar()), + leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT + ); } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java index 13437076d..863159256 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/ComputerSidebar.java @@ -6,9 +6,7 @@ package dan200.computercraft.client.gui.widgets; import dan200.computercraft.client.gui.GuiSprites; import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage; -import dan200.computercraft.client.render.SpriteRenderer; import dan200.computercraft.shared.computer.core.InputHandler; -import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.network.chat.Component; @@ -24,12 +22,9 @@ public final class ComputerSidebar { private static final int ICON_MARGIN = 2; private static final int CORNERS_BORDER = 3; - private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN; private static final int BUTTONS = 2; - private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2; - - private static final int TEX_HEIGHT = 14; + public static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2; private ComputerSidebar() { } @@ -63,14 +58,6 @@ public final class ComputerSidebar { )); } - public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) { - var texture = textures.sidebar(); - if (texture == null) throw new NullPointerException(textures + " has no sidebar texture"); - var sprite = GuiSprites.get(texture); - - renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT); - } - private static void toggleComputer(BooleanSupplier isOn, InputHandler input) { if (isOn.getAsBoolean()) { input.shutdown(); diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/ComputerBorderRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/ComputerBorderRenderer.java index fed82c125..9dff4e3f5 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/ComputerBorderRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/ComputerBorderRenderer.java @@ -1,17 +1,14 @@ -// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers // -// SPDX-License-Identifier: LicenseRef-CCPL +// SPDX-License-Identifier: MPL-2.0 package dan200.computercraft.client.render; -import dan200.computercraft.client.gui.GuiSprites; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; - -import static dan200.computercraft.client.render.SpriteRenderer.u; -import static dan200.computercraft.client.render.SpriteRenderer.v; +import dan200.computercraft.client.gui.ComputerScreen; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling; /** - * Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or + * Constants for the borders of computers, either for a {@linkplain ComputerScreen GUI} or * {@linkplain PocketItemRenderer in-hand pocket computers}. */ public final class ComputerBorderRenderer { @@ -21,55 +18,13 @@ public final class ComputerBorderRenderer { public static final int MARGIN = 2; /** - * The width of the terminal border. + * The size of the terminal border. + *

+ * This is only used for layout of elements within UI. When rendering, the size of the computer's border is + * determined by its {@link GuiSpriteScaling}. */ public static final int BORDER = 12; - public static final int LIGHT_HEIGHT = 8; - - private static final int TEX_SIZE = 36; - private ComputerBorderRenderer() { } - - public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) { - var endX = x + width; - var endY = y + height; - - var border = GuiSprites.get(textures.border()); - - // Top bar - blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER); - blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER); - blitBorder(renderer, border, endX, y - BORDER, BORDER * 2, 0, BORDER, BORDER); - - // Vertical bars - blitBorder(renderer, border, x - BORDER, y, 0, BORDER, BORDER, height); - blitBorder(renderer, border, endX, y, BORDER * 2, BORDER, BORDER, height); - - // Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the - // pocket computer's lights). - if (withLight) { - var pocketBottomTexture = textures.pocketBottom(); - if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture"); - var pocketBottom = GuiSprites.get(pocketBottomTexture); - - renderer.blitHorizontalSliced( - pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT, - BORDER, BORDER, BORDER * 3 - ); - } else { - blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER); - blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER); - blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER); - } - } - - private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) { - renderer.blit( - x, y, width, height, - u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE), - u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE) - ); - } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java index ea0f6869d..0dbd0efa1 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/PocketItemRenderer.java @@ -13,13 +13,16 @@ import dan200.computercraft.core.util.Colour; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.pocket.items.PocketComputerItem; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling; import net.minecraft.util.FastColor; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.DyedItemColor; import org.joml.Matrix4f; -import static dan200.computercraft.client.render.ComputerBorderRenderer.*; +import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; +import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN; import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT; import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH; @@ -29,6 +32,11 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON public final class PocketItemRenderer extends ItemMapLikeRenderer { public static final PocketItemRenderer INSTANCE = new PocketItemRenderer(); + /** + * The height of the pocket computer's light. + */ + private static final int LIGHT_HEIGHT = 8; + private PocketItemRenderer() { } @@ -83,14 +91,69 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer { } private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) { - var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family); + var textures = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family); + var spriteRenderer = new SpriteRenderer(transform, render, 0, light, colour); + renderBorder(spriteRenderer, textures, width, height); + } - var r = (colour >>> 16) & 0xFF; - var g = (colour >>> 8) & 0xFF; - var b = colour & 0xFF; + private static void renderBorder(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int width, int height) { + var sprites = Minecraft.getInstance().getGuiSprites(); - var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b); - ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true); + // Find our border, forcing it to be a nine-sliced texture. + var borderSprite = sprites.getSprite(textures.border()); + var borderSlice = getSlice(sprites.getSpriteScaling(borderSprite), DEFAULT_BORDER); + var borderBounds = borderSlice.border(); + + // And take the separate bottom bit of the pocket computer. + var bottomTexture = textures.pocketBottom(); + if (bottomTexture == null) throw new NullPointerException(textures + " has no pocket texture"); + var bottomSprite = sprites.getSprite(bottomTexture); + var bottomSlice = getSlice(sprites.getSpriteScaling(bottomSprite), DEFAULT_BOTTOM); + var bottomBounds = bottomSlice.border(); + + // Now draw a nine-sliced texture, by stitching together the top parts of the border with the pocket bottom. + + // Top bar + renderer.blit( + borderSprite, -borderBounds.left(), -borderBounds.top(), borderBounds.left(), borderBounds.top(), + 0, 0, borderSlice.width(), borderSlice.height() + ); + renderer.blitTiled( + borderSprite, 0, -borderBounds.top(), width, borderBounds.top(), + borderBounds.left(), 0, borderSlice.width() - borderBounds.left() - borderBounds.right(), borderBounds.top(), + borderSlice.width(), borderSlice.height() + ); + renderer.blit( + borderSprite, width, -borderBounds.top(), borderBounds.right(), borderBounds.top(), + borderSlice.width() - borderBounds.right(), 0, borderSlice.width(), borderSlice.height() + ); + + // Vertical bars + renderer.blitTiled( + borderSprite, -borderBounds.left(), 0, borderBounds.left(), height, + 0, borderBounds.top(), borderBounds.left(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(), + borderSlice.width(), borderSlice.height() + ); + renderer.blitTiled( + borderSprite, width, 0, borderBounds.right(), height, + borderSlice.width() - borderBounds.right(), borderBounds.top(), borderBounds.right(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(), + borderSlice.width(), borderSlice.height() + ); + + // Bottom + renderer.blit( + bottomSprite, -bottomBounds.left(), height, bottomBounds.left(), bottomSlice.height(), + 0, 0, bottomSlice.width(), bottomSlice.height() + ); + renderer.blitTiled( + bottomSprite, 0, height, width, bottomSlice.height(), + bottomBounds.left(), 0, bottomSlice.width() - bottomBounds.left() - bottomBounds.right(), bottomSlice.height(), + bottomSlice.width(), bottomSlice.height() + ); + renderer.blit( + bottomSprite, width, height, bottomBounds.right(), bottomSlice.height(), + bottomSlice.width() - bottomBounds.right(), 0, bottomSlice.width(), bottomSlice.height() + ); } private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) { @@ -101,4 +164,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer { FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP ); } + + private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice( + 36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12) + ); + + private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice( + 36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0) + ); + + private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) { + return scaling instanceof GuiSpriteScaling.NineSlice slice ? slice : fallback; + } } diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java b/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java index 82807cda5..444e0b3f8 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/RenderTypes.java @@ -7,7 +7,6 @@ package dan200.computercraft.client.render; import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.VertexFormat; import dan200.computercraft.api.ComputerCraftAPI; -import dan200.computercraft.client.gui.GuiSprites; import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader; import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import net.minecraft.client.renderer.GameRenderer; @@ -54,11 +53,6 @@ public class RenderTypes { */ public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png")); - /** - * Render type for {@linkplain GuiSprites GUI sprites}. - */ - public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE); - public static MonitorTextureBufferShader getMonitorTextureBufferShader() { if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered"); return monitorTboShader; diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/SpriteRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/SpriteRenderer.java index bb1a3d02e..09ad76f9c 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/SpriteRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/SpriteRenderer.java @@ -6,129 +6,69 @@ package dan200.computercraft.client.render; import com.mojang.blaze3d.vertex.VertexConsumer; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.resources.ResourceLocation; import org.joml.Matrix4f; /** - * A {@link GuiGraphics}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity - * renderer). + * A {@link GuiGraphics}-equivalent renders to a {@link VertexConsumer}. This is suitable for rendering outside of a + * GUI, such as part of an entity renderer. *

* This batches all render calls together, though requires that all {@link TextureAtlasSprite}s are on the same sprite * sheet. */ public class SpriteRenderer { + public static final ResourceLocation TEXTURE = ResourceLocation.withDefaultNamespace("textures/atlas/gui.png"); + private final Matrix4f transform; - private final VertexConsumer builder; + private final MultiBufferSource buffers; private final int light; private final int z; - private final int r, g, b; + private final int colour; - public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) { + public SpriteRenderer(Matrix4f transform, MultiBufferSource buffers, int z, int light, int colour) { this.transform = transform; - this.builder = builder; + this.buffers = buffers; this.z = z; this.light = light; - this.r = r; - this.g = g; - this.b = b; + this.colour = colour; } - public static SpriteRenderer createForGui(GuiGraphics graphics, RenderType renderType) { - return new SpriteRenderer( - graphics.pose().last().pose(), graphics.bufferSource().getBuffer(renderType), - 0, RenderTypes.FULL_BRIGHT_LIGHTMAP, 255, 255, 255 - ); + public void blit(TextureAtlasSprite sprite, int x0, int y0, int width, int height, int spriteX, int spriteY, int spriteWidth, int spriteHeight) { + if (width == 0 || height == 0) return; + + var x1 = x0 + width; + var y1 = y0 + height; + var u0 = sprite.getU((float) spriteX / spriteWidth); + var u1 = sprite.getU((float) (spriteX + width) / spriteWidth); + var v0 = sprite.getV((float) spriteY / spriteHeight); + var v1 = sprite.getV((float) (spriteY + height) / spriteHeight); + + var vertices = buffers.getBuffer(RenderType.text(sprite.atlasLocation())); + vertices.addVertex(transform, x0, y1, z).setColor(colour).setUv(u0, v1).setLight(light); + vertices.addVertex(transform, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light); + vertices.addVertex(transform, x1, y0, z).setColor(colour).setUv(u1, v0).setLight(light); + vertices.addVertex(transform, x0, y0, z).setColor(colour).setUv(u0, v0).setLight(light); } - /** - * Render a single sprite. - * - * @param sprite The texture to draw. - * @param x The x position of the rectangle we'll draw. - * @param y The x position of the rectangle we'll draw. - * @param width The width of the rectangle we'll draw. - * @param height The height of the rectangle we'll draw. - */ - public void blit(TextureAtlasSprite sprite, int x, int y, int width, int height) { - blit(x, y, width, height, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1()); - } + public void blitTiled( + TextureAtlasSprite sprite, + int x, int y, int width, int height, + int tileX, int tileY, int tileWidth, int tileHeight, int spriteWidth, int spriteHeight + ) { + if (width <= 0 || height <= 0) return; + if (tileWidth <= 0 || tileHeight <= 0) { + throw new IllegalArgumentException("Tiled sprite texture size must be positive, got " + tileWidth + "x" + tileHeight); + } - /** - * Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiGraphics#blitNineSliced}, - * the middle texture is stretched rather than repeated. - * - * @param sprite The texture to draw. - * @param x The x position of the rectangle we'll draw. - * @param y The x position of the rectangle we'll draw. - * @param width The width of the rectangle we'll draw. - * @param height The height of the rectangle we'll draw. - * @param leftBorder The width of the left border. - * @param rightBorder The width of the right border. - * @param textureWidth The width of the whole texture. - */ - public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) { - // TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does. - if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders"); - - var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth); - var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth); - - blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1()); - blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1()); - blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1()); - } - - /** - * Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiGraphics#blitNineSliced}, - * the middle texture is stretched rather than repeated. - * - * @param sprite The texture to draw. - * @param x The x position of the rectangle we'll draw. - * @param y The x position of the rectangle we'll draw. - * @param width The width of the rectangle we'll draw. - * @param height The height of the rectangle we'll draw. - * @param topBorder The height of the top border. - * @param bottomBorder The height of the bottom border. - * @param textureHeight The height of the whole texture. - */ - public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) { - // TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does. - if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders"); - - var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight); - var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight); - - blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart); - blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd); - blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1()); - } - - /** - * The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite. - * - * @param x The x position of the rectangle we'll draw. - * @param y The x position of the rectangle we'll draw. - * @param width The width of the rectangle we'll draw. - * @param height The height of the rectangle we'll draw. - * @param u0 The first U coordinate. - * @param v0 The first V coordinate. - * @param u1 The second U coordinate. - * @param v1 The second V coordinate. - */ - public void blit( - int x, int y, int width, int height, float u0, float v0, float u1, float v1) { - builder.addVertex(transform, x, y + height, z).setColor(r, g, b, 255).setUv(u0, v1).setLight(light); - builder.addVertex(transform, x + width, y + height, z).setColor(r, g, b, 255).setUv(u1, v1).setLight(light); - builder.addVertex(transform, x + width, y, z).setColor(r, g, b, 255).setUv(u1, v0).setLight(light); - builder.addVertex(transform, x, y, z).setColor(r, g, b, 255).setUv(u0, v0).setLight(light); - } - - public static float u(TextureAtlasSprite sprite, int x, int width) { - return sprite.getU((float) x / width); - } - - public static float v(TextureAtlasSprite sprite, int y, int height) { - return sprite.getV((float) y / height); + for (var xOffset = 0; xOffset < width; xOffset += tileWidth) { + var sliceWidth = Math.min(tileWidth, width - xOffset); + for (var yOffset = 0; yOffset < height; yOffset += tileHeight) { + var sliceHeight = Math.min(tileHeight, height - yOffset); + blit(sprite, x + xOffset, y + yOffset, sliceWidth, sliceHeight, tileX, tileY, spriteWidth, spriteHeight); + } + } } } diff --git a/projects/common/src/datagen/java/dan200/computercraft/data/DataProviders.java b/projects/common/src/datagen/java/dan200/computercraft/data/DataProviders.java index c2d4aa756..a38034d90 100644 --- a/projects/common/src/datagen/java/dan200/computercraft/data/DataProviders.java +++ b/projects/common/src/datagen/java/dan200/computercraft/data/DataProviders.java @@ -76,7 +76,7 @@ public final class DataProviders { LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED, LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT ))); - out.accept(GuiSprites.SPRITE_SHEET, makeSprites( + out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites( // Computers GuiSprites.COMPUTER_NORMAL.textures(), GuiSprites.COMPUTER_ADVANCED.textures(), @@ -85,6 +85,8 @@ public final class DataProviders { )); }); + generator.add(ResourceMetadataProvider::new); + generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) { @Override public Stream getModels(HolderLookup.Provider registries) { diff --git a/projects/common/src/datagen/java/dan200/computercraft/data/ResourceMetadataProvider.java b/projects/common/src/datagen/java/dan200/computercraft/data/ResourceMetadataProvider.java new file mode 100644 index 000000000..fa010189d --- /dev/null +++ b/projects/common/src/datagen/java/dan200/computercraft/data/ResourceMetadataProvider.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.data; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import dan200.computercraft.client.gui.GuiSprites; +import net.minecraft.client.resources.metadata.gui.GuiMetadataSection; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataProvider; +import net.minecraft.data.PackOutput; +import net.minecraft.data.metadata.PackMetadataGenerator; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.metadata.MetadataSectionType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * Similar to {@link PackMetadataGenerator}, but for individual resources. + */ +final class ResourceMetadataProvider implements DataProvider { + private final PackOutput output; + + ResourceMetadataProvider(PackOutput output) { + this.output = output; + } + + private void register(Builder builder) { + for (var computerTextures : List.of( + GuiSprites.COMPUTER_ADVANCED, + GuiSprites.COMPUTER_COLOUR, + GuiSprites.COMPUTER_COMMAND, + GuiSprites.COMPUTER_NORMAL + )) { + builder.texture(computerTextures.border()).add(GuiMetadataSection.TYPE, new GuiMetadataSection( + new GuiSpriteScaling.NineSlice(36, 36, simpleNineSlicedBorder(12)) + )); + + var sidebar = computerTextures.sidebar(); + if (sidebar != null) { + builder.texture(sidebar).add(GuiMetadataSection.TYPE, new GuiMetadataSection( + new GuiSpriteScaling.NineSlice(17, 14, new GuiSpriteScaling.NineSlice.Border(3, 4, 0, 3)) + )); + } + + var pocketBottom = computerTextures.pocketBottom(); + if (pocketBottom != null) { + builder.texture(pocketBottom).add(GuiMetadataSection.TYPE, new GuiMetadataSection( + new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0)) + )); + } + } + } + + private static GuiSpriteScaling.NineSlice.Border simpleNineSlicedBorder(int size) { + return new GuiSpriteScaling.NineSlice.Border(size, size, size, size); + } + + @Override + public CompletableFuture run(CachedOutput cachedOutput) { + var builder = new Builder(); + register(builder); + + var outputPath = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK); + return CompletableFuture.allOf(builder.metadata.entrySet().stream().map(entry -> { + var json = new JsonObject(); + entry.getValue().elements.forEach((name, element) -> json.add(name, element.get())); + return DataProvider.saveStable(cachedOutput, json, outputPath.resolve(entry.getKey().getNamespace()).resolve(entry.getKey().getPath() + ".mcmeta")); + }).toArray(CompletableFuture[]::new)); + } + + @Override + public String getName() { + return "Resource Metadata"; + } + + /** + * A builder for a set of {@code mcmeta} files. + */ + private static final class Builder { + private final Map metadata = new HashMap<>(); + + FileMetadata texture(ResourceLocation texture) { + return file(texture.withPrefix("textures/").withSuffix(".png")); + } + + FileMetadata file(ResourceLocation path) { + return metadata.computeIfAbsent(path, p -> new FileMetadata()); + } + } + + /** + * A builder for a given file's {@code mcmeta} file. + */ + private static final class FileMetadata { + private final Map> elements = new HashMap<>(); + + FileMetadata add(MetadataSectionType type, T value) { + elements.put(type.getMetadataSectionName(), () -> type.toJson(value)); + return this; + } + } +} diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_advanced.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_advanced.png.mcmeta new file mode 100644 index 000000000..a2d2a767f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_advanced.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": 12, + "height": 36, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_colour.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_colour.png.mcmeta new file mode 100644 index 000000000..a2d2a767f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_colour.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": 12, + "height": 36, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_command.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_command.png.mcmeta new file mode 100644 index 000000000..a2d2a767f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_command.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": 12, + "height": 36, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_normal.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_normal.png.mcmeta new file mode 100644 index 000000000..a2d2a767f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/border_normal.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": 12, + "height": 36, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_advanced.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_advanced.png.mcmeta new file mode 100644 index 000000000..8bb0aac9f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_advanced.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": { + "bottom": 0, + "left": 12, + "right": 12, + "top": 0 + }, + "height": 20, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_colour.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_colour.png.mcmeta new file mode 100644 index 000000000..8bb0aac9f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_colour.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": { + "bottom": 0, + "left": 12, + "right": 12, + "top": 0 + }, + "height": 20, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_normal.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_normal.png.mcmeta new file mode 100644 index 000000000..8bb0aac9f --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/pocket_bottom_normal.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": { + "bottom": 0, + "left": 12, + "right": 12, + "top": 0 + }, + "height": 20, + "width": 36 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_advanced.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_advanced.png.mcmeta new file mode 100644 index 000000000..1fbfab193 --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_advanced.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": { + "bottom": 3, + "left": 3, + "right": 0, + "top": 4 + }, + "height": 14, + "width": 17 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_command.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_command.png.mcmeta new file mode 100644 index 000000000..1fbfab193 --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_command.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": { + "bottom": 3, + "left": 3, + "right": 0, + "top": 4 + }, + "height": 14, + "width": 17 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_normal.png.mcmeta b/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_normal.png.mcmeta new file mode 100644 index 000000000..1fbfab193 --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_normal.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "border": { + "bottom": 3, + "left": 3, + "right": 0, + "top": 4 + }, + "height": 14, + "width": 17 + } + } +} \ No newline at end of file diff --git a/projects/common/src/generated/resources/assets/computercraft/atlases/gui.json b/projects/common/src/generated/resources/assets/minecraft/atlases/gui.json similarity index 100% rename from projects/common/src/generated/resources/assets/computercraft/atlases/gui.json rename to projects/common/src/generated/resources/assets/minecraft/atlases/gui.json diff --git a/projects/fabric/src/client/java/dan200/computercraft/mixin/client/MinecraftMixin.java b/projects/fabric/src/client/java/dan200/computercraft/mixin/client/MinecraftMixin.java index 7288723b9..ac11fe0bf 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/mixin/client/MinecraftMixin.java +++ b/projects/fabric/src/client/java/dan200/computercraft/mixin/client/MinecraftMixin.java @@ -5,25 +5,16 @@ package dan200.computercraft.mixin.client; import dan200.computercraft.client.ClientHooks; -import dan200.computercraft.client.ClientRegistry; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.main.GameConfig; import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.server.packs.resources.ReloadableResourceManager; -import org.spongepowered.asm.mixin.Final; 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(Minecraft.class) class MinecraftMixin { - @Shadow - @Final - private ReloadableResourceManager resourceManager; - @Inject(method = "updateLevelInEngines", at = @At("HEAD")) @SuppressWarnings("unused") private void updateLevelInEngines(ClientLevel screen, CallbackInfo ci) { @@ -35,17 +26,4 @@ class MinecraftMixin { private void disconnect(Screen screen, boolean keepResourcePacks, CallbackInfo ci) { ClientHooks.onDisconnect(); } - - @Inject( - method = "(Lnet/minecraft/client/main/GameConfig;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/ResourceLoadStateTracker;startReload(Lnet/minecraft/client/ResourceLoadStateTracker$ReloadReason;Ljava/util/List;)V", - ordinal = 0 - ) - ) - @SuppressWarnings("unused") - private void beforeInitialResourceReload(GameConfig gameConfig, CallbackInfo ci) { - ClientRegistry.registerReloadListeners(resourceManager::registerReloadListener, (Minecraft) (Object) this); - } } diff --git a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java index 84704d7da..1a377496a 100644 --- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java +++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java @@ -18,7 +18,10 @@ import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.ModLoader; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; -import net.neoforged.neoforge.client.event.*; +import net.neoforged.neoforge.client.event.ModelEvent; +import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.event.RegisterShadersEvent; import java.io.IOException; @@ -82,11 +85,6 @@ public final class ForgeClientRegistry { ClientRegistry.registerMenuScreens(event::register); } - @SubscribeEvent - public static void registerReloadListeners(RegisterClientReloadListenersEvent event) { - ClientRegistry.registerReloadListeners(event::registerReloadListener, Minecraft.getInstance()); - } - @SubscribeEvent public static void setupClient(FMLClientSetupEvent event) { ClientRegistry.register();