diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2e622eca..18ba67008 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,8 +59,8 @@ jmh = "1.37" # Build tools cctJavadoc = "1.8.5" -checkstyle = "10.21.4" -errorProne-core = "2.37.0" +checkstyle = "10.23.1" +errorProne-core = "2.38.0" errorProne-plugin = "4.1.0" fabric-loom = "1.10.4" githubRelease = "2.5.2" @@ -70,7 +70,7 @@ illuaminate = "0.1.0-83-g1131f68" lwjgl = "3.3.3" minotaur = "2.8.7" modDevGradle = "2.0.82" -nullAway = "0.12.4" +nullAway = "0.12.7" shadow = "8.3.1" spotless = "7.0.2" taskTree = "2.1.1" 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 5ce1ef4eb..db3cf468e 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -5,7 +5,6 @@ package dan200.computercraft.client; import com.mojang.serialization.MapCodec; -import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.client.StandaloneModel; import dan200.computercraft.api.client.turtle.*; import dan200.computercraft.client.gui.*; @@ -23,7 +22,6 @@ import dan200.computercraft.client.turtle.TurtleOverlayManager; import dan200.computercraft.client.turtle.TurtleUpgradeModelManager; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; -import net.minecraft.client.Minecraft; import net.minecraft.client.color.item.ItemTintSource; import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.gui.screens.Screen; @@ -38,7 +36,6 @@ import net.minecraft.client.resources.model.ModelBaker; import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ResolvableModel; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.MenuType; @@ -107,10 +104,6 @@ public final class ClientRegistry { register.register(SelectUpgradeModel.ID, SelectUpgradeModel.CODEC); } - public static void registerReloadListeners(BiConsumer register, Minecraft minecraft) { - register.accept(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sprites"), GuiSprites.initialise(minecraft.getTextureManager())); - } - private static final ResourceLocation[] EXTRA_MODELS = { TurtleOverlay.ELF_MODEL, TurtleBlockEntityRenderer.NORMAL_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 45896e151..118d6e6dc 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,10 +6,10 @@ 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.SpriteRenderer; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderType; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; @@ -39,14 +39,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 computerTextures = GuiSprites.getComputerTextures(family); - SpriteRenderer.inGui(graphics, spriteRenderer -> { - var computerTextures = GuiSprites.getComputerTextures(family); - ComputerBorderRenderer.render( - spriteRenderer, computerTextures, - terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false - ); - ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset); - }); + graphics.blitSprite( + RenderType::guiTextured, computerTextures.border(), + terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2 + ); + graphics.blitSprite( + RenderType::guiTextured, Nullability.assertNonNull(computerTextures.sidebar()), + leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT + ); } } 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/KeyConverter.java b/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java new file mode 100644 index 000000000..f99a254bf --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.gui; + +import org.lwjgl.glfw.GLFW; + +/** + * Supports for converting/translating key codes. + */ +public class KeyConverter { + /** + * GLFW's key events refer to the physical key code, rather than the "actual" key code (with keyboard layout + * applied). + *

+ * This makes sense for WASD-style input, but is a right pain for keyboard shortcuts — this function attempts to + * translate those keys back to their "actual" key code. See also + * this discussion on GLFW's GitHub. + * + * @param key The current key code. + * @param scanCode The current scan code. + * @return The translated key code. + */ + public static int physicalToActual(int key, int scanCode) { + var name = GLFW.glfwGetKeyName(key, scanCode); + if (name == null || name.length() != 1) return key; + + // If we've got a single character as the key name, treat that as the ASCII value of the key, + // and map that back to a key code. + var character = name.charAt(0); + + // 0-9 and A-Z map directly to their GLFW key (they're the same ASCII code). + if ((character >= '0' && character <= '9') || (character >= 'A' && character <= 'Z')) return character; + // a-z map to GLFW_KEY_{A,Z} + if (character >= 'a' && character <= 'z') return GLFW.GLFW_KEY_A + (character - 'a'); + + return key; + } +} 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 70c1c119a..6b10c1bc4 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,7 +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.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; @@ -67,8 +67,9 @@ public class TurtleScreen extends AbstractComputerScreen { } // Render sidebar - SpriteRenderer.inGui(graphics, spriteRenderer -> - ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset) + graphics.blitSprite( + RenderType::guiTextured, 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/gui/widgets/TerminalWidget.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java index 0f43e69d5..fcd3f8af4 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java @@ -4,6 +4,7 @@ package dan200.computercraft.client.gui.widgets; +import dan200.computercraft.client.gui.KeyConverter; import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.util.StringUtil; @@ -82,7 +83,7 @@ public class TerminalWidget extends AbstractWidget { } if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) { - switch (key) { + switch (KeyConverter.physicalToActual(key, scancode)) { case GLFW.GLFW_KEY_T -> { if (terminateTimer < 0) terminateTimer = 0; } @@ -118,7 +119,7 @@ public class TerminalWidget extends AbstractWidget { computer.keyUp(key); } - switch (key) { + switch (KeyConverter.physicalToActual(key, scancode)) { case GLFW.GLFW_KEY_T -> terminateTimer = -1; case GLFW.GLFW_KEY_R -> rebootTimer = -1; case GLFW.GLFW_KEY_S -> shutdownTimer = -1; 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 0d47c2301..f4e445817 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,15 +13,17 @@ 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.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling; import net.minecraft.util.ARGB; 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; @@ -31,6 +33,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() { } @@ -85,14 +92,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(RenderType.text(GuiSprites.TEXTURE)), 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) { @@ -103,4 +165,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer { ARGB.opaque(colour), LightTexture.FULL_BRIGHT ); } + + private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice( + 36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12), false + ); + + private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice( + 36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false + ); + + 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/SpriteRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/SpriteRenderer.java index 672ffa917..816c201cc 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 @@ -5,134 +5,71 @@ package dan200.computercraft.client.render; import com.mojang.blaze3d.vertex.VertexConsumer; -import dan200.computercraft.client.gui.GuiSprites; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.renderer.LightTexture; +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; -import java.util.function.Consumer; /** - * 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 void inGui(GuiGraphics graphics, Consumer renderer) { - graphics.drawSpecial(bufferSource -> renderer.accept(new SpriteRenderer( - graphics.pose().last().pose(), bufferSource.getBuffer(RenderType.guiTextured(GuiSprites.TEXTURE)), - 0, LightTexture.FULL_BRIGHT, 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.21.4): 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.21.4): 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/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java index 3fb152b99..3a8d216d4 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java @@ -65,8 +65,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer new LanguageProvider(out, fullRegistries)); generator.addFromCodec("Block atlases", PackOutput.Target.RESOURCE_PACK, "atlases", SpriteSources.FILE_CODEC, out -> { - out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of( + out.accept(AtlasIds.BLOCKS, makeSprites(Stream.of( LecternPrintoutModel.TEXTURE, LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED, LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT ))); - out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites(Stream.of( - UpgradeSlot.LEFT_UPGRADE, - UpgradeSlot.RIGHT_UPGRADE - ))); - - out.accept(GuiSprites.SPRITE_SHEET, makeSprites( + out.accept(AtlasIds.GUI, makeSprites( + Stream.of(UpgradeSlot.LEFT_UPGRADE, UpgradeSlot.RIGHT_UPGRADE), // Computers GuiSprites.COMPUTER_NORMAL.textures(), GuiSprites.COMPUTER_ADVANCED.textures(), @@ -89,6 +86,8 @@ public final class DataProviders { )); }); + generator.add(ResourceMetadataProvider::new); + generator.addFromCodec("Turtle overlays", PackOutput.Target.RESOURCE_PACK, TurtleOverlay.SOURCE, TurtleOverlay.CODEC, TurtleOverlays::register); generator.addFromCodec("Turtle upgrade models", PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC, TurtleUpgradeProvider::addModels); 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..908e2d75d --- /dev/null +++ b/projects/common/src/datagen/java/dan200/computercraft/data/ResourceMetadataProvider.java @@ -0,0 +1,113 @@ +// 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 com.mojang.serialization.JsonOps; +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; + +/** + * Generates {@code .mcmeta} files for texture files. + *

+ * This is 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), false) + )); + + 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), false) + )); + } + + 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), false) + )); + } + } + } + + 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.name(), () -> type.codec().encodeStart(JsonOps.INSTANCE, value).getOrThrow().getAsJsonObject()); + return this; + } + } +} diff --git a/projects/common/src/generated/resources/assets/computercraft/atlases/gui.json b/projects/common/src/generated/resources/assets/computercraft/atlases/gui.json deleted file mode 100644 index cbb2c9ee1..000000000 --- a/projects/common/src/generated/resources/assets/computercraft/atlases/gui.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "sources": [ - {"type": "minecraft:single", "resource": "computercraft:gui/border_normal"}, - {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"}, - {"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"}, - {"type": "minecraft:single", "resource": "computercraft:gui/border_advanced"}, - {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_advanced"}, - {"type": "minecraft:single", "resource": "computercraft:gui/sidebar_advanced"}, - {"type": "minecraft:single", "resource": "computercraft:gui/border_command"}, - {"type": "minecraft:single", "resource": "computercraft:gui/sidebar_command"}, - {"type": "minecraft:single", "resource": "computercraft:gui/border_colour"}, - {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_colour"} - ] -} 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/minecraft/atlases/gui.json b/projects/common/src/generated/resources/assets/minecraft/atlases/gui.json index d9d236296..94eb600bc 100644 --- a/projects/common/src/generated/resources/assets/minecraft/atlases/gui.json +++ b/projects/common/src/generated/resources/assets/minecraft/atlases/gui.json @@ -1,6 +1,16 @@ { "sources": [ {"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"}, - {"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"} + {"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"}, + {"type": "minecraft:single", "resource": "computercraft:gui/border_normal"}, + {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"}, + {"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"}, + {"type": "minecraft:single", "resource": "computercraft:gui/border_advanced"}, + {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_advanced"}, + {"type": "minecraft:single", "resource": "computercraft:gui/sidebar_advanced"}, + {"type": "minecraft:single", "resource": "computercraft:gui/border_command"}, + {"type": "minecraft:single", "resource": "computercraft:gui/sidebar_command"}, + {"type": "minecraft:single", "resource": "computercraft:gui/border_colour"}, + {"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_colour"} ] } diff --git a/projects/common/src/main/resources/assets/computercraft/textures/gui/sidebar_advanced.png b/projects/common/src/main/resources/assets/computercraft/textures/gui/sidebar_advanced.png index 8d96a99c8..ad80081cb 100644 Binary files a/projects/common/src/main/resources/assets/computercraft/textures/gui/sidebar_advanced.png and b/projects/common/src/main/resources/assets/computercraft/textures/gui/sidebar_advanced.png differ diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java index 395f27d45..05b6a9820 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java @@ -76,6 +76,7 @@ public abstract class AbstractHandle { * @cc.treturn [2] nil If seeking failed. * @cc.treturn string The reason seeking failed. * @cc.since 1.80pr1.9 + * @cc.changed 1.109.0 Now available on all file handles, not just binary-mode handles. */ public Object @Nullable [] seek(Optional whence, Optional offset) throws LuaException { checkOpen(); @@ -179,6 +180,8 @@ public abstract class AbstractHandle { * @throws LuaException If the file has been closed. * @cc.treturn string|nil The remaining contents of the file, or {@code nil} in the event of an error. * @cc.since 1.80pr1 + * @cc.changed 1.109.0 Binary-mode handles are now consistent with non-binary files, and return an empty string at + * the end of the file, rather than {@code nil}. */ public Object @Nullable [] readAll() throws LuaException { checkOpen(); diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java index 234127244..2916929f1 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java +++ b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java @@ -120,7 +120,7 @@ public final class StringUtil { var idx = 0; var iterator = clipboard.codePoints().iterator(); - while (iterator.hasNext() && idx <= output.length) { + while (iterator.hasNext() && idx < output.length) { var chr = unicodeToTerminal(iterator.next()); if (chr < 0) continue; // Strip out unconvertible characters if (!isTypableChar(chr)) break; // Stop at untypable ones. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua index b98ff8d75..f013a29e1 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua @@ -371,7 +371,7 @@ function toBlit(color) local hex = color_hex_lookup[color] if hex then return hex end - if color < 0 or color > 0xffff then error("Colour out of range", 2) end + if color < 1 or color > 0xffff then error("Colour out of range", 2) end return string.format("%x", math.floor(math.log(color, 2))) end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua index 7be7aebaf..5b5678d63 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua @@ -65,7 +65,7 @@ local function parse_color(color) return expect(1, color, "number") end - if color < 0 or color > 0xffff then error("Colour out of range", 3) end + if color < 1 or color > 0xffff then error("Colour out of range", 3) end return 2 ^ math.floor(math.log(color, 2)) end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua index 9faf3b5a9..2d8d91938 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua @@ -238,7 +238,7 @@ local function lex_token(context, str, pos) if end_pos then return tokens.STRING, end_pos end context.report(errors.unfinished_long_string, pos, boundary_pos, boundary_pos - pos) - return tokens.ERROR, #str + return tokens.STRING, #str elseif pos + 1 == boundary_pos then -- Just a "[" return tokens.OSQUARE, pos else -- Malformed long string, for instance "[=" @@ -260,7 +260,7 @@ local function lex_token(context, str, pos) if end_pos then return tokens.COMMENT, end_pos end context.report(errors.unfinished_long_comment, pos, boundary_pos, boundary_pos - comment_pos) - return tokens.ERROR, #str + return tokens.COMMENT, #str end end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index e05ec1766..0e7a131e8 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -175,63 +175,62 @@ local function save(_sPath, fWrite) return ok, err, fileerr end -local tKeywords = { - ["and"] = true, - ["break"] = true, - ["do"] = true, - ["else"] = true, - ["elseif"] = true, - ["end"] = true, - ["false"] = true, - ["for"] = true, - ["function"] = true, - ["if"] = true, - ["in"] = true, - ["local"] = true, - ["nil"] = true, - ["not"] = true, - ["or"] = true, - ["repeat"] = true, - ["return"] = true, - ["then"] = true, - ["true"] = true, - ["until"] = true, - ["while"] = true, -} -local function tryWrite(sLine, regex, colour) - local match = string.match(sLine, regex) - if match then - if type(colour) == "number" then - term.setTextColour(colour) - else - term.setTextColour(colour(match)) - end - term.write(match) - term.setTextColour(textColour) - return string.sub(sLine, #match + 1) - end - return nil +local tokens = require "cc.internal.syntax.parser".tokens +local lex_one = require "cc.internal.syntax.lexer".lex_one + +local token_colours = { + [tokens.STRING] = stringColour, + [tokens.COMMENT] = commentColour, + -- Keywords + [tokens.AND] = keywordColour, + [tokens.BREAK] = keywordColour, + [tokens.DO] = keywordColour, + [tokens.ELSE] = keywordColour, + [tokens.ELSEIF] = keywordColour, + [tokens.END] = keywordColour, + [tokens.FALSE] = keywordColour, + [tokens.FOR] = keywordColour, + [tokens.FUNCTION] = keywordColour, + [tokens.GOTO] = keywordColour, + [tokens.IF] = keywordColour, + [tokens.IN] = keywordColour, + [tokens.LOCAL] = keywordColour, + [tokens.NIL] = keywordColour, + [tokens.NOT] = keywordColour, + [tokens.OR] = keywordColour, + [tokens.REPEAT] = keywordColour, + [tokens.RETURN] = keywordColour, + [tokens.THEN] = keywordColour, + [tokens.TRUE] = keywordColour, + [tokens.UNTIL] = keywordColour, + [tokens.WHILE] = keywordColour, +} +-- Fill in the remaining tokens. +for _, token in pairs(tokens) do + if not token_colours[token] then token_colours[token] = textColour end end -local function writeHighlighted(sLine) - while #sLine > 0 do - sLine = - tryWrite(sLine, "^%-%-%[%[.-%]%]", commentColour) or - tryWrite(sLine, "^%-%-.*", commentColour) or - tryWrite(sLine, "^\"\"", stringColour) or - tryWrite(sLine, "^\".-[^\\]\"", stringColour) or - tryWrite(sLine, "^\'\'", stringColour) or - tryWrite(sLine, "^\'.-[^\\]\'", stringColour) or - tryWrite(sLine, "^%[%[.-%]%]", stringColour) or - tryWrite(sLine, "^[%w_]+", function(match) - if tKeywords[match] then - return keywordColour - end - return textColour - end) or - tryWrite(sLine, "^[^%w_]", textColour) +local lex_context = { line = function() end, report = function() end } + +local function writeHighlighted(line) + local pos, colour = 1, nil + + while true do + local token, _, finish = lex_one(lex_context, line, pos) + if not token then break end + + local new_colour = token_colours[token] + if new_colour ~= colour then + term.setTextColor(new_colour) + colour = new_colour + end + + term.write(line:sub(pos, finish)) + pos = finish + 1 end + + term.write(line:sub(pos)) end local tCompletions @@ -352,7 +351,7 @@ local tMenuFuncs = { if bReadOnly then set_status("Access denied", false) else - local ok, _, fileerr = save(sPath, function(file) + local ok, _, fileerr = save(sPath, function(file) for _, sLine in ipairs(tLines) do file.write(sLine .. "\n") end @@ -547,7 +546,7 @@ local function acceptCompletion() -- Append the completion local sCompletion = tCompletions[nCompletion] tLines[y] = tLines[y] .. sCompletion - setCursor(x + #sCompletion , y) + setCursor(x + #sCompletion, y) end end @@ -805,7 +804,7 @@ while bRunning do -- Input text local sLine = tLines[y] tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x) - setCursor(x + #param , y) + setCursor(x + #param, y) end elseif sEvent == "mouse_click" then diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua index f3b9f7a08..a14e999ef 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua @@ -191,7 +191,7 @@ end -- Show MOTD if settings.get("motd.enable") then - shell.run("motd") + shell.run("/rom/programs/motd") end -- Run the user created startup, either from disk drives or the root diff --git a/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java b/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java new file mode 100644 index 000000000..69e9d3c07 --- /dev/null +++ b/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.util; + +import dan200.computercraft.api.lua.LuaValues; +import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class) +class StringUtilTest { + @ParameterizedTest + @ValueSource(strings = { "hello\nworld", "hello\n\rworld", "hello\rworld" }) + public void getClipboardString_returns_a_single_line(String input) { + var result = StringUtil.getClipboardString(input); + assertEquals(LuaValues.encode("hello"), result); + } + + @Test + public void getClipboardString_limits_length() { + var input = "abcdefghijklmnop".repeat(50); + var result = StringUtil.getClipboardString(input); + assertEquals(StringUtil.MAX_PASTE_LENGTH, result.limit()); + + assertEquals( + LuaValues.encode(input.substring(0, StringUtil.MAX_PASTE_LENGTH)), + result + ); + } +} diff --git a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua index 46c77dcf8..8f50b0d56 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua @@ -94,6 +94,7 @@ describe("The colors library", function() end) it("errors on out-of-range colours", function() + expect.error(colors.toBlit, 0):eq("Colour out of range") expect.error(colors.toBlit, -120):eq("Colour out of range") expect.error(colors.toBlit, 0x10000):eq("Colour out of range") end) diff --git a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua index 89e4e2993..4ad3d797c 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua @@ -59,6 +59,16 @@ describe("The window library", function() expect.error(w.setTextColour, nil):eq("bad argument #1 (number expected, got nil)") expect.error(w.setTextColour, -5):eq("Colour out of range") + expect.error(w.setTextColour, 0):eq("Colour out of range") + expect.error(w.setTextColour, 0x10000):eq("Colour out of range") + end) + + it("accepts valid colours", function() + local w = mk() + for i = 0, 15 do + w.setBackgroundColour(2 ^ i) + expect(w.getBackgroundColour()):eq(2 ^ i) + end end) it("supports invalid combined colours", function() diff --git a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md index 9d5af0531..e73c4f62f 100644 --- a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md +++ b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md @@ -67,7 +67,7 @@ This comment was never finished. 1 | --[=[ | ^^^^^ Comment was started here. We expected a closing delimiter (]=]) somewhere after this comment was started. -1:1-1:5 ERROR --[=[ +1:1-1:5 COMMENT --[=[ ``` Nested comments are rejected, just as Lua 5.1 does: @@ -191,7 +191,7 @@ This string was never finished. 1 | return [[ | ^^ String was started here. We expected a closing delimiter (]]) somewhere after this string was started. -1:8-1:9 ERROR [[ +1:8-1:9 STRING [[ ``` We also handle malformed opening strings: 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 2159bfc1f..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((id, l) -> resourceManager.registerReloadListener(l), (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 31ce44cec..6a6bd2fde 100644 --- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java +++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java @@ -86,11 +86,6 @@ public final class ForgeClientRegistry { ClientRegistry.registerMenuScreens(event::register); } - @SubscribeEvent - public static void registerReloadListeners(AddClientReloadListenersEvent event) { - ClientRegistry.registerReloadListeners(event::addListener, Minecraft.getInstance()); - } - @SubscribeEvent public static void registerRenderStateModifiers(RegisterRenderStateModifiersEvent event) { event.registerEntityModifier(new TypeToken>() {