This commit is contained in:
rtm516 2024-04-28 19:22:54 +02:00 committed by GitHub
commit 9a9bce75a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 445 additions and 886 deletions

View File

@ -1,6 +1,7 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<copyright> <copyright>
<option name="notice" value="Copyright (c) 2019-&amp;#36;today.year GeyserMC. http://geysermc.org&#10;&#10;Permission is hereby granted, free of charge, to any person obtaining a copy&#10;of this software and associated documentation files (the &quot;Software&quot;), to deal&#10;in the Software without restriction, including without limitation the rights&#10;to use, copy, modify, merge, publish, distribute, sublicense, and/or sell&#10;copies of the Software, and to permit persons to whom the Software is&#10;furnished to do so, subject to the following conditions:&#10;&#10;The above copyright notice and this permission notice shall be included in&#10;all copies or substantial portions of the Software.&#10;&#10;THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR&#10;IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,&#10;FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE&#10;AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER&#10;LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,&#10;OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN&#10;THE SOFTWARE.&#10;&#10;@author GeyserMC&#10;@link https://github.com/GeyserMC/Geyser" /> <option name="allowReplaceRegexp" value="Copyright" />
<option name="notice" value="Copyright (c) &amp;#36;originalComment.match(&quot;Copyright \(c\) (\d+)&quot;, 1, &quot;-&quot;)&amp;#36;today.year GeyserMC. http://geysermc.org&#10;&#10;Permission is hereby granted, free of charge, to any person obtaining a copy&#10;of this software and associated documentation files (the &quot;Software&quot;), to deal&#10;in the Software without restriction, including without limitation the rights&#10;to use, copy, modify, merge, publish, distribute, sublicense, and/or sell&#10;copies of the Software, and to permit persons to whom the Software is&#10;furnished to do so, subject to the following conditions:&#10;&#10;The above copyright notice and this permission notice shall be included in&#10;all copies or substantial portions of the Software.&#10;&#10;THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR&#10;IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,&#10;FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE&#10;AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER&#10;LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,&#10;OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN&#10;THE SOFTWARE.&#10;&#10;@author GeyserMC&#10;@link https://github.com/GeyserMC/Geyser" />
<option name="myName" value="Geyser" /> <option name="myName" value="Geyser" />
</copyright> </copyright>
</component> </component>

View File

@ -1,5 +1,5 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<settings> <settings default="Geyser">
<module2copyright> <module2copyright>
<element module="All" copyright="Geyser" /> <element module="All" copyright="Geyser" />
</module2copyright> </module2copyright>

View File

@ -0,0 +1,144 @@
/*
* Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.event.bedrock;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.event.connection.ConnectionEvent;
import org.geysermc.geyser.api.skin.Cape;
import org.geysermc.geyser.api.skin.Skin;
import org.geysermc.geyser.api.skin.SkinData;
import org.geysermc.geyser.api.skin.SkinGeometry;
import java.util.UUID;
/**
* Called when a skin is applied to a player.
* <p>
* Won't be called when a fake player is spawned for a player skull.
*/
public abstract class SessionSkinApplyEvent extends ConnectionEvent {
private final String username;
private final UUID uuid;
private final boolean slim;
private final boolean bedrock;
private final SkinData originalSkinData;
public SessionSkinApplyEvent(@NonNull GeyserConnection connection, String username, UUID uuid, boolean slim, boolean bedrock, SkinData skinData) {
super(connection);
this.username = username;
this.uuid = uuid;
this.slim = slim;
this.bedrock = bedrock;
this.originalSkinData = skinData;
}
/**
* The username of the player.
*
* @return the username of the player
*/
public String username() {
return username;
}
/**
* The UUID of the player.
*
* @return the UUID of the player
*/
public UUID uuid() {
return uuid;
}
/**
* If the player is using a slim model.
*
* @return if the player is using a slim model
*/
public boolean slim() {
return slim;
}
/**
* If the player is a Bedrock player.
*
* @return if the player is a Bedrock player
*/
public boolean bedrock() {
return bedrock;
}
/**
* The original skin data of the player.
*
* @return the original skin data of the player
*/
public SkinData originalSkin() {
return originalSkinData;
}
/**
* The skin data of the player.
*
* @return the skin data of the player
*/
public abstract SkinData skinData();
/**
* Change the skin of the player.
*
* @param newSkin the new skin
*/
public abstract void skin(@NonNull Skin newSkin);
/**
* Change the cape of the player.
*
* @param newCape the new cape
*/
public abstract void cape(@NonNull Cape newCape);
/**
* Change the geometry of the player.
*
* @param newGeometry the new geometry
*/
public abstract void geometry(@NonNull SkinGeometry newGeometry);
/**
* Change the geometry of the player.
* <p>
* Constructs a generic {@link SkinGeometry} object with the given data.
*
* @param geometryName the name of the geometry
* @param geometryData the data of the geometry
*/
public void geometry(@NonNull String geometryName, @NonNull String geometryData) {
geometry(new SkinGeometry("{\"geometry\" :{\"default\" :\"" + geometryName + "\"}}", geometryData));
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.skin;
/**
* Represents a cape.
*
* @param textureUrl The URL of the cape texture
* @param capeId The ID of the cape
* @param capeData The raw cape image data in ARGB format
* @param failed If the cape failed to load, this is for things like fallback capes
*/
public record Cape(String textureUrl, String capeId, byte[] capeData, boolean failed) {
public Cape(String textureUrl, String capeId, byte[] capeData) {
this(textureUrl, capeId, capeData, false);
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.skin;
/**
* Represents a skin.
*
* @param textureUrl The URL/ID of the skin texture
* @param skinData The raw skin image data in ARGB
* @param failed If the skin failed to load, this is for things like fallback skins
*/
public record Skin(String textureUrl, byte[] skinData, boolean failed) {
public Skin(String textureUrl, byte[] skinData) {
this(textureUrl, skinData, false);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.skin;
/**
* Represents a full package of {@link Skin}, {@link Cape}, and {@link SkinGeometry}.
*/
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.skin;
/**
* Represents geometry of a skin.
*
* @param geometryName The name of the geometry (JSON)
* @param geometryData The geometry data (JSON)
*/
public record SkinGeometry(String geometryName, String geometryData) {
public static SkinGeometry WIDE = getLegacy(false);
public static SkinGeometry SLIM = getLegacy(true);
/**
* Generate generic geometry
*
* @param isSlim if true, it will be the slimmer alex model
* @return The generic geometry object
*/
private static SkinGeometry getLegacy(boolean isSlim) {
return new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "");
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -72,8 +72,10 @@ public interface GeyserConfiguration {
boolean isDebugMode(); boolean isDebugMode();
@Deprecated
boolean isAllowThirdPartyCapes(); boolean isAllowThirdPartyCapes();
@Deprecated
boolean isAllowThirdPartyEars(); boolean isAllowThirdPartyEars();
String getShowCooldown(); String getShowCooldown();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -116,7 +116,7 @@ public class SkullResourcePackManager {
return; return;
} }
BufferedImage image = SkinProvider.requestImage(skinUrl, null); BufferedImage image = SkinProvider.requestImage(skinUrl, false);
// Resize skins to 48x16 to save on space and memory // Resize skins to 48x16 to save on space and memory
BufferedImage skullTexture = new BufferedImage(48, 16, image.getType()); BufferedImage skullTexture = new BufferedImage(48, 16, image.getType());
// Reorder skin parts to fit into the space // Reorder skin parts to fit into the space

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -36,6 +36,10 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.skin.Cape;
import org.geysermc.geyser.api.skin.Skin;
import org.geysermc.geyser.api.skin.SkinData;
import org.geysermc.geyser.api.skin.SkinGeometry;
import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
@ -53,27 +57,27 @@ import java.util.concurrent.TimeUnit;
* Responsible for modifying a player's skin when wearing a player head * Responsible for modifying a player's skin when wearing a player head
*/ */
public class FakeHeadProvider { public class FakeHeadProvider {
private static final LoadingCache<FakeHeadEntry, SkinProvider.SkinData> MERGED_SKINS_LOADING_CACHE = CacheBuilder.newBuilder() private static final LoadingCache<FakeHeadEntry, SkinData> MERGED_SKINS_LOADING_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS) .expireAfterAccess(1, TimeUnit.HOURS)
.maximumSize(10000) .maximumSize(10000)
.build(new CacheLoader<>() { .build(new CacheLoader<>() {
@Override @Override
public SkinProvider.SkinData load(@NonNull FakeHeadEntry fakeHeadEntry) throws Exception { public SkinData load(@NonNull FakeHeadEntry fakeHeadEntry) throws Exception {
SkinProvider.SkinData skinData = SkinProvider.getOrDefault(SkinProvider.requestSkinData(fakeHeadEntry.getEntity()), null, 5); SkinData skinData = SkinProvider.getOrDefault(SkinProvider.requestSkinData(fakeHeadEntry.getEntity(), fakeHeadEntry.getSession()), null, 5);
if (skinData == null) { if (skinData == null) {
throw new Exception("Couldn't load player's original skin"); throw new Exception("Couldn't load player's original skin");
} }
SkinProvider.Skin skin = skinData.skin(); Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape(); Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}") SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
? SkinProvider.WEARING_CUSTOM_SKULL_SLIM : SkinProvider.WEARING_CUSTOM_SKULL; ? SkinProvider.WEARING_CUSTOM_SKULL_SLIM : SkinProvider.WEARING_CUSTOM_SKULL;
SkinProvider.Skin headSkin = SkinProvider.getOrDefault( Skin headSkin = SkinProvider.getOrDefault(
SkinProvider.requestSkin(fakeHeadEntry.getEntity().getUuid(), fakeHeadEntry.getFakeHeadSkinUrl(), false), SkinProvider.EMPTY_SKIN, 5); SkinProvider.requestSkin(fakeHeadEntry.getEntity().getUuid(), fakeHeadEntry.getFakeHeadSkinUrl(), false), SkinProvider.EMPTY_SKIN, 5);
BufferedImage originalSkinImage = SkinProvider.imageDataToBufferedImage(skin.getSkinData(), 64, skin.getSkinData().length / 4 / 64); BufferedImage originalSkinImage = SkinProvider.imageDataToBufferedImage(skin.skinData(), 64, skin.skinData().length / 4 / 64);
BufferedImage headSkinImage = SkinProvider.imageDataToBufferedImage(headSkin.getSkinData(), 64, headSkin.getSkinData().length / 4 / 64); BufferedImage headSkinImage = SkinProvider.imageDataToBufferedImage(headSkin.skinData(), 64, headSkin.skinData().length / 4 / 64);
Graphics2D graphics2D = originalSkinImage.createGraphics(); Graphics2D graphics2D = originalSkinImage.createGraphics();
graphics2D.setComposite(AlphaComposite.Clear); graphics2D.setComposite(AlphaComposite.Clear);
@ -84,14 +88,15 @@ public class FakeHeadProvider {
// Make the skin key a combination of the current skin data and the new skin data // Make the skin key a combination of the current skin data and the new skin data
// Don't tie it to a player - that player *can* change skins in-game // Don't tie it to a player - that player *can* change skins in-game
String skinKey = "customPlayerHead_" + fakeHeadEntry.getFakeHeadSkinUrl() + "_" + skin.getTextureUrl(); String skinKey = "customPlayerHead_" + fakeHeadEntry.getFakeHeadSkinUrl() + "_" + skin.textureUrl();
byte[] targetSkinData = SkinProvider.bufferedImageToImageData(originalSkinImage); byte[] targetSkinData = SkinProvider.bufferedImageToImageData(originalSkinImage);
SkinProvider.Skin mergedSkin = new SkinProvider.Skin(fakeHeadEntry.getEntity().getUuid(), skinKey, targetSkinData, System.currentTimeMillis(), false, false); Skin mergedSkin = new Skin(skinKey, targetSkinData);
// Avoiding memory leak // Avoiding memory leak
fakeHeadEntry.setEntity(null); fakeHeadEntry.setEntity(null);
fakeHeadEntry.setSession(null);
return new SkinProvider.SkinData(mergedSkin, cape, geometry); return new SkinData(mergedSkin, cape, geometry);
} }
}); });
@ -136,7 +141,7 @@ public class FakeHeadProvider {
String texturesProperty = entity.getTexturesProperty(); String texturesProperty = entity.getTexturesProperty();
SkinProvider.getExecutorService().execute(() -> { SkinProvider.getExecutorService().execute(() -> {
try { try {
SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity)); SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity, session));
SkinManager.sendSkinPacket(session, entity, mergedSkinData); SkinManager.sendSkinPacket(session, entity, mergedSkinData);
} catch (ExecutionException e) { } catch (ExecutionException e) {
GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e); GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e);
@ -153,7 +158,7 @@ public class FakeHeadProvider {
return; return;
} }
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> { SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> {
if (throwable != null) { if (throwable != null) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable); GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable);
return; return;
@ -170,6 +175,7 @@ public class FakeHeadProvider {
private final String texturesProperty; private final String texturesProperty;
private final String fakeHeadSkinUrl; private final String fakeHeadSkinUrl;
private PlayerEntity entity; private PlayerEntity entity;
private GeyserSession session;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -26,6 +26,7 @@
package org.geysermc.geyser.skin; package org.geysermc.geyser.skin;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.skin.Skin;
import org.geysermc.geyser.util.AssetUtils; import org.geysermc.geyser.util.AssetUtils;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -67,7 +68,7 @@ public final class ProvidedSkins {
} }
public static final class ProvidedSkin { public static final class ProvidedSkin {
private SkinProvider.Skin data; private Skin data;
private final boolean slim; private final boolean slim;
ProvidedSkin(String asset, boolean slim) { ProvidedSkin(String asset, boolean slim) {
@ -94,14 +95,14 @@ public final class ProvidedSkins {
image.flush(); image.flush();
String identifier = "geysermc:" + assetName + "_" + (slim ? "slim" : "wide"); String identifier = "geysermc:" + assetName + "_" + (slim ? "slim" : "wide");
this.data = new SkinProvider.Skin(-1, identifier, byteData); this.data = new Skin(identifier, byteData, true);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
})); }));
} }
public SkinProvider.Skin getData() { public Skin getData() {
// Fall back to the default skin if we can't load our skins, or it's not loaded yet. // Fall back to the default skin if we can't load our skins, or it's not loaded yet.
return Objects.requireNonNullElse(data, SkinProvider.EMPTY_SKIN); return Objects.requireNonNullElse(data, SkinProvider.EMPTY_SKIN);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -35,6 +35,10 @@ import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.skin.Cape;
import org.geysermc.geyser.api.skin.Skin;
import org.geysermc.geyser.api.skin.SkinData;
import org.geysermc.geyser.api.skin.SkinGeometry;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
@ -56,21 +60,21 @@ public class SkinManager {
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) { public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
// First: see if we have the cached skin texture ID. // First: see if we have the cached skin texture ID.
GameProfileData data = GameProfileData.from(playerEntity); GameProfileData data = GameProfileData.from(playerEntity);
SkinProvider.Skin skin = null; Skin skin = null;
SkinProvider.Cape cape = null; Cape cape = null;
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.WIDE; SkinGeometry geometry = SkinGeometry.WIDE;
if (data != null) { if (data != null) {
// GameProfileData is not null = server provided us with textures data to work with. // GameProfileData is not null = server provided us with textures data to work with.
skin = SkinProvider.getCachedSkin(data.skinUrl()); skin = SkinProvider.getCachedSkin(data.skinUrl());
cape = SkinProvider.getCachedCape(data.capeUrl()); cape = SkinProvider.getCachedCape(data.capeUrl());
geometry = data.isAlex() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE; geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
} }
if (skin == null || cape == null) { if (skin == null || cape == null) {
// The server either didn't have a texture to send, or we didn't have the texture ID cached. // The server either didn't have a texture to send, or we didn't have the texture ID cached.
// Let's see if this player is a Bedrock player, and if so, let's pull their skin. // Let's see if this player is a Bedrock player, and if so, let's pull their skin.
// Otherwise, grab the default player skin // Otherwise, grab the default player skin
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid()); SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
if (skin == null) { if (skin == null) {
skin = fallbackSkinData.skin(); skin = fallbackSkinData.skin();
geometry = fallbackSkinData.geometry(); geometry = fallbackSkinData.geometry();
@ -95,10 +99,10 @@ public class SkinManager {
* With all the information needed, build a Bedrock player entry with translated skin information. * With all the information needed, build a Bedrock player entry with translated skin information.
*/ */
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
SkinProvider.Skin skin, Skin skin,
SkinProvider.Cape cape, Cape cape,
SkinProvider.SkinGeometry geometry) { SkinGeometry geometry) {
SerializedSkin serializedSkin = getSkin(skin.getTextureUrl(), skin, cape, geometry); SerializedSkin serializedSkin = getSkin(skin.textureUrl(), skin, cape, geometry);
// This attempts to find the XUID of the player so profile images show up for Xbox accounts // This attempts to find the XUID of the player so profile images show up for Xbox accounts
String xuid = ""; String xuid = "";
@ -128,10 +132,10 @@ public class SkinManager {
return entry; return entry;
} }
public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) { public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinData skinData) {
SkinProvider.Skin skin = skinData.skin(); Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape(); Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry(); SkinGeometry geometry = skinData.geometry();
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
// TODO is this special behavior needed? // TODO is this special behavior needed?
@ -153,23 +157,23 @@ public class SkinManager {
PlayerSkinPacket packet = new PlayerSkinPacket(); PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid()); packet.setUuid(entity.getUuid());
packet.setOldSkinName(""); packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl()); packet.setNewSkinName(skin.textureUrl());
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry)); packet.setSkin(getSkin(skin.textureUrl(), skin, cape, geometry));
packet.setTrustedSkin(true); packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet); session.sendUpstreamPacket(packet);
} }
} }
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) { private static SerializedSkin getSkin(String skinId, Skin skin, Cape cape, SkinGeometry geometry) {
return SerializedSkin.of(skinId, "", geometry.geometryName(), return SerializedSkin.of(skinId, "", geometry.geometryName(),
ImageData.of(skin.getSkinData()), Collections.emptyList(), ImageData.of(skin.skinData()), Collections.emptyList(),
ImageData.of(cape.capeData()), geometry.geometryData(), ImageData.of(cape.capeData()), geometry.geometryData(),
"", true, false, false, cape.capeId(), skinId); "", true, false, false, cape.capeId(), skinId);
} }
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session, public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) { Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> { SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> {
if (skinData == null) { if (skinData == null) {
if (skinAndCapeConsumer != null) { if (skinAndCapeConsumer != null) {
skinAndCapeConsumer.accept(null); skinAndCapeConsumer.accept(null);

View File

@ -35,7 +35,12 @@ import lombok.NoArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.bedrock.SessionSkinApplyEvent;
import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.skin.Cape;
import org.geysermc.geyser.api.skin.Skin;
import org.geysermc.geyser.api.skin.SkinData;
import org.geysermc.geyser.api.skin.SkinGeometry;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
@ -45,7 +50,6 @@ import org.geysermc.geyser.util.WebUtils;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -57,11 +61,10 @@ import java.util.concurrent.*;
import java.util.function.Predicate; import java.util.function.Predicate;
public class SkinProvider { public class SkinProvider {
private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
private static ExecutorService EXECUTOR_SERVICE; private static ExecutorService EXECUTOR_SERVICE;
static final Skin EMPTY_SKIN; static final Skin EMPTY_SKIN;
static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, -1, true); static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, true);
private static final Cache<String, Cape> CACHED_JAVA_CAPES = CacheBuilder.newBuilder() private static final Cache<String, Cape> CACHED_JAVA_CAPES = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS) .expireAfterAccess(1, TimeUnit.HOURS)
@ -88,9 +91,6 @@ public class SkinProvider {
*/ */
private static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2; private static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
private static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
private static final String EARS_GEOMETRY;
private static final String EARS_GEOMETRY_SLIM;
static final SkinGeometry SKULL_GEOMETRY; static final SkinGeometry SKULL_GEOMETRY;
static final SkinGeometry WEARING_CUSTOM_SKULL; static final SkinGeometry WEARING_CUSTOM_SKULL;
static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM; static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
@ -114,28 +114,27 @@ public class SkinProvider {
outputStream.write((rgba >> 24) & 0xFF); // Alpha outputStream.write((rgba >> 24) & 0xFF); // Alpha
} }
} }
EMPTY_SKIN = new Skin(-1, "geysermc:empty", outputStream.toByteArray()); EMPTY_SKIN = new Skin("geysermc:empty", outputStream.toByteArray(), true);
/* Load in the normal ears geometry */
EARS_GEOMETRY = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.ears.json"), StandardCharsets.UTF_8);
/* Load in the slim ears geometry */
EARS_GEOMETRY_SLIM = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.earsSlim.json"), StandardCharsets.UTF_8);
/* Load in the custom skull geometry */ /* Load in the custom skull geometry */
String skullData = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.customskull.json"), StandardCharsets.UTF_8); String skullData = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.customskull.json"), StandardCharsets.UTF_8);
SKULL_GEOMETRY = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.customskull\"}}", skullData, false); SKULL_GEOMETRY = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.customskull\"}}", skullData);
/* Load in the player head skull geometry */ /* Load in the player head skull geometry */
String wearingCustomSkull = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkull.json"), StandardCharsets.UTF_8); String wearingCustomSkull = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkull.json"), StandardCharsets.UTF_8);
WEARING_CUSTOM_SKULL = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkull\"}}", wearingCustomSkull, false); WEARING_CUSTOM_SKULL = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkull\"}}", wearingCustomSkull);
String wearingCustomSkullSlim = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json"), StandardCharsets.UTF_8); String wearingCustomSkullSlim = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json"), StandardCharsets.UTF_8);
WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkullSlim\"}}", wearingCustomSkullSlim, false); WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkullSlim\"}}", wearingCustomSkullSlim);
GeyserImpl geyser = GeyserImpl.getInstance();
if (geyser.getConfig().isAllowThirdPartyEars() || geyser.getConfig().isAllowThirdPartyCapes()) {
geyser.getLogger().warning("Third-party ears/capes have been removed from Geyser, if you still wish to have this functionality please use the extension: https://github.com/GeyserMC/ThirdPartyCosmetics");
}
} }
public static ExecutorService getExecutorService() { public static ExecutorService getExecutorService() {
if (EXECUTOR_SERVICE == null) { if (EXECUTOR_SERVICE == null) {
EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14); EXECUTOR_SERVICE = Executors.newFixedThreadPool(14);
} }
return EXECUTOR_SERVICE; return EXECUTOR_SERVICE;
} }
@ -204,7 +203,7 @@ public class SkinProvider {
// We don't have a skin for the player right now. Fall back to a default. // We don't have a skin for the player right now. Fall back to a default.
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid); ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid);
skin = providedSkin.getData(); skin = providedSkin.getData();
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE; geometry = providedSkin.isSlim() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
} }
if (cape == null) { if (cape == null) {
@ -238,7 +237,7 @@ public class SkinProvider {
return CACHED_JAVA_CAPES.getIfPresent(capeUrl); return CACHED_JAVA_CAPES.getIfPresent(capeUrl);
} }
static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) { static CompletableFuture<SkinData> requestSkinData(PlayerEntity entity, GeyserSession session) {
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity); SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
if (data == null) { if (data == null) {
// This player likely does not have a textures property // This player likely does not have a textures property
@ -260,42 +259,33 @@ public class SkinProvider {
cape = getCachedBedrockCape(entity.getUuid()); cape = getCachedBedrockCape(entity.getUuid());
} }
if (cape.failed() && ALLOW_THIRD_PARTY_CAPES) { // Call event to allow extensions to modify the skin, cape and geo
cape = getOrDefault(requestUnofficialCape( boolean isBedrock = GeyserImpl.getInstance().connectionByUuid(entity.getUuid()) != null;
cape, entity.getUuid(), SkinData skinData = new SkinData(skin, cape, geometry);
entity.getUsername(), false final EventSkinData eventSkinData = new EventSkinData(skinData);
), EMPTY_CAPE, CapeProvider.VALUES.length * 3); GeyserImpl.getInstance().eventBus().fire(new SessionSkinApplyEvent(session, entity.getUsername(), entity.getUuid(), data.isAlex(), isBedrock, skinData) {
} @Override
public SkinData skinData() {
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername()); return eventSkinData.skinData();
// Not a bedrock player check for ears
if (geometry.failed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
boolean isEars;
// Its deadmau5, gotta support his skin :)
if (isDeadmau5) {
isEars = true;
} else {
// Get the ears texture for the player
skin = getOrDefault(requestUnofficialEars(
skin, entity.getUuid(), entity.getUsername(), false
), skin, 3);
isEars = skin.isEars();
} }
// Does the skin have an ears texture @Override
if (isEars) { public void skin(@NonNull Skin newSkin) {
// Get the new geometry eventSkinData.skinData(new SkinData(newSkin, skinData.cape(), skinData.geometry()));
geometry = SkinGeometry.getEars(data.isAlex());
// Store the skin and geometry for the ears
storeEarSkin(skin);
storeEarGeometry(entity.getUuid(), data.isAlex());
} }
}
return new SkinData(skin, cape, geometry); @Override
public void cape(@NonNull Cape newCape) {
eventSkinData.skinData(new SkinData(skinData.skin(), newCape, skinData.geometry()));
}
@Override
public void geometry(@NonNull SkinGeometry newGeometry) {
eventSkinData.skinData(new SkinData(skinData.skin(), skinData.cape(), newGeometry));
}
});
return eventSkinData.skinData();
} catch (Exception e) { } catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
} }
@ -308,10 +298,9 @@ public class SkinProvider {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
SkinAndCape skinAndCape = new SkinAndCape( SkinAndCape skinAndCape = new SkinAndCape(
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5), getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5) getOrDefault(requestCape(capeUrl, false), EMPTY_CAPE, 5)
); );
GeyserImpl.getInstance().getLogger().debug("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId); GeyserImpl.getInstance().getLogger().debug("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId);
@ -336,7 +325,6 @@ public class SkinProvider {
if (newThread) { if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), getExecutorService()) future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), getExecutorService())
.whenCompleteAsync((skin, throwable) -> { .whenCompleteAsync((skin, throwable) -> {
skin.updated = true;
CACHED_JAVA_SKINS.put(textureUrl, skin); CACHED_JAVA_SKINS.put(textureUrl, skin);
requestedSkins.remove(textureUrl); requestedSkins.remove(textureUrl);
}); });
@ -349,7 +337,7 @@ public class SkinProvider {
return future; return future;
} }
private static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) { private static CompletableFuture<Cape> requestCape(String capeUrl, boolean newThread) {
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE); if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl); CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl);
if (requestedCape != null) { if (requestedCape != null) {
@ -363,128 +351,48 @@ public class SkinProvider {
CompletableFuture<Cape> future; CompletableFuture<Cape> future;
if (newThread) { if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), getExecutorService()) future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl), getExecutorService())
.whenCompleteAsync((cape, throwable) -> { .whenCompleteAsync((cape, throwable) -> {
CACHED_JAVA_CAPES.put(capeUrl, cape); CACHED_JAVA_CAPES.put(capeUrl, cape);
requestedCapes.remove(capeUrl); requestedCapes.remove(capeUrl);
}); });
requestedCapes.put(capeUrl, future); requestedCapes.put(capeUrl, future);
} else { } else {
Cape cape = supplyCape(capeUrl, provider); // blocking Cape cape = supplyCape(capeUrl); // blocking
future = CompletableFuture.completedFuture(cape); future = CompletableFuture.completedFuture(cape);
CACHED_JAVA_CAPES.put(capeUrl, cape); CACHED_JAVA_CAPES.put(capeUrl, cape);
} }
return future; return future;
} }
private static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
String username, boolean newThread) {
if (officialCape.failed() && ALLOW_THIRD_PARTY_CAPES) {
for (CapeProvider provider : CapeProvider.VALUES) {
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
continue;
}
Cape cape1 = getOrDefault(
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
EMPTY_CAPE, 4
);
if (!cape1.failed()) {
return CompletableFuture.completedFuture(cape1);
}
}
}
return CompletableFuture.completedFuture(officialCape);
}
private static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
CompletableFuture<Skin> future;
if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), getExecutorService())
.whenCompleteAsync((outSkin, throwable) -> { });
} else {
Skin ears = supplyEars(skin, earsUrl); // blocking
future = CompletableFuture.completedFuture(ears);
}
return future;
}
/**
* Try and find an ear texture for a Java player
*
* @param officialSkin The current players skin
* @param playerId The players UUID
* @param username The players username
* @param newThread Should we start in a new thread
* @return The updated skin with ears
*/
private static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
for (EarsProvider provider : EarsProvider.VALUES) {
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
continue;
}
Skin skin1 = getOrDefault(
requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin),
officialSkin, 4
);
if (skin1.isEars()) {
return CompletableFuture.completedFuture(skin1);
}
}
return CompletableFuture.completedFuture(officialSkin);
}
static void storeBedrockSkin(UUID playerID, String skinId, byte[] skinData) { static void storeBedrockSkin(UUID playerID, String skinId, byte[] skinData) {
Skin skin = new Skin(playerID, skinId, skinData, System.currentTimeMillis(), true, false); Skin skin = new Skin(skinId, skinData);
CACHED_BEDROCK_SKINS.put(skin.getTextureUrl(), skin); CACHED_BEDROCK_SKINS.put(skin.textureUrl(), skin);
} }
static void storeBedrockCape(String capeId, byte[] capeData) { static void storeBedrockCape(String capeId, byte[] capeData) {
Cape cape = new Cape(capeId, capeId, capeData, System.currentTimeMillis(), false); Cape cape = new Cape(capeId, capeId, capeData);
CACHED_BEDROCK_CAPES.put(capeId, cape); CACHED_BEDROCK_CAPES.put(capeId, cape);
} }
static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) { static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData), false); SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData));
cachedGeometry.put(playerID, geometry); cachedGeometry.put(playerID, geometry);
} }
/**
* Stores the adjusted skin with the ear texture to the cache
*
* @param skin The skin to cache
*/
public static void storeEarSkin(Skin skin) {
CACHED_JAVA_SKINS.put(skin.getTextureUrl(), skin);
}
/**
* Stores the geometry for a Java player with ears
*
* @param playerID The UUID to cache it against
* @param isSlim If the player is using an slim base
*/
private static void storeEarGeometry(UUID playerID, boolean isSlim) {
cachedGeometry.put(playerID, SkinGeometry.getEars(isSlim));
}
private static Skin supplySkin(UUID uuid, String textureUrl) { private static Skin supplySkin(UUID uuid, String textureUrl) {
try { try {
byte[] skin = requestImageData(textureUrl, null); byte[] skin = requestImageData(textureUrl, false);
return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); return new Skin(textureUrl, skin);
} catch (Exception ignored) {} // just ignore I guess } catch (Exception ignored) {} // just ignore I guess
return new Skin(uuid, "empty", EMPTY_SKIN.getSkinData(), System.currentTimeMillis(), false, false); return new Skin("empty", EMPTY_SKIN.skinData(), true);
} }
private static Cape supplyCape(String capeUrl, CapeProvider provider) { private static Cape supplyCape(String capeUrl) {
byte[] cape = EMPTY_CAPE.capeData(); byte[] cape = EMPTY_CAPE.capeData();
try { try {
cape = requestImageData(capeUrl, provider); cape = requestImageData(capeUrl, true);
} catch (Exception ignored) { } catch (Exception ignored) {
} // just ignore I guess } // just ignore I guess
@ -494,54 +402,12 @@ public class SkinProvider {
capeUrl, capeUrl,
urlSection[urlSection.length - 1], // get the texture id and use it as cape id urlSection[urlSection.length - 1], // get the texture id and use it as cape id
cape, cape,
System.currentTimeMillis(),
cape.length == 0 cape.length == 0
); );
} }
/**
* Get the ears texture and place it on the skin from the given URL
*
* @param existingSkin The players current skin
* @param earsUrl The URL to get the ears texture from
* @return The updated skin with ears
*/
private static Skin supplyEars(Skin existingSkin, String earsUrl) {
try {
// Get the ears texture
BufferedImage ears = ImageIO.read(new URL(earsUrl));
if (ears == null) throw new NullPointerException();
// Convert the skin data to a BufferedImage
int height = (existingSkin.getSkinData().length / 4 / 64);
BufferedImage skinImage = imageDataToBufferedImage(existingSkin.getSkinData(), 64, height);
// Create a new image with the ears texture over it
BufferedImage newSkin = new BufferedImage(skinImage.getWidth(), skinImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D) newSkin.getGraphics();
g.drawImage(skinImage, 0, 0, null);
g.drawImage(ears, 24, 0, null);
// Turn the buffered image back into an array of bytes
byte[] data = bufferedImageToImageData(newSkin);
skinImage.flush();
// Create a new skin object with the new infomation
return new Skin(
existingSkin.getSkinOwner(),
existingSkin.getTextureUrl(),
data,
System.currentTimeMillis(),
true,
true
);
} catch (Exception ignored) {} // just ignore I guess
return existingSkin;
}
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public static BufferedImage requestImage(String imageUrl, CapeProvider provider) throws IOException { public static BufferedImage requestImage(String imageUrl, boolean isCape) throws IOException {
BufferedImage image = null; BufferedImage image = null;
// First see if we have a cached file. We also update the modification stamp so we know when the file was last used // First see if we have a cached file. We also update the modification stamp so we know when the file was last used
@ -556,7 +422,7 @@ public class SkinProvider {
// If no image we download it // If no image we download it
if (image == null) { if (image == null) {
image = downloadImage(imageUrl, provider); image = downloadImage(imageUrl);
GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl); GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl);
// Write to cache if we are allowed // Write to cache if we are allowed
@ -572,7 +438,7 @@ public class SkinProvider {
} }
// if the requested image is a cape // if the requested image is a cape
if (provider != null) { if (isCape) {
if (image.getWidth() > 64 || image.getHeight() > 32) { if (image.getWidth() > 64 || image.getHeight() > 32) {
// Prevent weirdly-scaled capes from being cut off // Prevent weirdly-scaled capes from being cut off
BufferedImage newImage = new BufferedImage(128, 64, BufferedImage.TYPE_INT_ARGB); BufferedImage newImage = new BufferedImage(128, 64, BufferedImage.TYPE_INT_ARGB);
@ -604,8 +470,8 @@ public class SkinProvider {
return image; return image;
} }
private static byte[] requestImageData(String imageUrl, CapeProvider provider) throws Exception { private static byte[] requestImageData(String imageUrl, boolean isCape) throws Exception {
BufferedImage image = requestImage(imageUrl, provider); BufferedImage image = requestImage(imageUrl, isCape);
byte[] data = bufferedImageToImageData(image); byte[] data = bufferedImageToImageData(image);
image.flush(); image.flush();
return data; return data;
@ -668,35 +534,20 @@ public class SkinProvider {
}); });
} }
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException { private static BufferedImage downloadImage(String imageUrl) throws IOException {
BufferedImage image; HttpURLConnection con = (HttpURLConnection) new URL(imageUrl).openConnection();
if (provider == CapeProvider.FIVEZIG) { con.setRequestProperty("User-Agent", WebUtils.getUserAgent());
image = readFiveZigCape(imageUrl); con.setConnectTimeout(10000);
} else { con.setReadTimeout(10000);
HttpURLConnection con = (HttpURLConnection) new URL(imageUrl).openConnection();
con.setRequestProperty("User-Agent", WebUtils.getUserAgent());
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
image = ImageIO.read(con.getInputStream()); BufferedImage image = ImageIO.read(con.getInputStream());
}
if (image == null) { if (image == null) {
throw new IllegalArgumentException("Failed to read image from: %s (cape provider=%s)".formatted(imageUrl, provider)); throw new IllegalArgumentException("Failed to read image from: %s".formatted(imageUrl));
} }
return image; return image;
} }
private static @Nullable BufferedImage readFiveZigCape(String url) throws IOException {
JsonNode element = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(url));
if (element != null && element.isObject()) {
JsonNode capeElement = element.get("d");
if (capeElement == null || capeElement.isNull()) return null;
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(capeElement.textValue())));
}
return null;
}
public static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) { public static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) {
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = resized.createGraphics(); Graphics2D g2 = resized.createGraphics();
@ -770,124 +621,19 @@ public class SkinProvider {
public record SkinAndCape(Skin skin, Cape cape) { public record SkinAndCape(Skin skin, Cape cape) {
} }
/** public static class EventSkinData {
* Represents a full package of skin, cape, and geometry. private SkinData skinData;
*/
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
}
@AllArgsConstructor public EventSkinData(SkinData skinData) {
@Getter this.skinData = skinData;
public static class Skin { }
private UUID skinOwner;
private final String textureUrl;
private final byte[] skinData;
private final long requestedOn;
private boolean updated;
private boolean ears;
Skin(long requestedOn, String textureUrl, byte[] skinData) { public SkinData skinData() {
this.requestedOn = requestedOn; return skinData;
this.textureUrl = textureUrl; }
public void skinData(SkinData skinData) {
this.skinData = skinData; this.skinData = skinData;
} }
} }
public record Cape(String textureUrl, String capeId, byte[] capeData, long requestedOn, boolean failed) {
}
public record SkinGeometry(String geometryName, String geometryData, boolean failed) {
public static SkinGeometry WIDE = getLegacy(false);
public static SkinGeometry SLIM = getLegacy(true);
/**
* Generate generic geometry
*
* @param isSlim Should it be the alex model
* @return The generic geometry object
*/
private static SkinGeometry getLegacy(boolean isSlim) {
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "", true);
}
/**
* Generate basic geometry with ears
*
* @param isSlim Should it be the alex model
* @return The generated geometry for the ears model
*/
private static SkinGeometry getEars(boolean isSlim) {
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.ears" + (isSlim ? "Slim" : "") + "\"}}", (isSlim ? EARS_GEOMETRY_SLIM : EARS_GEOMETRY), false);
}
}
/*
* Sorted by 'priority'
*/
@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum CapeProvider {
MINECRAFT,
OPTIFINE("https://optifine.net/capes/%s.png", CapeUrlType.USERNAME),
LABYMOD("https://dl.labymod.net/capes/%s", CapeUrlType.UUID_DASHED),
FIVEZIG("https://textures.5zigreborn.eu/profile/%s", CapeUrlType.UUID_DASHED),
MINECRAFTCAPES("https://api.minecraftcapes.net/profile/%s/cape", CapeUrlType.UUID);
public static final CapeProvider[] VALUES = Arrays.copyOfRange(values(), 1, 5);
private String url;
private CapeUrlType type;
public String getUrlFor(String type) {
return String.format(url, type);
}
public String getUrlFor(UUID uuid, String username) {
return getUrlFor(toRequestedType(type, uuid, username));
}
public static String toRequestedType(CapeUrlType type, UUID uuid, String username) {
return switch (type) {
case UUID -> uuid.toString().replace("-", "");
case UUID_DASHED -> uuid.toString();
default -> username;
};
}
}
public enum CapeUrlType {
USERNAME,
UUID,
UUID_DASHED
}
/*
* Sorted by 'priority'
*/
@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum EarsProvider {
MINECRAFTCAPES("https://api.minecraftcapes.net/profile/%s/ears", CapeUrlType.UUID);
public static final EarsProvider[] VALUES = values();
private String url;
private CapeUrlType type;
public String getUrlFor(String type) {
return String.format(url, type);
}
public String getUrlFor(UUID uuid, String username) {
return getUrlFor(toRequestedType(type, uuid, username));
}
public static String toRequestedType(CapeUrlType type, UUID uuid, String username) {
return switch (type) {
case UUID -> uuid.toString().replace("-", "");
case UUID_DASHED -> uuid.toString();
default -> username;
};
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -29,6 +29,8 @@ import org.cloudburstmc.protocol.bedrock.data.skin.ImageData;
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin; import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.skin.Skin;
import org.geysermc.geyser.api.skin.SkinData;
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.GeyserLocale;
@ -50,14 +52,14 @@ public class SkullSkinManager extends SkinManager {
} }
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session, public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.Skin> skinConsumer) { Consumer<Skin> skinConsumer) {
BiConsumer<SkinProvider.Skin, Throwable> applySkin = (skin, throwable) -> { BiConsumer<Skin, Throwable> applySkin = (skin, throwable) -> {
try { try {
PlayerSkinPacket packet = new PlayerSkinPacket(); PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid()); packet.setUuid(entity.getUuid());
packet.setOldSkinName(""); packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl()); packet.setNewSkinName(skin.textureUrl());
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData())); packet.setSkin(buildSkullEntryManually(skin.textureUrl(), skin.skinData()));
packet.setTrustedSkin(true); packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet); session.sendUpstreamPacket(packet);
} catch (Exception e) { } catch (Exception e) {
@ -74,7 +76,7 @@ public class SkullSkinManager extends SkinManager {
GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() + GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() +
" with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID()); " with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID());
// No texture available, fallback using the UUID // No texture available, fallback using the UUID
SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID()); SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID());
applySkin.accept(fallback.skin(), null); applySkin.accept(fallback.skin(), null);
} else { } else {
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true) SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)

View File

@ -1,249 +0,0 @@
{
"format_version": "1.14.0",
"minecraft:geometry": [
{
"bones": [
{
"name" : "root",
"pivot" : [ 0.0, 0.0, 0.0 ]
},
{
"name" : "waist",
"parent" : "root",
"pivot" : [ 0.0, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes" : []
},
{
"name": "body",
"parent" : "waist",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 12.0, -2.0 ],
"size": [ 8, 12, 4 ],
"uv": [ 16, 16 ]
}
]
},
{
"name": "jacket",
"parent" : "body",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 12.0, -2.0 ],
"size": [ 8, 12, 4 ],
"uv": [ 16, 32 ],
"inflate": 0.25
}
]
},
{
"name": "head",
"parent" : "body",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 24.0, -4.0 ],
"size": [ 8, 8, 8 ],
"uv": [ 0, 0 ]
}
]
},
{
"name": "hat",
"parent" : "head",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 24.0, -4.0 ],
"size": [ 8, 8, 8 ],
"uv": [ 32, 0 ],
"inflate": 0.5
}
]
},
{
"name": "leftArm",
"parent" : "body",
"pivot": [ 5.0, 22.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ 4.0, 12.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 32, 48 ]
}
]
},
{
"name": "rightArm",
"parent" : "body",
"pivot": [ -5.0, 22.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -8.0, 12.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 40, 16 ]
}
]
},
{
"name": "leftSleeve",
"parent" : "leftArm",
"pivot": [ 5.0, 22.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ 4.0, 12.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 48, 48 ],
"inflate": 0.25
}
]
},
{
"name": "rightSleeve",
"parent" : "rightArm",
"pivot": [ -5.0, 22.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -8.0, 12.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 40, 32 ],
"inflate": 0.25
}
]
},
{
"name": "leftLeg",
"parent" : "root",
"pivot": [ 1.9, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -0.1, 0.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 0, 16 ]
}
]
},
{
"name": "rightLeg",
"parent" : "root",
"pivot": [ -1.9, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -3.9, 0.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 0, 16 ]
}
]
},
{
"name": "leftPants",
"parent" : "leftLeg",
"pivot": [1.9, 12.0, 0.0],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -0.1, 0.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 0, 48 ],
"inflate": 0.25
}
]
},
{
"name": "rightPants",
"parent" : "rightLeg",
"pivot": [ -1.9, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -3.9, 0.0, -2.0] ,
"size": [ 4, 12, 4 ],
"uv": [ 0, 32],
"inflate": 0.25
}
]
},
{
"name" : "rightItem",
"parent" : "rightArm",
"pivot" : [ -6.0, 15.0, 1.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes" : []
},
{
"name" : "leftItem",
"parent" : "leftArm",
"pivot" : [ 6.0, 15.0, 1.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes" : []
},
{
"name": "leftEar",
"parent" : "head",
"pivot": [ -1.9, 12.0, 0.0 ],
"cubes": [
{
"origin": [ 3.0, 31.0, -0.5 ],
"size": [ 6, 6, 1 ],
"uv": [ 24, 0 ],
"inflate": 0.5
}
]
},
{
"name": "rightEar",
"parent" : "head",
"pivot": [ -1.9, 12.0, 0.0 ],
"cubes": [
{
"origin": [ -9.0, 31.0, -0.5 ],
"size": [ 6, 6, 1 ],
"uv": [ 24, 0 ],
"inflate": 0.5
}
]
}
],
"description": {
"identifier": "geometry.humanoid.ears",
"texture_height": 64,
"texture_width": 64
}
}
]
}

View File

@ -1,249 +0,0 @@
{
"format_version": "1.14.0",
"minecraft:geometry": [
{
"bones": [
{
"name" : "root",
"pivot" : [ 0.0, 0.0, 0.0 ]
},
{
"name" : "waist",
"parent" : "root",
"pivot" : [ 0.0, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes" : []
},
{
"name": "body",
"parent" : "waist",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 12.0, -2.0 ],
"size": [ 8, 12, 4 ],
"uv": [ 16, 16 ]
}
]
},
{
"name": "jacket",
"parent" : "body",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 12.0, -2.0 ],
"size": [ 8, 12, 4 ],
"uv": [ 16, 32 ],
"inflate": 0.25
}
]
},
{
"name": "head",
"parent" : "body",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 24.0, -4.0 ],
"size": [ 8, 8, 8 ],
"uv": [ 0, 0 ]
}
]
},
{
"name": "hat",
"parent" : "head",
"pivot": [ 0.0, 24.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -4.0, 24.0, -4.0 ],
"size": [ 8, 8, 8 ],
"uv": [ 32, 0 ],
"inflate": 0.5
}
]
},
{
"name": "leftArm",
"parent" : "body",
"pivot": [ 5.0, 21.5, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ 4.0, 12, -2.0 ],
"size": [ 3, 12, 4 ],
"uv": [ 32, 48 ]
}
]
},
{
"name": "rightArm",
"parent" : "body",
"pivot": [ -5.0, 21.5, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -7.0, 12, -2.0 ],
"size": [ 3, 12, 4 ],
"uv": [ 40, 16 ]
}
]
},
{
"name": "leftSleeve",
"parent" : "leftArm",
"pivot": [ 5.0, 21.5, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ 4.0, 11.5, -2.0 ],
"size": [ 3, 12, 4 ],
"uv": [ 48, 48 ],
"inflate": 0.25
}
]
},
{
"name": "rightSleeve",
"parent" : "rightArm",
"pivot": [ -5.0, 21.5, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -7.0, 11.5, -2.0 ],
"size": [ 3, 12, 4 ],
"uv": [ 40, 32 ],
"inflate": 0.25
}
]
},
{
"name": "leftLeg",
"parent" : "root",
"pivot": [ 1.9, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -0.1, 0.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 0, 16 ]
}
]
},
{
"name": "rightLeg",
"parent" : "root",
"pivot": [ -1.9, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -3.9, 0.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 0, 16 ]
}
]
},
{
"name": "leftPants",
"parent" : "leftLeg",
"pivot": [1.9, 12.0, 0.0],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -0.1, 0.0, -2.0 ],
"size": [ 4, 12, 4 ],
"uv": [ 0, 48 ],
"inflate": 0.25
}
]
},
{
"name": "rightPants",
"parent" : "rightLeg",
"pivot": [ -1.9, 12.0, 0.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes": [
{
"origin": [ -3.9, 0.0, -2.0] ,
"size": [ 4, 12, 4 ],
"uv": [ 0, 32],
"inflate": 0.25
}
]
},
{
"name" : "rightItem",
"parent" : "rightArm",
"pivot" : [ -6.0, 15.0, 1.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes" : []
},
{
"name" : "leftItem",
"parent" : "leftArm",
"pivot" : [ 6.0, 15.0, 1.0 ],
"rotation" : [ 0.0, 0.0, 0.0 ],
"cubes" : []
},
{
"name": "leftEar",
"parent" : "head",
"pivot": [ -1.9, 12.0, 0.0 ],
"cubes": [
{
"origin": [ 3.0, 31.0, -0.5 ],
"size": [ 6, 6, 1 ],
"uv": [ 24, 0 ],
"inflate": 0.5
}
]
},
{
"name": "rightEar",
"parent" : "head",
"pivot": [ -1.9, 12.0, 0.0 ],
"cubes": [
{
"origin": [ -9.0, 31.0, -0.5 ],
"size": [ 6, 6, 1 ],
"uv": [ 24, 0 ],
"inflate": 0.5
}
]
}
],
"description": {
"identifier": "geometry.humanoid.earsSlim",
"texture_height": 64,
"texture_width": 64
}
}
]
}

View File

@ -113,14 +113,6 @@ max-players: 100
# If debug messages should be sent through console # If debug messages should be sent through console
debug-mode: false debug-mode: false
# Allow third party capes to be visible. Currently allowing:
# OptiFine capes, LabyMod capes, 5Zig capes and MinecraftCapes
allow-third-party-capes: false
# Allow third party deadmau5 ears to be visible. Currently allowing:
# MinecraftCapes
allow-third-party-ears: false
# Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat. # Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat.
# Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below: # Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below:
# https://cdn.discordapp.com/attachments/613170125696270357/957075682230419466/Screenshot_from_2022-03-25_20-35-08.png # https://cdn.discordapp.com/attachments/613170125696270357/957075682230419466/Screenshot_from_2022-03-25_20-35-08.png

View File

@ -7,5 +7,5 @@ org.gradle.vfs.watch=false
group=org.geysermc group=org.geysermc
id=geyser id=geyser
version=2.2.3-SNAPSHOT version=2.2.4-SNAPSHOT
description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers. description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers.