1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-25 06:52:17 +00:00

Merge branch 'mc-1.21.x' into mc-1.21.y

This commit is contained in:
Jonathan Coates 2025-06-15 16:59:26 +01:00
commit f3f43191ab
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
40 changed files with 574 additions and 356 deletions

View File

@ -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"

View File

@ -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<ResourceLocation, PreparableReloadListener> 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,

View File

@ -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<T extends AbstractComputerMenu> 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
);
}
}

View File

@ -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.
*

View File

@ -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).
* <p>
* 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
* <a href="https://github.com/glfw/glfw/issues/1502"> this discussion on GLFW's GitHub.</a>
*
* @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;
}
}

View File

@ -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<TurtleMenu> {
}
// 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
);
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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.
* <p>
* 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)
);
}
}

View File

@ -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;
}
}

View File

@ -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.
* <p>
* 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<SpriteRenderer> 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);
}
}
}
}

View File

@ -65,8 +65,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
var matrix = transform.last().pose();
var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
var width = -font.width(label) / 2.0f;
font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
font.drawInBatch(label, width, (float) 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
font.drawInBatch(label, width, 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
font.drawInBatch(label, width, 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
transform.popPose();
}

View File

@ -21,6 +21,7 @@ import net.minecraft.client.data.models.ItemModelGenerators;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.client.resources.model.AtlasIds;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder;
import net.minecraft.data.DataProvider;
@ -69,18 +70,14 @@ public final class DataProviders {
generator.add(out -> 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);

View File

@ -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.
* <p>
* 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<ResourceLocation, FileMetadata> 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<String, Supplier<JsonElement>> elements = new HashMap<>();
<T> FileMetadata add(MetadataSectionType<T> type, T value) {
elements.put(type.name(), () -> type.codec().encodeStart(JsonOps.INSTANCE, value).getOrThrow().getAsJsonObject());
return this;
}
}
}

View File

@ -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"}
]
}

View File

@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 0,
"left": 12,
"right": 12,
"top": 0
},
"height": 20,
"width": 36
}
}
}

View File

@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 0,
"left": 12,
"right": 12,
"top": 0
},
"height": 20,
"width": 36
}
}
}

View File

@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 0,
"left": 12,
"right": 12,
"top": 0
},
"height": 20,
"width": 36
}
}
}

View File

@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 3,
"left": 3,
"right": 0,
"top": 4
},
"height": 14,
"width": 17
}
}
}

View File

@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 3,
"left": 3,
"right": 0,
"top": 4
},
"height": 14,
"width": 17
}
}
}

View File

@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 3,
"left": 3,
"right": 0,
"top": 4
},
"height": 14,
"width": 17
}
}
}

View File

@ -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"}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 136 B

View File

@ -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<String> whence, Optional<Long> 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();

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
);
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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:

View File

@ -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 = "<init>(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);
}
}

View File

@ -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<ItemFrameRenderer<?>>() {