Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-11-20 06:50:09 +01:00
Merge remote-tracking branch 'upstream/master' into client-vehicle
Dieser Commit ist enthalten in:
Commit
967d0a9fdd
@ -1,6 +1,7 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright (c) 2019-&#36;today.year 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" />
|
||||
<option name="allowReplaceRegexp" value="Copyright" />
|
||||
<option name="notice" value="Copyright (c) &#36;originalComment.match("Copyright \(c\) (\d+)", 1, "-")&#36;today.year 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" />
|
||||
<option name="myName" value="Geyser" />
|
||||
</copyright>
|
||||
</component>
|
@ -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 @NonNull String username() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UUID of the player.
|
||||
*
|
||||
* @return the UUID of the player
|
||||
*/
|
||||
public @NonNull 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 @NonNull SkinData originalSkin() {
|
||||
return originalSkinData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The skin data of the player.
|
||||
*
|
||||
* @return the current skin data of the player
|
||||
*/
|
||||
public abstract @NonNull 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));
|
||||
}
|
||||
}
|
@ -46,13 +46,35 @@ public final class ConnectionRequestEvent implements Event, Cancellable {
|
||||
this.proxyIp = proxyIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The IP address of the client attempting to connect
|
||||
*
|
||||
* @return the IP address of the client attempting to connect
|
||||
* @deprecated Use {@link #inetSocketAddress()} instead
|
||||
*/
|
||||
@NonNull @Deprecated(forRemoval = true)
|
||||
public InetSocketAddress getInetSocketAddress() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* The IP address of the proxy handling the connection. It will return null if there is no proxy.
|
||||
*
|
||||
* @return the IP address of the proxy handling the connection
|
||||
* @deprecated Use {@link #proxyIp()} instead
|
||||
*/
|
||||
@Nullable @Deprecated(forRemoval = true)
|
||||
public InetSocketAddress getProxyIp() {
|
||||
return proxyIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The IP address of the client attempting to connect
|
||||
*
|
||||
* @return the IP address of the client attempting to connect
|
||||
*/
|
||||
@NonNull
|
||||
public InetSocketAddress getInetSocketAddress() {
|
||||
public InetSocketAddress inetSocketAddress() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
@ -62,7 +84,7 @@ public final class ConnectionRequestEvent implements Event, Cancellable {
|
||||
* @return the IP address of the proxy handling the connection
|
||||
*/
|
||||
@Nullable
|
||||
public InetSocketAddress getProxyIp() {
|
||||
public InetSocketAddress proxyIp() {
|
||||
return proxyIp;
|
||||
}
|
||||
|
||||
|
40
api/src/main/java/org/geysermc/geyser/api/skin/Cape.java
Normale Datei
40
api/src/main/java/org/geysermc/geyser/api/skin/Cape.java
Normale Datei
@ -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);
|
||||
}
|
||||
}
|
39
api/src/main/java/org/geysermc/geyser/api/skin/Skin.java
Normale Datei
39
api/src/main/java/org/geysermc/geyser/api/skin/Skin.java
Normale Datei
@ -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);
|
||||
}
|
||||
}
|
32
api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java
Normale Datei
32
api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java
Normale Datei
@ -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) {
|
||||
}
|
48
api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java
Normale Datei
48
api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java
Normale Datei
@ -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" : "") + "\"}}", "");
|
||||
}
|
||||
}
|
@ -31,7 +31,9 @@ dependencies {
|
||||
|
||||
// Let's shade in our own api
|
||||
shadow(projects.api) { isTransitive = false }
|
||||
shadow(projects.common) { isTransitive = false }
|
||||
|
||||
// cannot be shaded, since neoforge will complain if floodgate-neoforge tries to provide this
|
||||
include(projects.common)
|
||||
|
||||
// Include all transitive deps of core via JiJ
|
||||
includeTransitive(projects.core)
|
||||
|
@ -27,6 +27,7 @@ package org.geysermc.geyser.platform.neoforge;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.neoforged.fml.ModContainer;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.fml.loading.FMLLoader;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
@ -43,8 +44,8 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
|
||||
|
||||
private final GeyserNeoForgePermissionHandler permissionHandler = new GeyserNeoForgePermissionHandler();
|
||||
|
||||
public GeyserNeoForgeBootstrap() {
|
||||
super(new GeyserNeoForgePlatform());
|
||||
public GeyserNeoForgeBootstrap(ModContainer container) {
|
||||
super(new GeyserNeoForgePlatform(container));
|
||||
|
||||
if (isServer()) {
|
||||
// Set as an event so we can get the proper IP and port if needed
|
||||
|
@ -26,20 +26,29 @@
|
||||
package org.geysermc.geyser.platform.neoforge;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.neoforged.fml.ModContainer;
|
||||
import net.neoforged.fml.ModList;
|
||||
import net.neoforged.fml.loading.FMLPaths;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.geysermc.geyser.GeyserBootstrap;
|
||||
import org.geysermc.geyser.api.util.PlatformType;
|
||||
import org.geysermc.geyser.dump.BootstrapDumpInfo;
|
||||
import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
|
||||
import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class GeyserNeoForgePlatform implements GeyserModPlatform {
|
||||
|
||||
private final ModContainer container;
|
||||
|
||||
public GeyserNeoForgePlatform(ModContainer container) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull PlatformType platformType() {
|
||||
return PlatformType.NEOFORGE;
|
||||
@ -62,11 +71,21 @@ public class GeyserNeoForgePlatform implements GeyserModPlatform {
|
||||
|
||||
@Override
|
||||
public boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap) {
|
||||
return false; // No Floodgate mod for NeoForge yet
|
||||
if (ModList.get().isLoaded("floodgate")) {
|
||||
Path floodgateDataFolder = FMLPaths.CONFIGDIR.get().resolve("floodgate");
|
||||
bootstrap.getGeyserConfig().loadFloodgate(bootstrap, floodgateDataFolder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable InputStream resolveResource(@NonNull String resource) {
|
||||
return GeyserBootstrap.class.getClassLoader().getResourceAsStream(resource);
|
||||
try {
|
||||
Path path = container.getModInfo().getOwningFile().getFile().findResource(resource);
|
||||
return Files.newInputStream(path);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -72,8 +72,10 @@ public interface GeyserConfiguration {
|
||||
|
||||
boolean isDebugMode();
|
||||
|
||||
@Deprecated
|
||||
boolean isAllowThirdPartyCapes();
|
||||
|
||||
@Deprecated
|
||||
boolean isAllowThirdPartyEars();
|
||||
|
||||
String getShowCooldown();
|
||||
|
@ -53,6 +53,16 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Item {
|
||||
/**
|
||||
* This is a map from Java-only enchantments to their translation keys so that we can
|
||||
* map these enchantments to Bedrock clients, since they don't actually exist there.
|
||||
*/
|
||||
private static final Map<Enchantment.JavaEnchantment, String> ENCHANTMENT_TRANSLATION_KEYS = Map.of(
|
||||
Enchantment.JavaEnchantment.SWEEPING_EDGE, "enchantment.minecraft.sweeping",
|
||||
Enchantment.JavaEnchantment.DENSITY, "enchantment.minecraft.density",
|
||||
Enchantment.JavaEnchantment.BREACH, "enchantment.minecraft.breach",
|
||||
Enchantment.JavaEnchantment.WIND_BURST, "enchantment.minecraft.wind_burst");
|
||||
|
||||
private final String javaIdentifier;
|
||||
private int javaId = -1;
|
||||
private final int stackSize;
|
||||
@ -227,8 +237,10 @@ public class Item {
|
||||
// TODO verify
|
||||
// TODO streamline Enchantment process
|
||||
Enchantment.JavaEnchantment enchantment = Enchantment.JavaEnchantment.of(enchantId);
|
||||
if (enchantment == Enchantment.JavaEnchantment.SWEEPING_EDGE) {
|
||||
addSweeping(session, builder, level);
|
||||
String translationKey = ENCHANTMENT_TRANSLATION_KEYS.get(enchantment);
|
||||
if (translationKey != null) {
|
||||
String enchantmentTranslation = MinecraftLocale.getLocaleString(translationKey, session.locale());
|
||||
addJavaOnlyEnchantment(session, builder, enchantmentTranslation, level);
|
||||
return null;
|
||||
}
|
||||
if (enchantment == null) {
|
||||
@ -242,11 +254,10 @@ public class Item {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addSweeping(GeyserSession session, BedrockItemBuilder builder, int level) {
|
||||
String sweepingTranslation = MinecraftLocale.getLocaleString("enchantment.minecraft.sweeping", session.locale());
|
||||
private void addJavaOnlyEnchantment(GeyserSession session, BedrockItemBuilder builder, String enchantmentName, int level) {
|
||||
String lvlTranslation = MinecraftLocale.getLocaleString("enchantment.level." + level, session.locale());
|
||||
|
||||
builder.getOrCreateLore().add(ChatColor.RESET + ChatColor.GRAY + sweepingTranslation + " " + lvlTranslation);
|
||||
builder.getOrCreateLore().add(ChatColor.RESET + ChatColor.GRAY + enchantmentName + " " + lvlTranslation);
|
||||
}
|
||||
|
||||
/* Translation methods end */
|
||||
|
@ -25,11 +25,8 @@
|
||||
|
||||
package org.geysermc.geyser.network;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.DefaultEventLoopGroup;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.util.concurrent.DefaultThreadFactory;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.cloudburstmc.protocol.bedrock.BedrockPeer;
|
||||
@ -37,7 +34,6 @@ import org.cloudburstmc.protocol.bedrock.BedrockServerSession;
|
||||
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
|
||||
import org.cloudburstmc.protocol.bedrock.netty.initializer.BedrockServerInitializer;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.event.bedrock.SessionInitializeEvent;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
@ -72,7 +68,6 @@ public class GeyserServerInitializer extends BedrockServerInitializer {
|
||||
channel.pipeline().addAfter(BedrockPacketCodec.NAME, InvalidPacketHandler.NAME, new InvalidPacketHandler(session));
|
||||
|
||||
bedrockServerSession.setPacketHandler(new UpstreamPacketHandler(this.geyser, session));
|
||||
this.geyser.eventBus().fire(new SessionInitializeEvent(session));
|
||||
} catch (Throwable e) {
|
||||
// Error must be caught or it will be swallowed
|
||||
this.geyser.getLogger().error("Error occurred while initializing player!", e);
|
||||
|
@ -54,6 +54,7 @@ import org.cloudburstmc.protocol.common.PacketSignal;
|
||||
import org.cloudburstmc.protocol.common.util.Zlib;
|
||||
import org.geysermc.geyser.Constants;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.event.bedrock.SessionInitializeEvent;
|
||||
import org.geysermc.geyser.api.network.AuthType;
|
||||
import org.geysermc.geyser.api.pack.PackCodec;
|
||||
import org.geysermc.geyser.api.pack.ResourcePack;
|
||||
@ -188,6 +189,9 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
|
||||
return PacketSignal.HANDLED;
|
||||
}
|
||||
|
||||
// Fire SessionInitializeEvent here as we now know the client data
|
||||
geyser.eventBus().fire(new SessionInitializeEvent(session));
|
||||
|
||||
PlayStatusPacket playStatus = new PlayStatusPacket();
|
||||
playStatus.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS);
|
||||
session.sendUpstreamPacket(playStatus);
|
||||
|
@ -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
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -118,7 +118,7 @@ public class SkullResourcePackManager {
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage image = SkinProvider.requestImage(skinUrl, null);
|
||||
BufferedImage image = SkinProvider.requestImage(skinUrl, false);
|
||||
// Resize skins to 48x16 to save on space and memory
|
||||
BufferedImage skullTexture = new BufferedImage(48, 16, image.getType());
|
||||
// Reorder skin parts to fit into the space
|
||||
|
@ -1083,9 +1083,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
||||
if (!closed) {
|
||||
loggedIn = false;
|
||||
|
||||
// Fire SessionDisconnectEvent
|
||||
SessionDisconnectEvent disconnectEvent = new SessionDisconnectEvent(this, reason);
|
||||
if (authData != null && clientData != null) { // can occur if player disconnects before Bedrock auth finishes
|
||||
// Fire SessionDisconnectEvent
|
||||
geyser.getEventBus().fire(disconnectEvent);
|
||||
}
|
||||
|
||||
// Disconnect downstream if necessary
|
||||
if (downstream != null) {
|
||||
@ -1416,7 +1418,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return null;
|
||||
return playerEntity != null ? javaUsername() : bedrockUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1953,12 +1955,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
||||
|
||||
@Override
|
||||
public @MonotonicNonNull String javaUsername() {
|
||||
return playerEntity.getUsername();
|
||||
return playerEntity != null ? playerEntity.getUsername() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID javaUuid() {
|
||||
return playerEntity.getUuid();
|
||||
return playerEntity != null ? playerEntity.getUuid() : null ;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -38,6 +38,10 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
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.player.PlayerEntity;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
@ -59,27 +63,27 @@ import java.util.concurrent.TimeUnit;
|
||||
* Responsible for modifying a player's skin when wearing a player head
|
||||
*/
|
||||
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)
|
||||
.maximumSize(10000)
|
||||
.build(new CacheLoader<>() {
|
||||
@Override
|
||||
public SkinProvider.SkinData load(@NonNull FakeHeadEntry fakeHeadEntry) throws Exception {
|
||||
SkinProvider.SkinData skinData = SkinProvider.getOrDefault(SkinProvider.requestSkinData(fakeHeadEntry.getEntity()), null, 5);
|
||||
public SkinData load(@NonNull FakeHeadEntry fakeHeadEntry) throws Exception {
|
||||
SkinData skinData = SkinProvider.getOrDefault(SkinProvider.requestSkinData(fakeHeadEntry.getEntity(), fakeHeadEntry.getSession()), null, 5);
|
||||
|
||||
if (skinData == null) {
|
||||
throw new Exception("Couldn't load player's original skin");
|
||||
}
|
||||
|
||||
SkinProvider.Skin skin = skinData.skin();
|
||||
SkinProvider.Cape cape = skinData.cape();
|
||||
SkinProvider.SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
|
||||
Skin skin = skinData.skin();
|
||||
Cape cape = skinData.cape();
|
||||
SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
|
||||
? 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);
|
||||
BufferedImage originalSkinImage = SkinProvider.imageDataToBufferedImage(skin.getSkinData(), 64, skin.getSkinData().length / 4 / 64);
|
||||
BufferedImage headSkinImage = SkinProvider.imageDataToBufferedImage(headSkin.getSkinData(), 64, headSkin.getSkinData().length / 4 / 64);
|
||||
BufferedImage originalSkinImage = SkinProvider.imageDataToBufferedImage(skin.skinData(), 64, skin.skinData().length / 4 / 64);
|
||||
BufferedImage headSkinImage = SkinProvider.imageDataToBufferedImage(headSkin.skinData(), 64, headSkin.skinData().length / 4 / 64);
|
||||
|
||||
Graphics2D graphics2D = originalSkinImage.createGraphics();
|
||||
graphics2D.setComposite(AlphaComposite.Clear);
|
||||
@ -90,14 +94,15 @@ public class FakeHeadProvider {
|
||||
|
||||
// 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
|
||||
String skinKey = "customPlayerHead_" + fakeHeadEntry.getFakeHeadSkinUrl() + "_" + skin.getTextureUrl();
|
||||
String skinKey = "customPlayerHead_" + fakeHeadEntry.getFakeHeadSkinUrl() + "_" + skin.textureUrl();
|
||||
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
|
||||
fakeHeadEntry.setEntity(null);
|
||||
fakeHeadEntry.setSession(null);
|
||||
|
||||
return new SkinProvider.SkinData(mergedSkin, cape, geometry);
|
||||
return new SkinData(mergedSkin, cape, geometry);
|
||||
}
|
||||
});
|
||||
|
||||
@ -164,7 +169,7 @@ public class FakeHeadProvider {
|
||||
String texturesProperty = entity.getTexturesProperty();
|
||||
SkinProvider.getExecutorService().execute(() -> {
|
||||
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);
|
||||
} catch (ExecutionException e) {
|
||||
GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e);
|
||||
@ -181,7 +186,7 @@ public class FakeHeadProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> {
|
||||
SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> {
|
||||
if (throwable != null) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), throwable);
|
||||
return;
|
||||
@ -198,6 +203,7 @@ public class FakeHeadProvider {
|
||||
private final String texturesProperty;
|
||||
private final String fakeHeadSkinUrl;
|
||||
private PlayerEntity entity;
|
||||
private GeyserSession session;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
@ -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
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -26,6 +26,7 @@
|
||||
package org.geysermc.geyser.skin;
|
||||
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.skin.Skin;
|
||||
import org.geysermc.geyser.util.AssetUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@ -67,7 +68,7 @@ public final class ProvidedSkins {
|
||||
}
|
||||
|
||||
public static final class ProvidedSkin {
|
||||
private SkinProvider.Skin data;
|
||||
private Skin data;
|
||||
private final boolean slim;
|
||||
|
||||
ProvidedSkin(String asset, boolean slim) {
|
||||
@ -94,14 +95,14 @@ public final class ProvidedSkins {
|
||||
image.flush();
|
||||
|
||||
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) {
|
||||
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.
|
||||
return Objects.requireNonNullElse(data, SkinProvider.EMPTY_SKIN);
|
||||
}
|
||||
|
@ -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
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@ -34,6 +34,10 @@ import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
|
||||
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.SkullPlayerEntity;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
@ -56,21 +60,21 @@ public class SkinManager {
|
||||
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
|
||||
// First: see if we have the cached skin texture ID.
|
||||
GameProfileData data = GameProfileData.from(playerEntity);
|
||||
SkinProvider.Skin skin = null;
|
||||
SkinProvider.Cape cape = null;
|
||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.WIDE;
|
||||
Skin skin = null;
|
||||
Cape cape = null;
|
||||
SkinGeometry geometry = SkinGeometry.WIDE;
|
||||
if (data != null) {
|
||||
// GameProfileData is not null = server provided us with textures data to work with.
|
||||
skin = SkinProvider.getCachedSkin(data.skinUrl());
|
||||
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) {
|
||||
// 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.
|
||||
// Otherwise, grab the default player skin
|
||||
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
|
||||
SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
|
||||
if (skin == null) {
|
||||
skin = fallbackSkinData.skin();
|
||||
geometry = fallbackSkinData.geometry();
|
||||
@ -95,10 +99,10 @@ public class SkinManager {
|
||||
* 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,
|
||||
SkinProvider.Skin skin,
|
||||
SkinProvider.Cape cape,
|
||||
SkinProvider.SkinGeometry geometry) {
|
||||
SerializedSkin serializedSkin = getSkin(skin.getTextureUrl(), skin, cape, geometry);
|
||||
Skin skin,
|
||||
Cape cape,
|
||||
SkinGeometry 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
|
||||
String xuid = "";
|
||||
@ -128,10 +132,10 @@ public class SkinManager {
|
||||
return entry;
|
||||
}
|
||||
|
||||
public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) {
|
||||
SkinProvider.Skin skin = skinData.skin();
|
||||
SkinProvider.Cape cape = skinData.cape();
|
||||
SkinProvider.SkinGeometry geometry = skinData.geometry();
|
||||
public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinData skinData) {
|
||||
Skin skin = skinData.skin();
|
||||
Cape cape = skinData.cape();
|
||||
SkinGeometry geometry = skinData.geometry();
|
||||
|
||||
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
|
||||
// TODO is this special behavior needed?
|
||||
@ -153,23 +157,23 @@ public class SkinManager {
|
||||
PlayerSkinPacket packet = new PlayerSkinPacket();
|
||||
packet.setUuid(entity.getUuid());
|
||||
packet.setOldSkinName("");
|
||||
packet.setNewSkinName(skin.getTextureUrl());
|
||||
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
|
||||
packet.setNewSkinName(skin.textureUrl());
|
||||
packet.setSkin(getSkin(skin.textureUrl(), skin, cape, geometry));
|
||||
packet.setTrustedSkin(true);
|
||||
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(),
|
||||
ImageData.of(skin.getSkinData()), Collections.emptyList(),
|
||||
ImageData.of(skin.skinData()), Collections.emptyList(),
|
||||
ImageData.of(cape.capeData()), geometry.geometryData(),
|
||||
"", true, false, false, cape.capeId(), skinId);
|
||||
}
|
||||
|
||||
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
||||
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
|
||||
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> {
|
||||
SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> {
|
||||
if (skinData == null) {
|
||||
if (skinAndCapeConsumer != null) {
|
||||
skinAndCapeConsumer.accept(null);
|
||||
|
@ -35,7 +35,12 @@ import lombok.NoArgsConstructor;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
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.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.session.GeyserSession;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
@ -45,7 +50,6 @@ import org.geysermc.geyser.util.WebUtils;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -57,11 +61,10 @@ import java.util.concurrent.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class SkinProvider {
|
||||
private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
|
||||
private static ExecutorService EXECUTOR_SERVICE;
|
||||
|
||||
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()
|
||||
.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 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 WEARING_CUSTOM_SKULL;
|
||||
static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
|
||||
@ -114,28 +114,27 @@ public class SkinProvider {
|
||||
outputStream.write((rgba >> 24) & 0xFF); // Alpha
|
||||
}
|
||||
}
|
||||
EMPTY_SKIN = new Skin(-1, "geysermc:empty", outputStream.toByteArray());
|
||||
|
||||
/* 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);
|
||||
EMPTY_SKIN = new Skin("geysermc:empty", outputStream.toByteArray(), true);
|
||||
|
||||
/* Load in the custom skull geometry */
|
||||
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 */
|
||||
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);
|
||||
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() {
|
||||
if (EXECUTOR_SERVICE == null) {
|
||||
EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
|
||||
EXECUTOR_SERVICE = Executors.newFixedThreadPool(14);
|
||||
}
|
||||
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.
|
||||
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid);
|
||||
skin = providedSkin.getData();
|
||||
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
||||
geometry = providedSkin.isSlim() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
|
||||
}
|
||||
|
||||
if (cape == null) {
|
||||
@ -238,7 +237,7 @@ public class SkinProvider {
|
||||
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);
|
||||
if (data == null) {
|
||||
// This player likely does not have a textures property
|
||||
@ -260,42 +259,33 @@ public class SkinProvider {
|
||||
cape = getCachedBedrockCape(entity.getUuid());
|
||||
}
|
||||
|
||||
if (cape.failed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||
cape = getOrDefault(requestUnofficialCape(
|
||||
cape, entity.getUuid(),
|
||||
entity.getUsername(), false
|
||||
), EMPTY_CAPE, CapeProvider.VALUES.length * 3);
|
||||
// Call event to allow extensions to modify the skin, cape and geo
|
||||
boolean isBedrock = GeyserImpl.getInstance().connectionByUuid(entity.getUuid()) != null;
|
||||
SkinData skinData = new SkinData(skin, cape, geometry);
|
||||
final EventSkinData eventSkinData = new EventSkinData(skinData);
|
||||
GeyserImpl.getInstance().eventBus().fire(new SessionSkinApplyEvent(session, entity.getUsername(), entity.getUuid(), data.isAlex(), isBedrock, skinData) {
|
||||
@Override
|
||||
public SkinData skinData() {
|
||||
return eventSkinData.skinData();
|
||||
}
|
||||
|
||||
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername());
|
||||
// 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();
|
||||
@Override
|
||||
public void skin(@NonNull Skin newSkin) {
|
||||
eventSkinData.skinData(new SkinData(Objects.requireNonNull(newSkin), eventSkinData.skinData().cape(), eventSkinData.skinData().geometry()));
|
||||
}
|
||||
|
||||
// Does the skin have an ears texture
|
||||
if (isEars) {
|
||||
// Get the new geometry
|
||||
geometry = SkinGeometry.getEars(data.isAlex());
|
||||
|
||||
// Store the skin and geometry for the ears
|
||||
storeEarSkin(skin);
|
||||
storeEarGeometry(entity.getUuid(), data.isAlex());
|
||||
}
|
||||
@Override
|
||||
public void cape(@NonNull Cape newCape) {
|
||||
eventSkinData.skinData(new SkinData(eventSkinData.skinData().skin(), Objects.requireNonNull(newCape), eventSkinData.skinData().geometry()));
|
||||
}
|
||||
|
||||
return new SkinData(skin, cape, geometry);
|
||||
@Override
|
||||
public void geometry(@NonNull SkinGeometry newGeometry) {
|
||||
eventSkinData.skinData(new SkinData(eventSkinData.skinData().skin(), eventSkinData.skinData().cape(), Objects.requireNonNull(newGeometry)));
|
||||
}
|
||||
});
|
||||
|
||||
return eventSkinData.skinData();
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
||||
}
|
||||
@ -308,10 +298,9 @@ public class SkinProvider {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
|
||||
SkinAndCape skinAndCape = new SkinAndCape(
|
||||
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);
|
||||
@ -336,7 +325,6 @@ public class SkinProvider {
|
||||
if (newThread) {
|
||||
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), getExecutorService())
|
||||
.whenCompleteAsync((skin, throwable) -> {
|
||||
skin.updated = true;
|
||||
CACHED_JAVA_SKINS.put(textureUrl, skin);
|
||||
requestedSkins.remove(textureUrl);
|
||||
});
|
||||
@ -349,7 +337,7 @@ public class SkinProvider {
|
||||
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);
|
||||
CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl);
|
||||
if (requestedCape != null) {
|
||||
@ -363,128 +351,48 @@ public class SkinProvider {
|
||||
|
||||
CompletableFuture<Cape> future;
|
||||
if (newThread) {
|
||||
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), getExecutorService())
|
||||
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl), getExecutorService())
|
||||
.whenCompleteAsync((cape, throwable) -> {
|
||||
CACHED_JAVA_CAPES.put(capeUrl, cape);
|
||||
requestedCapes.remove(capeUrl);
|
||||
});
|
||||
requestedCapes.put(capeUrl, future);
|
||||
} else {
|
||||
Cape cape = supplyCape(capeUrl, provider); // blocking
|
||||
Cape cape = supplyCape(capeUrl); // blocking
|
||||
future = CompletableFuture.completedFuture(cape);
|
||||
CACHED_JAVA_CAPES.put(capeUrl, cape);
|
||||
}
|
||||
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) {
|
||||
Skin skin = new Skin(playerID, skinId, skinData, System.currentTimeMillis(), true, false);
|
||||
CACHED_BEDROCK_SKINS.put(skin.getTextureUrl(), skin);
|
||||
Skin skin = new Skin(skinId, skinData);
|
||||
CACHED_BEDROCK_SKINS.put(skin.textureUrl(), skin);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
byte[] skin = requestImageData(textureUrl, null);
|
||||
return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false);
|
||||
byte[] skin = requestImageData(textureUrl, false);
|
||||
return new Skin(textureUrl, skin);
|
||||
} 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();
|
||||
try {
|
||||
cape = requestImageData(capeUrl, provider);
|
||||
cape = requestImageData(capeUrl, true);
|
||||
} catch (Exception ignored) {
|
||||
} // just ignore I guess
|
||||
|
||||
@ -494,54 +402,12 @@ public class SkinProvider {
|
||||
capeUrl,
|
||||
urlSection[urlSection.length - 1], // get the texture id and use it as cape id
|
||||
cape,
|
||||
System.currentTimeMillis(),
|
||||
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")
|
||||
public static BufferedImage requestImage(String imageUrl, CapeProvider provider) throws IOException {
|
||||
public static BufferedImage requestImage(String imageUrl, boolean isCape) throws IOException {
|
||||
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
|
||||
@ -556,7 +422,7 @@ public class SkinProvider {
|
||||
|
||||
// If no image we download it
|
||||
if (image == null) {
|
||||
image = downloadImage(imageUrl, provider);
|
||||
image = downloadImage(imageUrl);
|
||||
GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl);
|
||||
|
||||
// Write to cache if we are allowed
|
||||
@ -572,7 +438,7 @@ public class SkinProvider {
|
||||
}
|
||||
|
||||
// if the requested image is a cape
|
||||
if (provider != null) {
|
||||
if (isCape) {
|
||||
if (image.getWidth() > 64 || image.getHeight() > 32) {
|
||||
// Prevent weirdly-scaled capes from being cut off
|
||||
BufferedImage newImage = new BufferedImage(128, 64, BufferedImage.TYPE_INT_ARGB);
|
||||
@ -604,8 +470,8 @@ public class SkinProvider {
|
||||
return image;
|
||||
}
|
||||
|
||||
private static byte[] requestImageData(String imageUrl, CapeProvider provider) throws Exception {
|
||||
BufferedImage image = requestImage(imageUrl, provider);
|
||||
private static byte[] requestImageData(String imageUrl, boolean isCape) throws Exception {
|
||||
BufferedImage image = requestImage(imageUrl, isCape);
|
||||
byte[] data = bufferedImageToImageData(image);
|
||||
image.flush();
|
||||
return data;
|
||||
@ -668,35 +534,20 @@ public class SkinProvider {
|
||||
});
|
||||
}
|
||||
|
||||
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
|
||||
BufferedImage image;
|
||||
if (provider == CapeProvider.FIVEZIG) {
|
||||
image = readFiveZigCape(imageUrl);
|
||||
} else {
|
||||
private static BufferedImage downloadImage(String imageUrl) throws IOException {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = resized.createGraphics();
|
||||
@ -770,124 +621,19 @@ public class SkinProvider {
|
||||
public record SkinAndCape(Skin skin, Cape cape) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a full package of skin, cape, and geometry.
|
||||
*/
|
||||
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
|
||||
public static class EventSkinData {
|
||||
private SkinData skinData;
|
||||
|
||||
public EventSkinData(SkinData skinData) {
|
||||
this.skinData = skinData;
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
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;
|
||||
public SkinData skinData() {
|
||||
return skinData;
|
||||
}
|
||||
|
||||
Skin(long requestedOn, String textureUrl, byte[] skinData) {
|
||||
this.requestedOn = requestedOn;
|
||||
this.textureUrl = textureUrl;
|
||||
public void skinData(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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
* 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.packet.PlayerSkinPacket;
|
||||
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.session.GeyserSession;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
@ -50,14 +52,14 @@ public class SkullSkinManager extends SkinManager {
|
||||
}
|
||||
|
||||
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
|
||||
Consumer<SkinProvider.Skin> skinConsumer) {
|
||||
BiConsumer<SkinProvider.Skin, Throwable> applySkin = (skin, throwable) -> {
|
||||
Consumer<Skin> skinConsumer) {
|
||||
BiConsumer<Skin, Throwable> applySkin = (skin, throwable) -> {
|
||||
try {
|
||||
PlayerSkinPacket packet = new PlayerSkinPacket();
|
||||
packet.setUuid(entity.getUuid());
|
||||
packet.setOldSkinName("");
|
||||
packet.setNewSkinName(skin.getTextureUrl());
|
||||
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
|
||||
packet.setNewSkinName(skin.textureUrl());
|
||||
packet.setSkin(buildSkullEntryManually(skin.textureUrl(), skin.skinData()));
|
||||
packet.setTrustedSkin(true);
|
||||
session.sendUpstreamPacket(packet);
|
||||
} catch (Exception e) {
|
||||
@ -74,7 +76,7 @@ public class SkullSkinManager extends SkinManager {
|
||||
GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() +
|
||||
" with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID());
|
||||
// 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);
|
||||
} else {
|
||||
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -113,14 +113,6 @@ max-players: 100
|
||||
# If debug messages should be sent through console
|
||||
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.
|
||||
# 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
|
||||
|
@ -7,5 +7,5 @@ org.gradle.vfs.watch=false
|
||||
|
||||
group=org.geysermc
|
||||
id=geyser
|
||||
version=2.3.0-SNAPSHOT
|
||||
version=2.3.1-SNAPSHOT
|
||||
description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers.
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren