Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ public void writeSkin(ByteBuf buffer, SerializedSkin skin) {
this.writeString(buffer, skin.getFullSkinId());

buffer.writeByte("slim".equalsIgnoreCase(skin.getArmSize()) ? 0 : 1);
buffer.writeIntLE(parseSkinColor(skin.getSkinColor()));
buffer.writeIntLE(skinColorArgb(skin));

List<PersonaPieceData> pieces = skin.getPersonaPieces();
VarInts.writeUnsignedInt(buffer, pieces.size());
Expand All @@ -767,7 +767,8 @@ public void writeSkin(ByteBuf buffer, SerializedSkin skin) {
List<PersonaPieceTintData> tints = skin.getTintColors();
VarInts.writeUnsignedInt(buffer, tints.size());
for (PersonaPieceTintData tint : tints) {
this.writeString(buffer, tint.type());
// BDS writes the canonical name ("mouth"), while clients send the persona_ form in their login data.
this.writeString(buffer, PersonaPieceType.fromName(tint.type()).getSerializeName());
List<String> colors = tint.colors();
if (colors.size() != 4) {
throw new IllegalArgumentException("Expected 4 colors in PersonaPieceTintData");
Expand Down Expand Up @@ -1252,12 +1253,9 @@ public void writeSource(ByteBuf buffer, InventorySource inventorySource) {
}
}

protected static int parseSkinColor(String color) {
if (color == null || color.isBlank()) {
return 0;
}
String value = color.charAt(0) == '#' ? color.substring(1) : color;
return (int) Long.parseLong(value, 16);
protected static int skinColorArgb(SerializedSkin skin) {
var color = skin.getColor();
return color == null ? 0 : color.getRGB();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public void writeSkin(ByteBuf buffer, SerializedSkin skin) {
this.writeString(buffer, skin.getFullSkinId());

buffer.writeByte("slim".equalsIgnoreCase(skin.getArmSize()) ? 0 : 1);
buffer.writeIntLE(parseSkinColor(skin.getSkinColor()));
buffer.writeIntLE(skinColorArgb(skin));

List<PersonaPieceData> pieces = skin.getPersonaPieces();
VarInts.writeUnsignedInt(buffer, pieces.size());
Expand All @@ -141,7 +141,7 @@ public void writeSkin(ByteBuf buffer, SerializedSkin skin) {
List<PersonaPieceTintData> tints = skin.getTintColors();
VarInts.writeUnsignedInt(buffer, tints.size());
for (PersonaPieceTintData tint : tints) {
this.writeString(buffer, tint.type());
this.writeString(buffer, PersonaPieceType.fromName(tint.type()).getSerializeName());
List<String> colors = tint.colors();
if (colors.size() != 4) {
throw new IllegalArgumentException("Expected 4 colors in PersonaPieceTintData");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,7 @@ public static SerializedSkin of(String skinId, String playFabId, String skinReso
boolean trusted, String profileHash) {
Color color = null;
if (skinColor != null && !skinColor.isEmpty()) {
color = "#0".equals(skinColor) ? new Color(0, true) :
new Color((int) Long.parseLong(skinColor.startsWith("#") ? skinColor.substring(1) : skinColor, 16), true);
color = parseColor(skinColor);
}
return new SerializedSkin(skinId, null, skinData, capeData, geometryData, premium, skinResourcePatch,
Collections.unmodifiableList(new ObjectArrayList<>(animations)), animationData, persona, capeOnClassic,
Expand All @@ -259,6 +258,15 @@ public static SerializedSkin of(String skinId, String playFabId, String skinReso
geometryDataEngineVersion, primaryUser, overridingPlayerAppearance, trusted, profileHash);
}

/**
* Parses a skin colour as clients send it: {@code #RRGGBB} is opaque, {@code #AARRGGBB} keeps its alpha.
*/
private static Color parseColor(String skinColor) {
String value = skinColor.startsWith("#") ? skinColor.substring(1) : skinColor;
int argb = (int) Long.parseLong(value, 16);
return new Color(value.length() == 6 ? argb | 0xFF000000 : argb, true);
}

public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -345,11 +353,7 @@ public String getSkinColor() {
*/
public Color getColor() {
if (color == null && skinColor != null && !skinColor.isEmpty()) {
if (skinColor.equals("#0")) {
return new Color(0, true);
} else {
return new Color((int) Long.parseLong(skinColor.startsWith("#") ? skinColor.substring(1) : skinColor, 16), true);
}
return parseColor(skinColor);
}
return color;
}
Expand Down Expand Up @@ -428,8 +432,7 @@ public SerializedSkin build() {

Color resolvedColor = color;
if (resolvedColor == null) {
resolvedColor = "#0".equals(skinColor) ? new Color(0, true) :
new Color((int) Long.parseLong(skinColor.startsWith("#") ? skinColor.substring(1) : skinColor, 16), true);
resolvedColor = parseColor(skinColor);
}
if (skinResourcePatch == null) {
return new SerializedSkin(skinId, geometryName, skinData, capeData, geometryData, premium, null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.cloudburstmc.protocol.bedrock.codec.v2168;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.cloudburstmc.protocol.bedrock.codec.v2193.Bedrock_v2193;
import org.cloudburstmc.protocol.bedrock.data.skin.ImageData;
import org.cloudburstmc.protocol.bedrock.data.skin.PersonaPieceData;
import org.cloudburstmc.protocol.bedrock.data.skin.PersonaPieceTintData;
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class BedrockCodecHelper_v2168SkinTest {

@Test
@SuppressWarnings("deprecation")
void writesSkinsFromLoginDataTheWayBdsDoes() {
// Login data carries "persona_" tint types and an "#RRGGBB" skin colour, as a 1.26.52 client sends them.
SerializedSkin skin = SerializedSkin.builder()
.skinId("persona-id")
.playFabId("F17248EADD1FD0A8")
.skinResourcePatch("{\"geometry\":{\"default\":\"geometry.persona\"}}")
.skinData(ImageData.of(1, 1, new byte[4]))
.capeData(ImageData.EMPTY)
.geometryData("")
.geometryDataEngineVersion("1.14.0")
.animationData("")
.capeId("")
.fullSkinId("persona-id")
.armSize("wide")
.skinColor("#7e5337")
.personaPieces(List.of(new PersonaPieceData(
"8f96d1f8-e9bb-40d2-acc8-eb79746c5d7c", "persona_skeleton",
"2099de18-429a-465a-a49b-fc4710a17bb3", true, "")))
.tintColors(List.of(new PersonaPieceTintData("persona_mouth", List.of("#0", "#0", "#ff45220e", "#0"))))
.persona(true)
.profileHash("hash")
.build();

var helper = Bedrock_v2193.CODEC.createHelper();
ByteBuf buffer = Unpooled.buffer();
try {
helper.writeSkin(buffer, skin);
String bytes = new String(ByteBufUtil.getBytes(buffer), StandardCharsets.ISO_8859_1);

assertTrue(bytes.contains("\u0005mouth"), "tint type is written by its canonical name");
assertFalse(bytes.contains("persona_mouth"));

SerializedSkin decoded = helper.readSkin(buffer);
assertEquals("#ff7e5337", decoded.getSkinColor());
assertEquals("mouth", decoded.getTintColors().getFirst().type());
assertEquals("skeleton", decoded.getPersonaPieces().getFirst().type());
assertEquals("F17248EADD1FD0A8", decoded.getPlayFabId());
assertEquals("hash", decoded.getProfileHash());
assertFalse(buffer.isReadable());
} finally {
buffer.release();
}
}

@Test
void keepsAnExplicitAlphaInTheSkinColour() {
assertEquals(0x407e5337, SerializedSkin.builder().skinId("id").skinColor("#407e5337").build().getColor().getRGB());
assertEquals(0xff7e5337, SerializedSkin.builder().skinId("id").skinColor("7e5337").build().getColor().getRGB());
assertEquals(0, SerializedSkin.builder().skinId("id").build().getColor().getRGB());
}
}