Split some textures into sprite sheets
- Split buttons.png into individual textures.
 - Split corners_xyz.png into the following:
   - borders_xyz.png: A nine-sliced texture of the computer borders.
   - pocket_bottom_xyz.png: A horizontally 3-sliced texture of the
     bottom part of a pocket computer.
   - sidebar_xyz.png: A vertically 3-sliced texture of the computer
     sidebar.
While not splitting the sliced textures into smaller ones may seem a
little odd, it's consistent with what vanilla does in 1.20.2, and I
think will make editing them easier than juggling 9 textures.
I do want to make this more data-driven in the future, but that will
have to wait until the changes in 1.20.2.
This also adds a tools/update-resources.py program, which performs this
transformation on a given resource pack.
			
			
| @@ -21,6 +21,7 @@ import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | |||||||
| import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; | import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; | ||||||
| import dan200.computercraft.shared.media.items.DiskItem; | import dan200.computercraft.shared.media.items.DiskItem; | ||||||
| import dan200.computercraft.shared.media.items.TreasureDiskItem; | import dan200.computercraft.shared.media.items.TreasureDiskItem; | ||||||
|  | import net.minecraft.client.Minecraft; | ||||||
| import net.minecraft.client.color.item.ItemColor; | import net.minecraft.client.color.item.ItemColor; | ||||||
| import net.minecraft.client.gui.screens.MenuScreens; | import net.minecraft.client.gui.screens.MenuScreens; | ||||||
| import net.minecraft.client.multiplayer.ClientLevel; | import net.minecraft.client.multiplayer.ClientLevel; | ||||||
| @@ -30,6 +31,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; | |||||||
| import net.minecraft.client.renderer.item.ClampedItemPropertyFunction; | import net.minecraft.client.renderer.item.ClampedItemPropertyFunction; | ||||||
| import net.minecraft.client.renderer.item.ItemProperties; | import net.minecraft.client.renderer.item.ItemProperties; | ||||||
| import net.minecraft.resources.ResourceLocation; | import net.minecraft.resources.ResourceLocation; | ||||||
|  | import net.minecraft.server.packs.resources.PreparableReloadListener; | ||||||
| import net.minecraft.server.packs.resources.ResourceProvider; | import net.minecraft.server.packs.resources.ResourceProvider; | ||||||
| import net.minecraft.world.entity.LivingEntity; | import net.minecraft.world.entity.LivingEntity; | ||||||
| import net.minecraft.world.item.Item; | import net.minecraft.world.item.Item; | ||||||
| @@ -107,6 +109,10 @@ public final class ClientRegistry { | |||||||
|         for (var item : items) ItemProperties.register(item.get(), id, getter); |         for (var item : items) ItemProperties.register(item.get(), id, getter); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) { | ||||||
|  |         register.accept(GuiSprites.initialise(minecraft.getTextureManager())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private static final String[] EXTRA_MODELS = new String[]{ |     private static final String[] EXTRA_MODELS = new String[]{ | ||||||
|         "block/turtle_colour", |         "block/turtle_colour", | ||||||
|         "block/turtle_elf_overlay", |         "block/turtle_elf_overlay", | ||||||
|   | |||||||
| @@ -5,15 +5,18 @@ | |||||||
| package dan200.computercraft.client.gui; | package dan200.computercraft.client.gui; | ||||||
| 
 | 
 | ||||||
| import com.mojang.blaze3d.vertex.PoseStack; | import com.mojang.blaze3d.vertex.PoseStack; | ||||||
|  | import com.mojang.blaze3d.vertex.Tesselator; | ||||||
| import dan200.computercraft.client.gui.widgets.ComputerSidebar; | import dan200.computercraft.client.gui.widgets.ComputerSidebar; | ||||||
| import dan200.computercraft.client.gui.widgets.TerminalWidget; | import dan200.computercraft.client.gui.widgets.TerminalWidget; | ||||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | import dan200.computercraft.client.render.ComputerBorderRenderer; | ||||||
|  | import dan200.computercraft.client.render.RenderTypes; | ||||||
|  | import dan200.computercraft.client.render.SpriteRenderer; | ||||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||||
|  | import net.minecraft.client.renderer.MultiBufferSource; | ||||||
| import net.minecraft.network.chat.Component; | import net.minecraft.network.chat.Component; | ||||||
| import net.minecraft.world.entity.player.Inventory; | import net.minecraft.world.entity.player.Inventory; | ||||||
| 
 | 
 | ||||||
| import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; | import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; | ||||||
| import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A GUI for computers which renders the terminal (and border), but with no UI elements. |  * A GUI for computers which renders the terminal (and border), but with no UI elements. | ||||||
| @@ -39,10 +42,16 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra | |||||||
|     public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) { |     public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) { | ||||||
|         // Draw a border around the terminal |         // Draw a border around the terminal | ||||||
|         var terminal = getTerminal(); |         var terminal = getTerminal(); | ||||||
|  |         var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); | ||||||
|  | 
 | ||||||
|  |         var spriteRenderer = SpriteRenderer.createForGui(stack, buffers.getBuffer(RenderTypes.GUI_SPRITES)); | ||||||
|  |         var computerTextures = GuiSprites.getComputerTextures(family); | ||||||
|  | 
 | ||||||
|         ComputerBorderRenderer.render( |         ComputerBorderRenderer.render( | ||||||
|             stack.last().pose(), ComputerBorderRenderer.getTexture(family), terminal.getX(), terminal.getY(), |             spriteRenderer, computerTextures, | ||||||
|             FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight() |             terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false | ||||||
|         ); |         ); | ||||||
|         ComputerSidebar.renderBackground(stack, leftPos, topPos + sidebarYOffset); |         ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset); | ||||||
|  |         buffers.endBatch(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,127 @@ | |||||||
|  | // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||||
|  | // | ||||||
|  | // SPDX-License-Identifier: MPL-2.0 | ||||||
|  | 
 | ||||||
|  | package dan200.computercraft.client.gui; | ||||||
|  | 
 | ||||||
|  | import dan200.computercraft.api.ComputerCraftAPI; | ||||||
|  | import dan200.computercraft.client.render.ComputerBorderRenderer; | ||||||
|  | import dan200.computercraft.data.client.ClientDataProviders; | ||||||
|  | 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 javax.annotation.Nullable; | ||||||
|  | import java.util.Objects; | ||||||
|  | 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 = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui"); | ||||||
|  |     public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png"); | ||||||
|  | 
 | ||||||
|  |     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"); | ||||||
|  | 
 | ||||||
|  |     public static final ComputerTextures COMPUTER_NORMAL = computer("normal", true, true); | ||||||
|  |     public static final ComputerTextures COMPUTER_ADVANCED = computer("advanced", true, true); | ||||||
|  |     public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true); | ||||||
|  |     public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false); | ||||||
|  | 
 | ||||||
|  |     private static ButtonTextures button(String name) { | ||||||
|  |         return new ButtonTextures( | ||||||
|  |             new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name), | ||||||
|  |             new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static ComputerTextures computer(String name, boolean pocket, boolean sidebar) { | ||||||
|  |         return new ComputerTextures( | ||||||
|  |             new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/border_" + name), | ||||||
|  |             pocket ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/pocket_bottom_" + name) : null, | ||||||
|  |             sidebar ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sidebar_" + name) : null | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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. | ||||||
|  |      * | ||||||
|  |      * @param family The computer family. | ||||||
|  |      * @return The family-specific textures. | ||||||
|  |      */ | ||||||
|  |     public static ComputerTextures getComputerTextures(ComputerFamily family) { | ||||||
|  |         return switch (family) { | ||||||
|  |             case NORMAL -> COMPUTER_NORMAL; | ||||||
|  |             case ADVANCED -> COMPUTER_ADVANCED; | ||||||
|  |             case COMMAND -> COMPUTER_COMMAND; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A set of sprites for a button, with both a normal and "active" state. | ||||||
|  |      * | ||||||
|  |      * @param normal The normal texture for the button. | ||||||
|  |      * @param active The texture for the button when it is active (hovered or focused). | ||||||
|  |      */ | ||||||
|  |     public record ButtonTextures(ResourceLocation normal, ResourceLocation active) { | ||||||
|  |         public TextureAtlasSprite get(boolean active) { | ||||||
|  |             return GuiSprites.get(active ? this.active : normal); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public Stream<ResourceLocation> textures() { | ||||||
|  |             return Stream.of(normal, active); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set the set of sprites for a computer family. | ||||||
|  |      * | ||||||
|  |      * @param border       The texture for the computer's border. | ||||||
|  |      * @param pocketBottom The texture for the bottom of a pocket computer. | ||||||
|  |      * @param sidebar      The texture for the computer sidebar. | ||||||
|  |      * @see ComputerBorderRenderer | ||||||
|  |      * @see ClientDataProviders | ||||||
|  |      */ | ||||||
|  |     public record ComputerTextures( | ||||||
|  |         ResourceLocation border, | ||||||
|  |         @Nullable ResourceLocation pocketBottom, | ||||||
|  |         @Nullable ResourceLocation sidebar | ||||||
|  |     ) { | ||||||
|  |         public Stream<ResourceLocation> textures() { | ||||||
|  |             return Stream.of(border, pocketBottom, sidebar).filter(Objects::nonNull); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -6,13 +6,16 @@ package dan200.computercraft.client.gui; | |||||||
| 
 | 
 | ||||||
| import com.mojang.blaze3d.systems.RenderSystem; | import com.mojang.blaze3d.systems.RenderSystem; | ||||||
| import com.mojang.blaze3d.vertex.PoseStack; | import com.mojang.blaze3d.vertex.PoseStack; | ||||||
|  | import com.mojang.blaze3d.vertex.Tesselator; | ||||||
| import dan200.computercraft.api.ComputerCraftAPI; | import dan200.computercraft.api.ComputerCraftAPI; | ||||||
| import dan200.computercraft.client.gui.widgets.ComputerSidebar; | import dan200.computercraft.client.gui.widgets.ComputerSidebar; | ||||||
| import dan200.computercraft.client.gui.widgets.TerminalWidget; | import dan200.computercraft.client.gui.widgets.TerminalWidget; | ||||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | import dan200.computercraft.client.render.RenderTypes; | ||||||
|  | import dan200.computercraft.client.render.SpriteRenderer; | ||||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||||
| import dan200.computercraft.shared.turtle.inventory.TurtleMenu; | import dan200.computercraft.shared.turtle.inventory.TurtleMenu; | ||||||
|  | import net.minecraft.client.renderer.MultiBufferSource; | ||||||
| import net.minecraft.network.chat.Component; | import net.minecraft.network.chat.Component; | ||||||
| import net.minecraft.resources.ResourceLocation; | import net.minecraft.resources.ResourceLocation; | ||||||
| import net.minecraft.world.entity.player.Inventory; | import net.minecraft.world.entity.player.Inventory; | ||||||
| @@ -60,7 +63,10 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> { | |||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         RenderSystem.setShaderTexture(0, advanced ? ComputerBorderRenderer.BACKGROUND_ADVANCED : ComputerBorderRenderer.BACKGROUND_NORMAL); |         // Render sidebar | ||||||
|         ComputerSidebar.renderBackground(transform, leftPos, topPos + sidebarYOffset); |         var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); | ||||||
|  |         var spriteRenderer = SpriteRenderer.createForGui(transform, buffers.getBuffer(RenderTypes.GUI_SPRITES)); | ||||||
|  |         ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset); | ||||||
|  |         buffers.endBatch(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,16 +4,13 @@ | |||||||
| 
 | 
 | ||||||
| package dan200.computercraft.client.gui.widgets; | package dan200.computercraft.client.gui.widgets; | ||||||
| 
 | 
 | ||||||
| import com.mojang.blaze3d.vertex.PoseStack; | import dan200.computercraft.client.gui.GuiSprites; | ||||||
| import dan200.computercraft.api.ComputerCraftAPI; |  | ||||||
| import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage; | import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage; | ||||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | import dan200.computercraft.client.render.SpriteRenderer; | ||||||
| import dan200.computercraft.shared.computer.core.InputHandler; | import dan200.computercraft.shared.computer.core.InputHandler; | ||||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||||
| import net.minecraft.client.gui.components.AbstractWidget; | import net.minecraft.client.gui.components.AbstractWidget; | ||||||
| import net.minecraft.client.gui.screens.Screen; |  | ||||||
| import net.minecraft.network.chat.Component; | import net.minecraft.network.chat.Component; | ||||||
| import net.minecraft.resources.ResourceLocation; |  | ||||||
| 
 | 
 | ||||||
| import java.util.function.BooleanSupplier; | import java.util.function.BooleanSupplier; | ||||||
| import java.util.function.Consumer; | import java.util.function.Consumer; | ||||||
| @@ -22,22 +19,18 @@ import java.util.function.Consumer; | |||||||
|  * Registers buttons to interact with a computer. |  * Registers buttons to interact with a computer. | ||||||
|  */ |  */ | ||||||
| public final class ComputerSidebar { | public final class ComputerSidebar { | ||||||
|     private static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/buttons.png"); |  | ||||||
| 
 |  | ||||||
|     private static final int TEX_SIZE = 64; |  | ||||||
| 
 |  | ||||||
|     private static final int ICON_WIDTH = 12; |     private static final int ICON_WIDTH = 12; | ||||||
|     private static final int ICON_HEIGHT = 12; |     private static final int ICON_HEIGHT = 12; | ||||||
|     private static final int ICON_MARGIN = 2; |     private static final int ICON_MARGIN = 2; | ||||||
| 
 | 
 | ||||||
|     private static final int ICON_TEX_Y_DIFF = 14; |  | ||||||
| 
 |  | ||||||
|     private static final int CORNERS_BORDER = 3; |     private static final int CORNERS_BORDER = 3; | ||||||
|     private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN; |     private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN; | ||||||
| 
 | 
 | ||||||
|     private static final int BUTTONS = 2; |     private static final int BUTTONS = 2; | ||||||
|     private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2; |     private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2; | ||||||
| 
 | 
 | ||||||
|  |     private static final int TEX_HEIGHT = 14; | ||||||
|  | 
 | ||||||
|     private ComputerSidebar() { |     private ComputerSidebar() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -51,16 +44,18 @@ public final class ComputerSidebar { | |||||||
|             Component.translatable("gui.computercraft.tooltip.turn_off.key") |             Component.translatable("gui.computercraft.tooltip.turn_off.key") | ||||||
|         ); |         ); | ||||||
|         add.accept(new DynamicImageButton( |         add.accept(new DynamicImageButton( | ||||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF, |             x, y, ICON_WIDTH, ICON_HEIGHT, | ||||||
|             TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input), |             h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h), | ||||||
|  |             b -> toggleComputer(isOn, input), | ||||||
|             () -> isOn.getAsBoolean() ? turnOff : turnOn |             () -> isOn.getAsBoolean() ? turnOff : turnOn | ||||||
|         )); |         )); | ||||||
| 
 | 
 | ||||||
|         y += ICON_HEIGHT + ICON_MARGIN * 2; |         y += ICON_HEIGHT + ICON_MARGIN * 2; | ||||||
| 
 | 
 | ||||||
|         add.accept(new DynamicImageButton( |         add.accept(new DynamicImageButton( | ||||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF, |             x, y, ICON_WIDTH, ICON_HEIGHT, | ||||||
|             TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"), |             GuiSprites.TERMINATE::get, | ||||||
|  |             b -> input.queueEvent("terminate"), | ||||||
|             new HintedMessage( |             new HintedMessage( | ||||||
|                 Component.translatable("gui.computercraft.tooltip.terminate"), |                 Component.translatable("gui.computercraft.tooltip.terminate"), | ||||||
|                 Component.translatable("gui.computercraft.tooltip.terminate.key") |                 Component.translatable("gui.computercraft.tooltip.terminate.key") | ||||||
| @@ -68,22 +63,12 @@ public final class ComputerSidebar { | |||||||
|         )); |         )); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static void renderBackground(PoseStack transform, int x, int y) { |     public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) { | ||||||
|         Screen.blit(transform, |         var texture = textures.sidebar(); | ||||||
|             x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER, |         if (texture == null) throw new NullPointerException(textures + " has no sidebar texture"); | ||||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE |         var sprite = GuiSprites.get(texture); | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         Screen.blit(transform, |         renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT); | ||||||
|             x, y + FULL_BORDER, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT - FULL_BORDER * 2, |  | ||||||
|             0, 107, AbstractComputerMenu.SIDEBAR_WIDTH, 4, |  | ||||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         Screen.blit(transform, |  | ||||||
|             x, y + HEIGHT - FULL_BORDER, 0, 111, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER, |  | ||||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static void toggleComputer(BooleanSupplier isOn, InputHandler input) { |     private static void toggleComputer(BooleanSupplier isOn, InputHandler input) { | ||||||
|   | |||||||
| @@ -6,14 +6,14 @@ package dan200.computercraft.client.gui.widgets; | |||||||
| 
 | 
 | ||||||
| import com.mojang.blaze3d.systems.RenderSystem; | import com.mojang.blaze3d.systems.RenderSystem; | ||||||
| import com.mojang.blaze3d.vertex.PoseStack; | import com.mojang.blaze3d.vertex.PoseStack; | ||||||
|  | import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction; | ||||||
| import net.minecraft.ChatFormatting; | import net.minecraft.ChatFormatting; | ||||||
| import net.minecraft.client.gui.components.Button; | import net.minecraft.client.gui.components.Button; | ||||||
| import net.minecraft.client.gui.components.Tooltip; | import net.minecraft.client.gui.components.Tooltip; | ||||||
|  | import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||||
| import net.minecraft.network.chat.Component; | import net.minecraft.network.chat.Component; | ||||||
| import net.minecraft.resources.ResourceLocation; |  | ||||||
| 
 | 
 | ||||||
| import javax.annotation.Nullable; | import javax.annotation.Nullable; | ||||||
| import java.util.function.IntSupplier; |  | ||||||
| import java.util.function.Supplier; | import java.util.function.Supplier; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @@ -21,50 +21,33 @@ import java.util.function.Supplier; | |||||||
|  * dynamically. |  * dynamically. | ||||||
|  */ |  */ | ||||||
| public class DynamicImageButton extends Button { | public class DynamicImageButton extends Button { | ||||||
|     private final ResourceLocation texture; |     private final Boolean2ObjectFunction<TextureAtlasSprite> texture; | ||||||
|     private final IntSupplier xTexStart; |  | ||||||
|     private final int yTexStart; |  | ||||||
|     private final int yDiffTex; |  | ||||||
|     private final int textureWidth; |  | ||||||
|     private final int textureHeight; |  | ||||||
|     private final Supplier<HintedMessage> message; |     private final Supplier<HintedMessage> message; | ||||||
| 
 | 
 | ||||||
|     public DynamicImageButton( |     public DynamicImageButton( | ||||||
|         int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex, |         int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress, | ||||||
|         ResourceLocation texture, int textureWidth, int textureHeight, |         HintedMessage message | ||||||
|         OnPress onPress, HintedMessage message |  | ||||||
|     ) { |     ) { | ||||||
|         this( |         this(x, y, width, height, texture, onPress, () -> message); | ||||||
|             x, y, width, height, () -> xTexStart, yTexStart, yDiffTex, |  | ||||||
|             texture, textureWidth, textureHeight, |  | ||||||
|             onPress, () -> message |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public DynamicImageButton( |     public DynamicImageButton( | ||||||
|         int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex, |         int x, int y, int width, int height, | ||||||
|         ResourceLocation texture, int textureWidth, int textureHeight, |         Boolean2ObjectFunction<TextureAtlasSprite> texture, | ||||||
|         OnPress onPress, Supplier<HintedMessage> message |         OnPress onPress, Supplier<HintedMessage> message | ||||||
|     ) { |     ) { | ||||||
|         super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION); |         super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION); | ||||||
|         this.textureWidth = textureWidth; |  | ||||||
|         this.textureHeight = textureHeight; |  | ||||||
|         this.xTexStart = xTexStart; |  | ||||||
|         this.yTexStart = yTexStart; |  | ||||||
|         this.yDiffTex = yDiffTex; |  | ||||||
|         this.texture = texture; |         this.texture = texture; | ||||||
|         this.message = message; |         this.message = message; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void renderWidget(PoseStack stack, int mouseX, int mouseY, float partialTicks) { |     public void renderWidget(PoseStack stack, int mouseX, int mouseY, float partialTicks) { | ||||||
|         RenderSystem.setShaderTexture(0, texture); |         var texture = this.texture.get(isHoveredOrFocused()); | ||||||
|  |         RenderSystem.setShaderTexture(0, texture.atlasLocation()); | ||||||
|         RenderSystem.disableDepthTest(); |         RenderSystem.disableDepthTest(); | ||||||
| 
 | 
 | ||||||
|         var yTex = yTexStart; |         blit(stack, getX(), getY(), 0, width, height, texture); | ||||||
|         if (isHoveredOrFocused()) yTex += yDiffTex; |  | ||||||
| 
 |  | ||||||
|         blit(stack, getX(), getY(), xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight); |  | ||||||
|         RenderSystem.enableDepthTest(); |         RenderSystem.enableDepthTest(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -4,25 +4,17 @@ | |||||||
| 
 | 
 | ||||||
| package dan200.computercraft.client.render; | package dan200.computercraft.client.render; | ||||||
| 
 | 
 | ||||||
| import com.mojang.blaze3d.vertex.Tesselator; | import dan200.computercraft.client.gui.GuiSprites; | ||||||
| import com.mojang.blaze3d.vertex.VertexConsumer; | import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||||
| import dan200.computercraft.api.ComputerCraftAPI; | 
 | ||||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | import static dan200.computercraft.client.render.SpriteRenderer.u; | ||||||
| import net.minecraft.client.renderer.MultiBufferSource; | import static dan200.computercraft.client.render.SpriteRenderer.v; | ||||||
| import net.minecraft.client.renderer.RenderType; |  | ||||||
| import net.minecraft.resources.ResourceLocation; |  | ||||||
| import org.joml.Matrix4f; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or |  * Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or | ||||||
|  * {@linkplain PocketItemRenderer in-hand pocket computers}. |  * {@linkplain PocketItemRenderer in-hand pocket computers}. | ||||||
|  */ |  */ | ||||||
| public class ComputerBorderRenderer { | public final class ComputerBorderRenderer { | ||||||
|     public static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_normal.png"); |  | ||||||
|     public static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_advanced.png"); |  | ||||||
|     public static final ResourceLocation BACKGROUND_COMMAND = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_command.png"); |  | ||||||
|     public static final ResourceLocation BACKGROUND_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_colour.png"); |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * The margin between the terminal and its border. |      * The margin between the terminal and its border. | ||||||
|      */ |      */ | ||||||
| @@ -33,100 +25,51 @@ public class ComputerBorderRenderer { | |||||||
|      */ |      */ | ||||||
|     public static final int BORDER = 12; |     public static final int BORDER = 12; | ||||||
| 
 | 
 | ||||||
|     private static final int CORNER_TOP_Y = 28; |  | ||||||
|     private static final int CORNER_BOTTOM_Y = CORNER_TOP_Y + BORDER; |  | ||||||
|     private static final int CORNER_LEFT_X = BORDER; |  | ||||||
|     private static final int CORNER_RIGHT_X = CORNER_LEFT_X + BORDER; |  | ||||||
|     private static final int BORDER_RIGHT_X = 36; |  | ||||||
|     private static final int LIGHT_BORDER_Y = 56; |  | ||||||
|     private static final int LIGHT_CORNER_Y = 80; |  | ||||||
| 
 |  | ||||||
|     public static final int LIGHT_HEIGHT = 8; |     public static final int LIGHT_HEIGHT = 8; | ||||||
| 
 | 
 | ||||||
|     public static final int TEX_SIZE = 256; |     private static final int TEX_SIZE = 36; | ||||||
|     private static final float TEX_SCALE = 1 / (float) TEX_SIZE; |  | ||||||
| 
 | 
 | ||||||
|     private final Matrix4f transform; |     private ComputerBorderRenderer() { | ||||||
|     private final VertexConsumer builder; |  | ||||||
|     private final int light; |  | ||||||
|     private final int z; |  | ||||||
|     private final float r, g, b; |  | ||||||
| 
 |  | ||||||
|     public ComputerBorderRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, float r, float g, float b) { |  | ||||||
|         this.transform = transform; |  | ||||||
|         this.builder = builder; |  | ||||||
|         this.z = z; |  | ||||||
|         this.light = light; |  | ||||||
|         this.r = r; |  | ||||||
|         this.g = g; |  | ||||||
|         this.b = b; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static ResourceLocation getTexture(ComputerFamily family) { |     public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) { | ||||||
|         return switch (family) { |  | ||||||
|             case NORMAL -> BACKGROUND_NORMAL; |  | ||||||
|             case ADVANCED -> BACKGROUND_ADVANCED; |  | ||||||
|             case COMMAND -> BACKGROUND_COMMAND; |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static RenderType getRenderType(ResourceLocation location) { |  | ||||||
|         // See note in RenderTypes about why we use text rather than anything intuitive. |  | ||||||
|         return RenderType.text(location); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void render(Matrix4f transform, ResourceLocation location, int x, int y, int light, int width, int height) { |  | ||||||
|         var source = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); |  | ||||||
|         render(transform, source.getBuffer(getRenderType(location)), x, y, 1, light, width, height, false, 1, 1, 1); |  | ||||||
|         source.endBatch(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void render(Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int light, int width, int height, boolean withLight, float r, float g, float b) { |  | ||||||
|         new ComputerBorderRenderer(transform, buffer, z, light, r, g, b).doRender(x, y, width, height, withLight); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void doRender(int x, int y, int width, int height, boolean withLight) { |  | ||||||
|         var endX = x + width; |         var endX = x + width; | ||||||
|         var endY = y + height; |         var endY = y + height; | ||||||
| 
 | 
 | ||||||
|         // Vertical bars |         var border = GuiSprites.get(textures.border()); | ||||||
|         renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y); |  | ||||||
|         renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y); |  | ||||||
| 
 | 
 | ||||||
|         // Top bar |         // Top bar | ||||||
|         renderLine(x, y - BORDER, 0, 0, endX - x, BORDER); |         blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER); | ||||||
|         renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y); |         blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER); | ||||||
|         renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y); |         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 |         // Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the | ||||||
|         // pocket computer's lights). |         // pocket computer's lights). | ||||||
|         if (withLight) { |         if (withLight) { | ||||||
|             renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT); |             var pocketBottomTexture = textures.pocketBottom(); | ||||||
|             renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); |             if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture"); | ||||||
|             renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); |             var pocketBottom = GuiSprites.get(pocketBottomTexture); | ||||||
|  | 
 | ||||||
|  |             renderer.blitHorizontalSliced( | ||||||
|  |                 pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT, | ||||||
|  |                 BORDER, BORDER, BORDER * 3 | ||||||
|  |             ); | ||||||
|         } else { |         } else { | ||||||
|             renderLine(x, endY, 0, BORDER, endX - x, BORDER); |             blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER); | ||||||
|             renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y); |             blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER); | ||||||
|             renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y); |             blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void renderCorner(int x, int y, int u, int v) { |     private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) { | ||||||
|         renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER); |         renderer.blit( | ||||||
|     } |             x, y, width, height, | ||||||
| 
 |             u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE), | ||||||
|     private void renderLine(int x, int y, int u, int v, int width, int height) { |             u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE) | ||||||
|         renderTexture(x, y, u, v, width, height, BORDER, BORDER); |         ); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void renderTexture(int x, int y, int u, int v, int width, int height) { |  | ||||||
|         renderTexture(x, y, u, v, width, height, width, height); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void renderTexture(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) { |  | ||||||
|         builder.vertex(transform, x, y + height, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex(); |  | ||||||
|         builder.vertex(transform, x + width, y + height, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex(); |  | ||||||
|         builder.vertex(transform, x + width, y, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex(); |  | ||||||
|         builder.vertex(transform, x, y, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package dan200.computercraft.client.render; | |||||||
| 
 | 
 | ||||||
| import com.mojang.blaze3d.vertex.PoseStack; | import com.mojang.blaze3d.vertex.PoseStack; | ||||||
| import com.mojang.math.Axis; | import com.mojang.math.Axis; | ||||||
|  | import dan200.computercraft.client.gui.GuiSprites; | ||||||
| import dan200.computercraft.client.pocket.ClientPocketComputers; | import dan200.computercraft.client.pocket.ClientPocketComputers; | ||||||
| import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | ||||||
| import dan200.computercraft.core.util.Colour; | import dan200.computercraft.core.util.Colour; | ||||||
| @@ -72,13 +73,14 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) { |     private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) { | ||||||
|         var texture = colour != -1 ? ComputerBorderRenderer.BACKGROUND_COLOUR : ComputerBorderRenderer.getTexture(family); |         var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family); | ||||||
| 
 | 
 | ||||||
|         var r = ((colour >>> 16) & 0xFF) / 255.0f; |         var r = (colour >>> 16) & 0xFF; | ||||||
|         var g = ((colour >>> 8) & 0xFF) / 255.0f; |         var g = (colour >>> 8) & 0xFF; | ||||||
|         var b = (colour & 0xFF) / 255.0f; |         var b = colour & 0xFF; | ||||||
| 
 | 
 | ||||||
|         ComputerBorderRenderer.render(transform, render.getBuffer(ComputerBorderRenderer.getRenderType(texture)), 0, 0, 0, light, width, height, true, r, g, b); |         var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b); | ||||||
|  |         ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) { |     private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) { | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ package dan200.computercraft.client.render; | |||||||
| import com.mojang.blaze3d.vertex.DefaultVertexFormat; | import com.mojang.blaze3d.vertex.DefaultVertexFormat; | ||||||
| import com.mojang.blaze3d.vertex.VertexFormat; | import com.mojang.blaze3d.vertex.VertexFormat; | ||||||
| import dan200.computercraft.api.ComputerCraftAPI; | import dan200.computercraft.api.ComputerCraftAPI; | ||||||
|  | import dan200.computercraft.client.gui.GuiSprites; | ||||||
| import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader; | import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader; | ||||||
| import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | ||||||
| import net.minecraft.client.renderer.GameRenderer; | import net.minecraft.client.renderer.GameRenderer; | ||||||
| @@ -53,6 +54,11 @@ public class RenderTypes { | |||||||
|      */ |      */ | ||||||
|     public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(new ResourceLocation("computercraft", "textures/gui/printout.png")); |     public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(new ResourceLocation("computercraft", "textures/gui/printout.png")); | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Render type for {@linkplain GuiSprites GUI sprites}. | ||||||
|  |      */ | ||||||
|  |     public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE); | ||||||
|  | 
 | ||||||
|     public static MonitorTextureBufferShader getMonitorTextureBufferShader() { |     public static MonitorTextureBufferShader getMonitorTextureBufferShader() { | ||||||
|         if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered"); |         if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered"); | ||||||
|         return monitorTboShader; |         return monitorTboShader; | ||||||
|   | |||||||
| @@ -0,0 +1,131 @@ | |||||||
|  | // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||||
|  | // | ||||||
|  | // SPDX-License-Identifier: MPL-2.0 | ||||||
|  | 
 | ||||||
|  | package dan200.computercraft.client.render; | ||||||
|  | 
 | ||||||
|  | import com.mojang.blaze3d.vertex.PoseStack; | ||||||
|  | import com.mojang.blaze3d.vertex.VertexConsumer; | ||||||
|  | import net.minecraft.client.gui.GuiComponent; | ||||||
|  | import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||||
|  | import org.joml.Matrix4f; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A {@link GuiComponent}-equivalent which is suitable for both rendering in to a GUI and in-world (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 { | ||||||
|  |     private final Matrix4f transform; | ||||||
|  |     private final VertexConsumer builder; | ||||||
|  |     private final int light; | ||||||
|  |     private final int z; | ||||||
|  |     private final int r, g, b; | ||||||
|  | 
 | ||||||
|  |     public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) { | ||||||
|  |         this.transform = transform; | ||||||
|  |         this.builder = builder; | ||||||
|  |         this.z = z; | ||||||
|  |         this.light = light; | ||||||
|  |         this.r = r; | ||||||
|  |         this.g = g; | ||||||
|  |         this.b = b; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static SpriteRenderer createForGui(PoseStack stack, VertexConsumer builder) { | ||||||
|  |         return new SpriteRenderer(stack.last().pose(), builder, 0, RenderTypes.FULL_BRIGHT_LIGHTMAP, 255, 255, 255); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiComponent#blitNineSliced}, | ||||||
|  |      * the middle texture is stretched rather than repeated. | ||||||
|  |      * | ||||||
|  |      * @param sprite       The texture to draw. | ||||||
|  |      * @param x            The x position of the rectangle we'll draw. | ||||||
|  |      * @param y            The x position of the rectangle we'll draw. | ||||||
|  |      * @param width        The width of the rectangle we'll draw. | ||||||
|  |      * @param height       The height of the rectangle we'll draw. | ||||||
|  |      * @param leftBorder   The width of the left border. | ||||||
|  |      * @param rightBorder  The width of the right border. | ||||||
|  |      * @param textureWidth The width of the whole texture. | ||||||
|  |      */ | ||||||
|  |     public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) { | ||||||
|  |         // TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does. | ||||||
|  |         if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders"); | ||||||
|  | 
 | ||||||
|  |         var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth); | ||||||
|  |         var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth); | ||||||
|  | 
 | ||||||
|  |         blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1()); | ||||||
|  |         blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1()); | ||||||
|  |         blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiComponent#blitNineSliced}, | ||||||
|  |      * the middle texture is stretched rather than repeated. | ||||||
|  |      * | ||||||
|  |      * @param sprite        The texture to draw. | ||||||
|  |      * @param x             The x position of the rectangle we'll draw. | ||||||
|  |      * @param y             The x position of the rectangle we'll draw. | ||||||
|  |      * @param width         The width of the rectangle we'll draw. | ||||||
|  |      * @param height        The height of the rectangle we'll draw. | ||||||
|  |      * @param topBorder     The height of the top border. | ||||||
|  |      * @param bottomBorder  The height of the bottom border. | ||||||
|  |      * @param textureHeight The height of the whole texture. | ||||||
|  |      */ | ||||||
|  |     public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) { | ||||||
|  |         // TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does. | ||||||
|  |         if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders"); | ||||||
|  | 
 | ||||||
|  |         var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight); | ||||||
|  |         var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight); | ||||||
|  | 
 | ||||||
|  |         blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart); | ||||||
|  |         blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd); | ||||||
|  |         blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite. | ||||||
|  |      * | ||||||
|  |      * @param x      The x position of the rectangle we'll draw. | ||||||
|  |      * @param y      The x position of the rectangle we'll draw. | ||||||
|  |      * @param width  The width of the rectangle we'll draw. | ||||||
|  |      * @param height The height of the rectangle we'll draw. | ||||||
|  |      * @param u0     The first U coordinate. | ||||||
|  |      * @param v0     The first V coordinate. | ||||||
|  |      * @param u1     The second U coordinate. | ||||||
|  |      * @param v1     The second V coordinate. | ||||||
|  |      */ | ||||||
|  |     public void blit( | ||||||
|  |         int x, int y, int width, int height, float u0, float v0, float u1, float v1) { | ||||||
|  |         builder.vertex(transform, x, y + height, z).color(r, g, b, 255).uv(u0, v1).uv2(light).endVertex(); | ||||||
|  |         builder.vertex(transform, x + width, y + height, z).color(r, g, b, 255).uv(u1, v1).uv2(light).endVertex(); | ||||||
|  |         builder.vertex(transform, x + width, y, z).color(r, g, b, 255).uv(u1, v0).uv2(light).endVertex(); | ||||||
|  |         builder.vertex(transform, x, y, z).color(r, g, b, 255).uv(u0, v0).uv2(light).endVertex(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static float u(TextureAtlasSprite sprite, int x, int width) { | ||||||
|  |         return sprite.getU((double) x / width * 16); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static float v(TextureAtlasSprite sprite, int y, int height) { | ||||||
|  |         return sprite.getV((double) y / height * 16); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,8 +4,10 @@ | |||||||
| 
 | 
 | ||||||
| package dan200.computercraft.data.client; | package dan200.computercraft.data.client; | ||||||
| 
 | 
 | ||||||
|  | import dan200.computercraft.client.gui.GuiSprites; | ||||||
| import dan200.computercraft.data.DataProviders; | import dan200.computercraft.data.DataProviders; | ||||||
| import dan200.computercraft.shared.turtle.inventory.UpgradeSlot; | import dan200.computercraft.shared.turtle.inventory.UpgradeSlot; | ||||||
|  | import net.minecraft.client.renderer.texture.atlas.SpriteSource; | ||||||
| import net.minecraft.client.renderer.texture.atlas.SpriteSources; | import net.minecraft.client.renderer.texture.atlas.SpriteSources; | ||||||
| import net.minecraft.client.renderer.texture.atlas.sources.SingleFile; | import net.minecraft.client.renderer.texture.atlas.sources.SingleFile; | ||||||
| import net.minecraft.resources.ResourceLocation; | import net.minecraft.resources.ResourceLocation; | ||||||
| @@ -13,6 +15,7 @@ import net.minecraft.server.packs.PackType; | |||||||
| 
 | 
 | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  | import java.util.stream.Stream; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A version of {@link DataProviders} which relies on client-side classes. |  * A version of {@link DataProviders} which relies on client-side classes. | ||||||
| @@ -29,6 +32,17 @@ public final class ClientDataProviders { | |||||||
|                 new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()), |                 new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()), | ||||||
|                 new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()) |                 new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()) | ||||||
|             )); |             )); | ||||||
|  |             out.accept(GuiSprites.SPRITE_SHEET, Stream.of( | ||||||
|  |                 // Buttons | ||||||
|  |                 GuiSprites.TURNED_OFF.textures(), | ||||||
|  |                 GuiSprites.TURNED_ON.textures(), | ||||||
|  |                 GuiSprites.TERMINATE.textures(), | ||||||
|  |                 // Computers | ||||||
|  |                 GuiSprites.COMPUTER_NORMAL.textures(), | ||||||
|  |                 GuiSprites.COMPUTER_ADVANCED.textures(), | ||||||
|  |                 GuiSprites.COMPUTER_COMMAND.textures(), | ||||||
|  |                 GuiSprites.COMPUTER_COLOUR.textures() | ||||||
|  |             ).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList()); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| After Width: | Height: | Size: 334 B | 
| After Width: | Height: | Size: 294 B | 
| After Width: | Height: | Size: 300 B | 
| After Width: | Height: | Size: 299 B | 
| Before Width: | Height: | Size: 303 B | 
| After Width: | Height: | Size: 145 B | 
| After Width: | Height: | Size: 144 B | 
| After Width: | Height: | Size: 145 B | 
| After Width: | Height: | Size: 145 B | 
| After Width: | Height: | Size: 146 B | 
| After Width: | Height: | Size: 146 B | 
| Before Width: | Height: | Size: 405 B | 
| Before Width: | Height: | Size: 352 B | 
| Before Width: | Height: | Size: 345 B | 
| Before Width: | Height: | Size: 399 B | 
| After Width: | Height: | Size: 223 B | 
| After Width: | Height: | Size: 211 B | 
| After Width: | Height: | Size: 216 B | 
| After Width: | Height: | Size: 148 B | 
| After Width: | Height: | Size: 141 B | 
| After Width: | Height: | Size: 141 B | 
| @@ -5,16 +5,25 @@ | |||||||
| package dan200.computercraft.mixin.client; | package dan200.computercraft.mixin.client; | ||||||
| 
 | 
 | ||||||
| import dan200.computercraft.client.ClientHooks; | import dan200.computercraft.client.ClientHooks; | ||||||
|  | import dan200.computercraft.client.ClientRegistry; | ||||||
| import net.minecraft.client.Minecraft; | import net.minecraft.client.Minecraft; | ||||||
| import net.minecraft.client.gui.screens.Screen; | import net.minecraft.client.gui.screens.Screen; | ||||||
|  | import net.minecraft.client.main.GameConfig; | ||||||
| import net.minecraft.client.multiplayer.ClientLevel; | 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.Mixin; | ||||||
|  | import org.spongepowered.asm.mixin.Shadow; | ||||||
| import org.spongepowered.asm.mixin.injection.At; | import org.spongepowered.asm.mixin.injection.At; | ||||||
| import org.spongepowered.asm.mixin.injection.Inject; | import org.spongepowered.asm.mixin.injection.Inject; | ||||||
| import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; | ||||||
| 
 | 
 | ||||||
| @Mixin(Minecraft.class) | @Mixin(Minecraft.class) | ||||||
| class MinecraftMixin { | class MinecraftMixin { | ||||||
|  |     @Shadow | ||||||
|  |     @Final | ||||||
|  |     private ReloadableResourceManager resourceManager; | ||||||
|  | 
 | ||||||
|     @Inject(method = "clearLevel(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("HEAD")) |     @Inject(method = "clearLevel(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("HEAD")) | ||||||
|     @SuppressWarnings("UnusedMethod") |     @SuppressWarnings("UnusedMethod") | ||||||
|     private void clearLevel(Screen screen, CallbackInfo ci) { |     private void clearLevel(Screen screen, CallbackInfo ci) { | ||||||
| @@ -26,4 +35,16 @@ class MinecraftMixin { | |||||||
|     private void setLevel(ClientLevel screen, CallbackInfo ci) { |     private void setLevel(ClientLevel screen, CallbackInfo ci) { | ||||||
|         ClientHooks.onWorldUnload(); |         ClientHooks.onWorldUnload(); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @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 | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |     public void beforeInitialResourceReload(GameConfig gameConfig, CallbackInfo ci) { | ||||||
|  |         ClientRegistry.registerReloadListeners(resourceManager::registerReloadListener, (Minecraft) (Object) this); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								projects/fabric/src/generated/resources/assets/computercraft/atlases/gui.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | |||||||
|  | { | ||||||
|  |   "sources": [ | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"}, | ||||||
|  |     {"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"} | ||||||
|  |   ] | ||||||
|  | } | ||||||
| @@ -6,8 +6,10 @@ package dan200.computercraft.client; | |||||||
| 
 | 
 | ||||||
| import dan200.computercraft.api.ComputerCraftAPI; | import dan200.computercraft.api.ComputerCraftAPI; | ||||||
| import dan200.computercraft.client.model.turtle.TurtleModelLoader; | import dan200.computercraft.client.model.turtle.TurtleModelLoader; | ||||||
|  | import net.minecraft.client.Minecraft; | ||||||
| import net.minecraftforge.api.distmarker.Dist; | import net.minecraftforge.api.distmarker.Dist; | ||||||
| import net.minecraftforge.client.event.ModelEvent; | import net.minecraftforge.client.event.ModelEvent; | ||||||
|  | import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; | ||||||
| import net.minecraftforge.client.event.RegisterColorHandlersEvent; | import net.minecraftforge.client.event.RegisterColorHandlersEvent; | ||||||
| import net.minecraftforge.client.event.RegisterShadersEvent; | import net.minecraftforge.client.event.RegisterShadersEvent; | ||||||
| import net.minecraftforge.eventbus.api.SubscribeEvent; | import net.minecraftforge.eventbus.api.SubscribeEvent; | ||||||
| @@ -44,6 +46,11 @@ public final class ForgeClientRegistry { | |||||||
|         ClientRegistry.registerItemColours(event::register); |         ClientRegistry.registerItemColours(event::register); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @SubscribeEvent | ||||||
|  |     public static void registerReloadListeners(RegisterClientReloadListenersEvent event) { | ||||||
|  |         ClientRegistry.registerReloadListeners(event::registerReloadListener, Minecraft.getInstance()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @SubscribeEvent |     @SubscribeEvent | ||||||
|     public static void setupClient(FMLClientSetupEvent event) { |     public static void setupClient(FMLClientSetupEvent event) { | ||||||
|         ClientRegistry.register(); |         ClientRegistry.register(); | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								projects/forge/src/generated/resources/assets/computercraft/atlases/gui.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | |||||||
|  | { | ||||||
|  |   "sources": [ | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"}, | ||||||
|  |     {"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"}, | ||||||
|  |     {"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"} | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										115
									
								
								tools/update-resources.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,115 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | # SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||||
|  | # | ||||||
|  | # SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Upgrades textures to newer resource pack formats. | ||||||
|  |  | ||||||
|  | Currently implemented transformations: | ||||||
|  |  - Split gui/corners_*.png and gui/buttons.png textures into smaller textures. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from PIL import Image | ||||||
|  | import os | ||||||
|  | import pathlib | ||||||
|  | import argparse | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def box(x: int, y: int, w: int, h: int): | ||||||
|  |     return (x, y, x + w, y + h) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def unstitch_buttons(input_file: pathlib.Path): | ||||||
|  |     """Unstitch the button texture,""" | ||||||
|  |     buttons = Image.open(input_file) | ||||||
|  |  | ||||||
|  |     output_dir = input_file.parent / "buttons" | ||||||
|  |     output_dir.mkdir(exist_ok=True) | ||||||
|  |  | ||||||
|  |     buttons.crop(box(1, 1, 12, 12)).save(output_dir / "turned_off.png") | ||||||
|  |     buttons.crop(box(1, 15, 12, 12)).save(output_dir / "turned_off_hover.png") | ||||||
|  |  | ||||||
|  |     buttons.crop(box(15, 1, 12, 12)).save(output_dir / "turned_on.png") | ||||||
|  |     buttons.crop(box(15, 15, 12, 12)).save(output_dir / "turned_on_hover.png") | ||||||
|  |  | ||||||
|  |     buttons.crop(box(29, 1, 12, 12)).save(output_dir / "terminate.png") | ||||||
|  |     buttons.crop(box(29, 15, 12, 12)).save(output_dir / "terminate_hover.png") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def unstitch_corners(input_file: pathlib.Path, family: str): | ||||||
|  |     """Unstitch the corners texture.""" | ||||||
|  |  | ||||||
|  |     input = Image.open(input_file) | ||||||
|  |     output_dir = input_file.parent | ||||||
|  |  | ||||||
|  |     border = Image.new("RGBA", (36, 36)) | ||||||
|  |     # Corners | ||||||
|  |     border.paste(input.crop(box(12, 28, 12, 12)), box(00, 00, 12, 12)) | ||||||
|  |     border.paste(input.crop(box(24, 28, 12, 12)), box(24, 00, 12, 12)) | ||||||
|  |     border.paste(input.crop(box(12, 40, 12, 12)), box(00, 24, 12, 12)) | ||||||
|  |     border.paste(input.crop(box(24, 40, 12, 12)), box(24, 24, 12, 12)) | ||||||
|  |     # Horizontal bars | ||||||
|  |     border.paste(input.crop(box(00, 00, 12, 12)), box(12, 00, 12, 12)) | ||||||
|  |     border.paste(input.crop(box(00, 12, 12, 12)), box(12, 24, 12, 12)) | ||||||
|  |     # Vertical bars | ||||||
|  |     border.paste(input.crop(box(00, 28, 12, 12)), box(00, 12, 12, 12)) | ||||||
|  |     border.paste(input.crop(box(36, 28, 12, 12)), box(24, 12, 12, 12)) | ||||||
|  |  | ||||||
|  |     border.save(output_dir / f"border_{family}.png") | ||||||
|  |  | ||||||
|  |     if family != "command": | ||||||
|  |         # Fatter bottom pocket computer border. | ||||||
|  |         pocket_computer = Image.new("RGBA", (36, 20)) | ||||||
|  |         # Middle | ||||||
|  |         pocket_computer.paste(input.crop(box(00, 56, 12, 20)), box(12, 0, 12, 20)) | ||||||
|  |         # Corners | ||||||
|  |         pocket_computer.paste(input.crop(box(12, 80, 12, 20)), box(00, 0, 12, 20)) | ||||||
|  |         pocket_computer.paste(input.crop(box(24, 80, 12, 20)), box(24, 0, 12, 20)) | ||||||
|  |         pocket_computer.save(output_dir / f"pocket_bottom_{family}.png") | ||||||
|  |  | ||||||
|  |     if family != "colour": | ||||||
|  |         # Sidebar | ||||||
|  |         sidebar = Image.new("RGBA", (17, 14)) | ||||||
|  |         sidebar.paste(input.crop(box(0, 102, 17, 14)), box(0, 0, 17, 14)) | ||||||
|  |         sidebar.save(output_dir / f"sidebar_{family}.png") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main() -> None: | ||||||
|  |     spec = argparse.ArgumentParser() | ||||||
|  |     spec.add_argument("dir", type=pathlib.Path) | ||||||
|  |  | ||||||
|  |     dir: pathlib.Path = spec.parse_args().dir | ||||||
|  |  | ||||||
|  |     texture_path = dir / "assets" / "computercraft" / "textures" | ||||||
|  |  | ||||||
|  |     transformed: list[pathlib.Path] = [] | ||||||
|  |  | ||||||
|  |     buttons_path = texture_path / "gui" / "buttons.png" | ||||||
|  |     if buttons_path.exists(): | ||||||
|  |         unstitch_buttons(buttons_path) | ||||||
|  |         transformed.append(buttons_path) | ||||||
|  |  | ||||||
|  |     for family in ("normal", "advanced", "command", "colour"): | ||||||
|  |         path = texture_path / "gui" / f"corners_{family}.png" | ||||||
|  |         if path.exists(): | ||||||
|  |             unstitch_corners(path, family) | ||||||
|  |             transformed.append(path) | ||||||
|  |  | ||||||
|  |     if len(transformed) == 0: | ||||||
|  |         print("No files were transformed") | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     print("The following files may be deleted") | ||||||
|  |     for file in transformed: | ||||||
|  |         print(f" - {file}") | ||||||
|  |  | ||||||
|  |     if input("Do so now? [y/N]").lower() == "y": | ||||||
|  |         for file in transformed: | ||||||
|  |             os.remove(file) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
 Jonathan Coates
					Jonathan Coates