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.media.items.DiskItem; | ||||
| import dan200.computercraft.shared.media.items.TreasureDiskItem; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraft.client.color.item.ItemColor; | ||||
| import net.minecraft.client.gui.screens.MenuScreens; | ||||
| 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.ItemProperties; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import net.minecraft.server.packs.resources.PreparableReloadListener; | ||||
| import net.minecraft.server.packs.resources.ResourceProvider; | ||||
| import net.minecraft.world.entity.LivingEntity; | ||||
| import net.minecraft.world.item.Item; | ||||
| @@ -107,6 +109,10 @@ public final class ClientRegistry { | ||||
|         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[]{ | ||||
|         "block/turtle_colour", | ||||
|         "block/turtle_elf_overlay", | ||||
|   | ||||
| @@ -5,15 +5,18 @@ | ||||
| package dan200.computercraft.client.gui; | ||||
| 
 | ||||
| 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.TerminalWidget; | ||||
| 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 net.minecraft.client.renderer.MultiBufferSource; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.world.entity.player.Inventory; | ||||
| 
 | ||||
| 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. | ||||
| @@ -39,10 +42,16 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra | ||||
|     public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) { | ||||
|         // Draw a border around the terminal | ||||
|         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( | ||||
|             stack.last().pose(), ComputerBorderRenderer.getTexture(family), terminal.getX(), terminal.getY(), | ||||
|             FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight() | ||||
|             spriteRenderer, computerTextures, | ||||
|             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.vertex.PoseStack; | ||||
| import com.mojang.blaze3d.vertex.Tesselator; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.client.gui.widgets.ComputerSidebar; | ||||
| import dan200.computercraft.client.gui.widgets.TerminalWidget; | ||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | ||||
| import dan200.computercraft.client.render.RenderTypes; | ||||
| import dan200.computercraft.client.render.SpriteRenderer; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||
| import dan200.computercraft.shared.turtle.inventory.TurtleMenu; | ||||
| import net.minecraft.client.renderer.MultiBufferSource; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| 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); | ||||
|         ComputerSidebar.renderBackground(transform, leftPos, topPos + sidebarYOffset); | ||||
|         // Render sidebar | ||||
|         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; | ||||
| 
 | ||||
| import com.mojang.blaze3d.vertex.PoseStack; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| 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.inventory.AbstractComputerMenu; | ||||
| import net.minecraft.client.gui.components.AbstractWidget; | ||||
| import net.minecraft.client.gui.screens.Screen; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| 
 | ||||
| import java.util.function.BooleanSupplier; | ||||
| import java.util.function.Consumer; | ||||
| @@ -22,22 +19,18 @@ import java.util.function.Consumer; | ||||
|  * Registers buttons to interact with a computer. | ||||
|  */ | ||||
| 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_HEIGHT = 12; | ||||
|     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 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; | ||||
| 
 | ||||
|     private ComputerSidebar() { | ||||
|     } | ||||
| 
 | ||||
| @@ -51,16 +44,18 @@ public final class ComputerSidebar { | ||||
|             Component.translatable("gui.computercraft.tooltip.turn_off.key") | ||||
|         ); | ||||
|         add.accept(new DynamicImageButton( | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF, | ||||
|             TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input), | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, | ||||
|             h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h), | ||||
|             b -> toggleComputer(isOn, input), | ||||
|             () -> isOn.getAsBoolean() ? turnOff : turnOn | ||||
|         )); | ||||
| 
 | ||||
|         y += ICON_HEIGHT + ICON_MARGIN * 2; | ||||
| 
 | ||||
|         add.accept(new DynamicImageButton( | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF, | ||||
|             TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"), | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, | ||||
|             GuiSprites.TERMINATE::get, | ||||
|             b -> input.queueEvent("terminate"), | ||||
|             new HintedMessage( | ||||
|                 Component.translatable("gui.computercraft.tooltip.terminate"), | ||||
|                 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) { | ||||
|         Screen.blit(transform, | ||||
|             x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER, | ||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE | ||||
|         ); | ||||
|     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); | ||||
| 
 | ||||
|         Screen.blit(transform, | ||||
|             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 | ||||
|         ); | ||||
|         renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT); | ||||
|     } | ||||
| 
 | ||||
|     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.vertex.PoseStack; | ||||
| import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction; | ||||
| import net.minecraft.ChatFormatting; | ||||
| import net.minecraft.client.gui.components.Button; | ||||
| import net.minecraft.client.gui.components.Tooltip; | ||||
| import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.function.IntSupplier; | ||||
| import java.util.function.Supplier; | ||||
| 
 | ||||
| /** | ||||
| @@ -21,50 +21,33 @@ import java.util.function.Supplier; | ||||
|  * dynamically. | ||||
|  */ | ||||
| public class DynamicImageButton extends Button { | ||||
|     private final ResourceLocation texture; | ||||
|     private final IntSupplier xTexStart; | ||||
|     private final int yTexStart; | ||||
|     private final int yDiffTex; | ||||
|     private final int textureWidth; | ||||
|     private final int textureHeight; | ||||
|     private final Boolean2ObjectFunction<TextureAtlasSprite> texture; | ||||
|     private final Supplier<HintedMessage> message; | ||||
| 
 | ||||
|     public DynamicImageButton( | ||||
|         int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex, | ||||
|         ResourceLocation texture, int textureWidth, int textureHeight, | ||||
|         OnPress onPress, HintedMessage message | ||||
|         int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress, | ||||
|         HintedMessage message | ||||
|     ) { | ||||
|         this( | ||||
|             x, y, width, height, () -> xTexStart, yTexStart, yDiffTex, | ||||
|             texture, textureWidth, textureHeight, | ||||
|             onPress, () -> message | ||||
|         ); | ||||
|         this(x, y, width, height, texture, onPress, () -> message); | ||||
|     } | ||||
| 
 | ||||
|     public DynamicImageButton( | ||||
|         int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex, | ||||
|         ResourceLocation texture, int textureWidth, int textureHeight, | ||||
|         int x, int y, int width, int height, | ||||
|         Boolean2ObjectFunction<TextureAtlasSprite> texture, | ||||
|         OnPress onPress, Supplier<HintedMessage> message | ||||
|     ) { | ||||
|         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.message = message; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     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(); | ||||
| 
 | ||||
|         var yTex = yTexStart; | ||||
|         if (isHoveredOrFocused()) yTex += yDiffTex; | ||||
| 
 | ||||
|         blit(stack, getX(), getY(), xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight); | ||||
|         blit(stack, getX(), getY(), 0, width, height, texture); | ||||
|         RenderSystem.enableDepthTest(); | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -4,25 +4,17 @@ | ||||
| 
 | ||||
| package dan200.computercraft.client.render; | ||||
| 
 | ||||
| import com.mojang.blaze3d.vertex.Tesselator; | ||||
| import com.mojang.blaze3d.vertex.VertexConsumer; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import net.minecraft.client.renderer.MultiBufferSource; | ||||
| import net.minecraft.client.renderer.RenderType; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import org.joml.Matrix4f; | ||||
| 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; | ||||
| 
 | ||||
| /** | ||||
|  * Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or | ||||
|  * {@linkplain PocketItemRenderer in-hand pocket computers}. | ||||
|  */ | ||||
| public 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"); | ||||
| 
 | ||||
| public final class ComputerBorderRenderer { | ||||
|     /** | ||||
|      * The margin between the terminal and its border. | ||||
|      */ | ||||
| @@ -33,100 +25,51 @@ public class ComputerBorderRenderer { | ||||
|      */ | ||||
|     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 TEX_SIZE = 256; | ||||
|     private static final float TEX_SCALE = 1 / (float) TEX_SIZE; | ||||
|     private static final int TEX_SIZE = 36; | ||||
| 
 | ||||
|     private final Matrix4f transform; | ||||
|     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; | ||||
|     private ComputerBorderRenderer() { | ||||
|     } | ||||
| 
 | ||||
|     public static ResourceLocation getTexture(ComputerFamily family) { | ||||
|         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) { | ||||
|     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; | ||||
| 
 | ||||
|         // Vertical bars | ||||
|         renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y); | ||||
|         renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y); | ||||
|         var border = GuiSprites.get(textures.border()); | ||||
| 
 | ||||
|         // Top bar | ||||
|         renderLine(x, y - BORDER, 0, 0, endX - x, BORDER); | ||||
|         renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y); | ||||
|         renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y); | ||||
|         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) { | ||||
|             renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT); | ||||
|             renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); | ||||
|             renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); | ||||
|             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 { | ||||
|             renderLine(x, endY, 0, BORDER, endX - x, BORDER); | ||||
|             renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y); | ||||
|             renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y); | ||||
|             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 void renderCorner(int x, int y, int u, int v) { | ||||
|         renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER); | ||||
|     } | ||||
| 
 | ||||
|     private void renderLine(int x, int y, int u, int v, int width, int height) { | ||||
|         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(); | ||||
|     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) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ package dan200.computercraft.client.render; | ||||
| 
 | ||||
| import com.mojang.blaze3d.vertex.PoseStack; | ||||
| import com.mojang.math.Axis; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.client.pocket.ClientPocketComputers; | ||||
| import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | ||||
| 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) { | ||||
|         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 g = ((colour >>> 8) & 0xFF) / 255.0f; | ||||
|         var b = (colour & 0xFF) / 255.0f; | ||||
|         var r = (colour >>> 16) & 0xFF; | ||||
|         var g = (colour >>> 8) & 0xFF; | ||||
|         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) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ package dan200.computercraft.client.render; | ||||
| import com.mojang.blaze3d.vertex.DefaultVertexFormat; | ||||
| import com.mojang.blaze3d.vertex.VertexFormat; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader; | ||||
| import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | ||||
| import net.minecraft.client.renderer.GameRenderer; | ||||
| @@ -53,6 +54,11 @@ public class RenderTypes { | ||||
|      */ | ||||
|     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() { | ||||
|         if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered"); | ||||
|         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; | ||||
| 
 | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.data.DataProviders; | ||||
| 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.sources.SingleFile; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| @@ -13,6 +15,7 @@ import net.minecraft.server.packs.PackType; | ||||
| 
 | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.stream.Stream; | ||||
| 
 | ||||
| /** | ||||
|  * 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.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; | ||||
| 
 | ||||
| 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 = "clearLevel(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("HEAD")) | ||||
|     @SuppressWarnings("UnusedMethod") | ||||
|     private void clearLevel(Screen screen, CallbackInfo ci) { | ||||
| @@ -26,4 +35,16 @@ class MinecraftMixin { | ||||
|     private void setLevel(ClientLevel screen, CallbackInfo ci) { | ||||
|         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.client.model.turtle.TurtleModelLoader; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraftforge.api.distmarker.Dist; | ||||
| import net.minecraftforge.client.event.ModelEvent; | ||||
| import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; | ||||
| import net.minecraftforge.client.event.RegisterColorHandlersEvent; | ||||
| import net.minecraftforge.client.event.RegisterShadersEvent; | ||||
| import net.minecraftforge.eventbus.api.SubscribeEvent; | ||||
| @@ -44,6 +46,11 @@ public final class ForgeClientRegistry { | ||||
|         ClientRegistry.registerItemColours(event::register); | ||||
|     } | ||||
| 
 | ||||
|     @SubscribeEvent | ||||
|     public static void registerReloadListeners(RegisterClientReloadListenersEvent event) { | ||||
|         ClientRegistry.registerReloadListeners(event::registerReloadListener, Minecraft.getInstance()); | ||||
|     } | ||||
| 
 | ||||
|     @SubscribeEvent | ||||
|     public static void setupClient(FMLClientSetupEvent event) { | ||||
|         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