Commit 16869b02 by Jonathan Thomas

Merge branch 'hidden-icon' into 'develop'

Custom Player Icons

See merge request !24
parents 237fd0be 0ea724d9
Pipeline #13263 passed with stages
in 2 minutes 10 seconds
......@@ -7,14 +7,14 @@ All notable changes to **CreatureChat** are documented in this file. The format
## Unreleased
### Added
- Rate limiter for LLM unit tests (to prevent rate limit issues from certain providers when running all tests)
- Check friendship direction (+ or -) in LLM unit tests (to verify friendship is output correctly)
### Changed
- Improved LLM unit tests to check for both a positive and negative behaviors (i.e. FOLLOW and not LEAD, ATTACK and not FLEE, etc...)
- Player Icons (custom art embedded in player skin)
- New mixin to extend PlayerSkinTexture to make a copy of the NativeImage + pixel toggle to enable
- Improved LLM Unit tests (to prevent rate limit issues from certain providers when running all tests)
- Check friendship direction (+ or -) in LLM unit tests (to verify friendship direction is output correctly)
### Fixed
- Changing death message timestamp output to use DEBUG log level
- Fixed death messages for mobs with no chat data
- Fixed transparent background behind chat screen for Minecraft 1.20 and 1.20.1.
## [1.2.1] - 2025-01-01
......
......@@ -11,7 +11,7 @@ archives_base_name=creaturechat
# check these on https://fabricmc.net/develop
minecraft_version=1.20.4
yarn_mappings=1.20.4+build.3
loader_version=0.15.10
loader_version=0.15.11
#Fabric api
fabric_version=0.97.0+1.20.4
\ No newline at end of file
package com.owlmaddie.mixin.client;
import com.owlmaddie.skin.PlayerCustomTexture;
import com.owlmaddie.skin.SkinUtils;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
/**
* A Mixin for PlayerCustomTexture to implement hasCustomIcon using SkinUtils.
*/
@Mixin(PlayerCustomTexture.class)
public abstract class MixinPlayerCustomTexture {
/**
* Overwrites the default implementation of hasCustomIcon to provide custom skin support.
*
* @param skinId the Identifier of the skin
* @return true if the skin has a custom icon; false otherwise
*
* @reason Add functionality to determine custom icons based on SkinUtils logic.
* @author jonoomph
*/
@Overwrite
public static boolean hasCustomIcon(Identifier skinId) {
// Delegate to SkinUtils to check for custom skin properties
return SkinUtils.checkCustomSkinKey(skinId);
}
}
package com.owlmaddie.mixin.client;
import com.owlmaddie.skin.IPlayerSkinTexture;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.PlayerSkinTexture;
import net.minecraft.client.texture.ResourceTexture;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* The {@code MixinPlayerSkinTexture} class injects code into the PlayerSkinTexture class, to make a copy
* of the player's skin native image, so we can later use it for pixel checking (black/white key) for
* loading custom player icons in the unused UV coordinates of the player skin image.
*/
@Mixin(PlayerSkinTexture.class)
public abstract class MixinPlayerSkinTexture extends ResourceTexture implements IPlayerSkinTexture {
@Unique
private NativeImage cachedSkinImage;
public MixinPlayerSkinTexture(Identifier location) {
super(location);
}
@Inject(method = "onTextureLoaded", at = @At("HEAD"))
private void captureNativeImage(NativeImage image, CallbackInfo ci) {
// Instead of image.copy(), we do a manual clone
this.cachedSkinImage = cloneNativeImage(image);
}
@Override
public NativeImage getLoadedImage() {
return this.cachedSkinImage;
}
// Example of the utility method in the same class (or in a separate helper):
private static NativeImage cloneNativeImage(NativeImage source) {
NativeImage copy = new NativeImage(
source.getFormat(),
source.getWidth(),
source.getHeight(),
false
);
copy.copyFrom(source);
return copy;
}
}
package com.owlmaddie.skin;
import net.minecraft.client.texture.NativeImage;
import org.jetbrains.annotations.Nullable;
/**
* The {@code IPlayerSkinTexture} interface adds a new getLoadedImage method to PlayerSkinTexture instances
*/
public interface IPlayerSkinTexture {
@Nullable
NativeImage getLoadedImage();
}
\ No newline at end of file
package com.owlmaddie.skin;
import net.minecraft.util.Identifier;
public class PlayerCustomTexture {
public static boolean hasCustomIcon(Identifier skinId) {
// By default, do nothing special
return false;
}
}
package com.owlmaddie.skin;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.texture.AbstractTexture;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.util.Identifier;
/**
* SkinUtils contains functions to check for certain black and white pixel values in a skin, to determine
* if the skin contains a custom hidden icon to show in the player chat message.
*/
public class SkinUtils {
public static boolean checkCustomSkinKey(Identifier skinId) {
// Grab the AbstractTexture from the TextureManager
AbstractTexture tex = MinecraftClient.getInstance().getTextureManager().getTexture(skinId);
// Check if it implements our Mixin interface: IPlayerSkinTexture
if (tex instanceof IPlayerSkinTexture iSkin) {
// Get the NativeImage we stored in the Mixin
NativeImage image = iSkin.getLoadedImage();
if (image != null) {
int width = image.getWidth();
int height = image.getHeight();
// Check we have the full 64x64
if (width == 64 && height == 64) {
// Example: black & white pixel at (31,48) and (32,48)
int color31_48 = image.getColor(31, 49);
int color32_48 = image.getColor(32, 49);
return (color31_48 == 0xFF000000 && color32_48 == 0xFFFFFFFF);
}
}
}
// If it's still loading, or not a PlayerSkinTexture, or no NativeImage loaded yet
return false;
}
}
......@@ -4,9 +4,8 @@ import com.mojang.blaze3d.systems.RenderSystem;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.utils.EntityHeights;
import com.owlmaddie.utils.EntityRendererAccessor;
import com.owlmaddie.utils.TextureLoader;
import com.owlmaddie.skin.PlayerCustomTexture;
import com.owlmaddie.utils.*;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
......@@ -228,6 +227,9 @@ public class BubbleRenderer {
EntityRenderer renderer = EntityRendererAccessor.getEntityRenderer(entity);
Identifier playerTexture = renderer.getTexture(entity);
// Check for black and white pixels (using the Mixin-based check)
boolean customSkinFound = PlayerCustomTexture.hasCustomIcon(playerTexture);
// Set shader & texture
RenderSystem.setShader(GameRenderer::getPositionColorTexLightmapProgram);
RenderSystem.setShaderTexture(0, playerTexture);
......@@ -241,42 +243,83 @@ public class BubbleRenderer {
// Prepare the tessellator and buffer
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder bufferBuilder = tessellator.getBuffer();
// Get the current matrix position
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
// Begin drawing quads with the correct vertex format
bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_LIGHT);
// Texture coordinates for the face region (8, 8) to (16, 16) in a 64x64 texture
float textureWidth = 64.0F;
float textureHeight = 64.0F;
float u1 = 8.0F / textureWidth;
float v1 = 8.0F / textureHeight;
float u2 = 16.0F / textureWidth;
float v2 = 16.0F / textureHeight;
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
float z = -0.01F;
// Draw face
bufferBuilder.vertex(matrix4f, x, y + height, z).color(255, 255, 255, 255).texture(u1, v2).light(light).overlay(overlay).next(); // bottom left
bufferBuilder.vertex(matrix4f, x + width, y + height, z).color(255, 255, 255, 255).texture(u2, v2).light(light).overlay(overlay).next(); // bottom right
bufferBuilder.vertex(matrix4f, x + width, y, z).color(255, 255, 255, 255).texture(u2, v1).light(light).overlay(overlay).next(); // top right
bufferBuilder.vertex(matrix4f, x, y, z).color(255, 255, 255, 255).texture(u1, v1).light(light).overlay(overlay).next(); // top left
// Coordinates for the hat (overlay)
float hatU1 = 40.0F / textureWidth;
float hatV1 = 8.0F / textureHeight;
float hatU2 = 48.0F / textureWidth;
float hatV2 = 16.0F / textureHeight;
// Adjust depth for hat layer
z -= 0.01F;
// Draw hat (overlay)
bufferBuilder.vertex(matrix4f, x, y + height, z).color(255, 255, 255, 255).texture(hatU1, hatV2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x + width, y + height, z).color(255, 255, 255, 255).texture(hatU2, hatV2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x + width, y, z).color(255, 255, 255, 255).texture(hatU2, hatV1).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x, y, z).color(255, 255, 255, 255).texture(hatU1, hatV1).light(light).overlay(overlay).next();
if (customSkinFound) {
// Hidden icon UV coordinates
float[][] newCoordinates = {
{0.0F, 0.0F, 8.0F, 8.0F, 0F, 0F}, // Row 1 left
{24.0F, 0.0F, 32.0F, 8.0F, 8F, 0F}, // Row 1 middle
{32.0F, 0.0F, 40.0F, 8.0F, 16F, 0F}, // Row 1 right
{56.0F, 0.0F, 64.0F, 8.0F, 0F, 8F}, // Row 2 left
{56.0F, 20.0F, 64.0F, 28.0F, 8F, 8F}, // Row 2 middle
{36.0F, 16.0F, 44.0F, 20.0F, 16F, 8F},// Row 2 right top
{56.0F, 16.0F, 64.0F, 20.0F, 16F, 12F},// Row 2 right bottom
{56.0F, 28.0F, 64.0F, 36.0F, 0F, 16F}, // Row 3 left
{56.0F, 36.0F, 64.0F, 44.0F, 8F, 16F}, // Row 3 middle
{56.0F, 44.0F, 64.0F, 52.0F, 16F, 16F},// Row 3 right
};
float scaleFactor = 0.77F;
for (float[] coords : newCoordinates) {
float newU1 = coords[0] / 64.0F;
float newV1 = coords[1] / 64.0F;
float newU2 = coords[2] / 64.0F;
float newV2 = coords[3] / 64.0F;
float offsetX = coords[4] * scaleFactor;
float offsetY = coords[5] * scaleFactor;
float scaledX = x + offsetX;
float scaledY = y + offsetY;
float scaledWidth = (coords[2] - coords[0]) * scaleFactor;
float scaledHeight = (coords[3] - coords[1]) * scaleFactor;
bufferBuilder.vertex(matrix4f, scaledX, scaledY + scaledHeight, z)
.color(255, 255, 255, 255).texture(newU1, newV2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, scaledX + scaledWidth, scaledY + scaledHeight, z)
.color(255, 255, 255, 255).texture(newU2, newV2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, scaledX + scaledWidth, scaledY, z)
.color(255, 255, 255, 255).texture(newU2, newV1).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, scaledX, scaledY, z)
.color(255, 255, 255, 255).texture(newU1, newV1).light(light).overlay(overlay).next();
}
} else {
// Normal face coordinates
float u1 = 8.0F / 64.0F;
float v1 = 8.0F / 64.0F;
float u2 = 16.0F / 64.0F;
float v2 = 16.0F / 64.0F;
bufferBuilder.vertex(matrix4f, x, y + height, z)
.color(255, 255, 255, 255).texture(u1, v2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x + width, y + height, z)
.color(255, 255, 255, 255).texture(u2, v2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x + width, y, z)
.color(255, 255, 255, 255).texture(u2, v1).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x, y, z)
.color(255, 255, 255, 255).texture(u1, v1).light(light).overlay(overlay).next();
// Hat layer
float hatU1 = 40.0F / 64.0F;
float hatV1 = 8.0F / 64.0F;
float hatU2 = 48.0F / 64.0F;
float hatV2 = 16.0F / 64.0F;
z -= 0.01F;
bufferBuilder.vertex(matrix4f, x, y + height, z)
.color(255, 255, 255, 255).texture(hatU1, hatV2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x + width, y + height, z)
.color(255, 255, 255, 255).texture(hatU2, hatV2).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x + width, y, z)
.color(255, 255, 255, 255).texture(hatU2, hatV1).light(light).overlay(overlay).next();
bufferBuilder.vertex(matrix4f, x, y, z)
.color(255, 255, 255, 255).texture(hatU1, hatV1).light(light).overlay(overlay).next();
}
tessellator.draw();
// Disable blending and depth test
......
......@@ -2,6 +2,7 @@ package com.owlmaddie.ui;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.utils.VersionUtils;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
......@@ -99,6 +100,11 @@ public class ChatScreen extends Screen {
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
// Render custom background only for older versions
if (VersionUtils.isOlderThan("1.20.2")) {
renderBackground(context);
}
// Render the label text above the text field
int labelWidth = textRenderer.getWidth(labelText);
int labelX = (this.width - labelWidth) / 2; // Centered X position
......@@ -116,6 +122,11 @@ public class ChatScreen extends Screen {
super.render(context, mouseX, mouseY, delta);
}
public void renderBackground(DrawContext context) {
// Draw a slightly lighter semi-transparent rectangle as the background
context.fillGradient(0, 0, this.width, this.height, 0xA3000000, 0xA3000000);
}
@Override
public boolean shouldCloseOnEsc() {
// Return true if you want the screen to close when the ESC key is pressed
......
package com.owlmaddie.utils;
import net.minecraft.SharedConstants;
/**
* The {@code VersionUtils} class is used to quickly compare the current version of Minecraft.
*/
public class VersionUtils {
public static boolean isOlderThan(String targetVersion) {
String currentVersion = SharedConstants.getGameVersion().getName();
return currentVersion.compareTo(targetVersion) < 0;
}
}
\ No newline at end of file
......@@ -3,8 +3,18 @@
"package": "com.owlmaddie.mixin.client",
"compatibilityLevel": "JAVA_17",
"client": [
"EntityRendererMixin"
"EntityRendererMixin",
"MixinPlayerCustomTexture",
"MixinPlayerSkinTexture"
],
"env": {
"client.MixinPlayerCustomTexture": {
"minVersion": "1.20"
},
"client.MixinPlayerSkinTexture": {
"minVersion": "1.20"
}
},
"injectors": {
"defaultRequire": 1
}
......
......@@ -33,7 +33,7 @@
"accessWidener": "creaturechat.accesswidener",
"depends": {
"fabricloader": ">=0.14.22",
"minecraft": "~1.20.2",
"minecraft": "~1.20.4",
"java": ">=17",
"fabric-api": "*"
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment