Merge branch 'dev'

This commit is contained in:
Camotoy 2023-01-02 12:19:19 -05:00
commit c483204446
No known key found for this signature in database
GPG Key ID: 7EEFB66FE798081F
38 changed files with 1427 additions and 651 deletions

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2019-2021 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.platform.bungeecord;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import net.md_5.bungee.protocol.packet.LoginSuccess;
import net.md_5.bungee.protocol.packet.SetCompression;
public class GeyserBungeeCompressionDisabler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof SetCompression)) {
if (msg instanceof LoginSuccess) {
// We're past the point that compression can be enabled
if (ctx.pipeline().get("compress") != null) {
ctx.pipeline().remove("compress");
}
if (ctx.pipeline().get("decompress") != null) {
ctx.pipeline().remove("decompress");
}
ctx.pipeline().remove(this);
}
super.write(ctx, msg, promise);
}
}
}

View File

@ -140,6 +140,11 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener {
channelInitializer = PipelineUtils.SERVER_CHILD;
}
initChannel.invoke(channelInitializer, ch);
if (bootstrap.getGeyserConfig().isDisableCompression()) {
ch.pipeline().addAfter(PipelineUtils.PACKET_ENCODER, "geyser-compression-disabler",
new GeyserBungeeCompressionDisabler());
}
}
})
.childAttr(listener, listenerInfo)
@ -163,7 +168,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener {
// If native compression is enabled, then this line is tripped up if a heap buffer is sent over in such a situation
// as a new direct buffer is not created with that patch (HeapByteBufs throw an UnsupportedOperationException here):
// https://github.com/SpigotMC/BungeeCord/blob/a283aaf724d4c9a815540cd32f3aafaa72df9e05/native/src/main/java/net/md_5/bungee/jni/zlib/NativeZlib.java#L43
// This issue could be mitigated down the line by preventing Bungee from setting compression
// If disable compression is enabled, this can probably be disabled now, but BungeeCord (not Waterfall) complains
LocalSession.createDirectByteBufAllocator();
}

View File

@ -31,12 +31,13 @@ import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.WritableBookItem;
import net.minecraft.world.item.WrittenBookItem;
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import org.geysermc.geyser.level.GeyserWorldManager;
@ -44,8 +45,10 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class GeyserFabricWorldManager extends GeyserWorldManager {
@ -127,7 +130,127 @@ public class GeyserFabricWorldManager extends GeyserWorldManager {
return Permissions.check(player, permission);
}
@Nonnull
@Override
public CompletableFuture<com.github.steveice10.opennbt.tag.builtin.CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
CompletableFuture<com.github.steveice10.opennbt.tag.builtin.CompoundTag> future = new CompletableFuture<>();
server.execute(() -> {
ServerPlayer player = getPlayer(session);
if (player == null) {
future.complete(null);
return;
}
BlockPos pos = new BlockPos(x, y, z);
// Don't create a new block entity if invalid
BlockEntity blockEntity = player.level.getChunkAt(pos).getBlockEntity(pos);
if (blockEntity instanceof BannerBlockEntity banner) {
// Potentially exposes other NBT data? But we need to get the NBT data for the banner patterns *and*
// the banner might have a custom name, both of which a Java client knows and caches
ItemStack itemStack = banner.getItem();
var tag = OpenNbtTagVisitor.convert("", itemStack.getOrCreateTag());
future.complete(tag);
return;
}
future.complete(null);
});
return future;
}
private ServerPlayer getPlayer(GeyserSession session) {
return server.getPlayerList().getPlayer(session.getPlayerEntity().getUuid());
}
// Future considerations: option to clone; would affect arrays
private static class OpenNbtTagVisitor implements TagVisitor {
private String currentKey;
private final com.github.steveice10.opennbt.tag.builtin.CompoundTag root;
private com.github.steveice10.opennbt.tag.builtin.Tag currentTag;
OpenNbtTagVisitor(String key) {
root = new com.github.steveice10.opennbt.tag.builtin.CompoundTag(key);
}
@Override
public void visitString(StringTag stringTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.StringTag(currentKey, stringTag.getAsString());
}
@Override
public void visitByte(ByteTag byteTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteTag(currentKey, byteTag.getAsByte());
}
@Override
public void visitShort(ShortTag shortTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.ShortTag(currentKey, shortTag.getAsShort());
}
@Override
public void visitInt(IntTag intTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.IntTag(currentKey, intTag.getAsInt());
}
@Override
public void visitLong(LongTag longTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.LongTag(currentKey, longTag.getAsLong());
}
@Override
public void visitFloat(FloatTag floatTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.FloatTag(currentKey, floatTag.getAsFloat());
}
@Override
public void visitDouble(DoubleTag doubleTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.DoubleTag(currentKey, doubleTag.getAsDouble());
}
@Override
public void visitByteArray(ByteArrayTag byteArrayTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteArrayTag(currentKey, byteArrayTag.getAsByteArray());
}
@Override
public void visitIntArray(IntArrayTag intArrayTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.IntArrayTag(currentKey, intArrayTag.getAsIntArray());
}
@Override
public void visitLongArray(LongArrayTag longArrayTag) {
currentTag = new com.github.steveice10.opennbt.tag.builtin.LongArrayTag(currentKey, longArrayTag.getAsLongArray());
}
@Override
public void visitList(ListTag listTag) {
var newList = new com.github.steveice10.opennbt.tag.builtin.ListTag(currentKey);
for (Tag tag : listTag) {
currentKey = "";
tag.accept(this);
newList.add(currentTag);
}
currentTag = newList;
}
@Override
public void visitCompound(CompoundTag compoundTag) {
currentTag = convert(currentKey, compoundTag);
}
private static com.github.steveice10.opennbt.tag.builtin.CompoundTag convert(String name, CompoundTag compoundTag) {
OpenNbtTagVisitor visitor = new OpenNbtTagVisitor(name);
for (String key : compoundTag.getAllKeys()) {
visitor.currentKey = key;
Tag tag = compoundTag.get(key);
tag.accept(visitor);
visitor.root.put(visitor.currentTag);
}
return visitor.root;
}
@Override
public void visitEnd(EndTag endTag) {
}
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) 2019-2021 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.platform.spigot;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import org.bukkit.Bukkit;
import org.geysermc.geyser.GeyserImpl;
/**
* Disables the compression packet (and the compression handlers from being added to the pipeline) for Geyser clients
* that won't be receiving the data over the network.
*
* As of 1.8 - 1.17.1, compression is enabled in the Netty pipeline by adding a listener after a packet is written.
* If we simply "cancel" or don't forward the packet, then the listener is never called.
*/
public class GeyserSpigotCompressionDisabler extends ChannelOutboundHandlerAdapter {
static final boolean ENABLED;
private static final Class<?> COMPRESSION_PACKET_CLASS;
private static final Class<?> LOGIN_SUCCESS_PACKET_CLASS;
private static final boolean PROTOCOL_SUPPORT_INSTALLED;
static {
PROTOCOL_SUPPORT_INSTALLED = Bukkit.getPluginManager().getPlugin("ProtocolSupport") != null;
Class<?> compressionPacketClass = null;
Class<?> loginSuccessPacketClass = null;
boolean enabled = false;
try {
compressionPacketClass = findCompressionPacket();
loginSuccessPacketClass = findLoginSuccessPacket();
enabled = true;
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Could not initialize compression disabler!", e);
}
COMPRESSION_PACKET_CLASS = compressionPacketClass;
LOGIN_SUCCESS_PACKET_CLASS = loginSuccessPacketClass;
ENABLED = enabled;
}
public GeyserSpigotCompressionDisabler() {
if (!ENABLED) {
throw new RuntimeException("Geyser compression disabler cannot be initialized in its current state!");
}
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
Class<?> msgClass = msg.getClass();
// Don't let any compression packet get through
if (!COMPRESSION_PACKET_CLASS.isAssignableFrom(msgClass)) {
if (LOGIN_SUCCESS_PACKET_CLASS.isAssignableFrom(msgClass)) {
if (PROTOCOL_SUPPORT_INSTALLED) {
// ProtocolSupport must send the compression packet, so let's remove what it did before it does damage
if (ctx.pipeline().get("compress") != null) {
ctx.pipeline().remove("compress");
}
if (ctx.pipeline().get("decompress") != null) {
ctx.pipeline().remove("decompress");
}
}
// We're past the point that a compression packet can be sent, so we can safely yeet ourselves away
ctx.channel().pipeline().remove(this);
}
super.write(ctx, msg, promise);
} else if (PROTOCOL_SUPPORT_INSTALLED) {
// We must indicate it "succeeded" or ProtocolSupport will time us out
promise.setSuccess();
}
}
private static Class<?> findCompressionPacket() throws ClassNotFoundException {
try {
return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression");
} catch (ClassNotFoundException e) {
String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
return Class.forName(prefix + ".PacketLoginOutSetCompression");
}
}
private static Class<?> findLoginSuccessPacket() throws ClassNotFoundException {
try {
return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess");
} catch (ClassNotFoundException e) {
String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
return Class.forName(prefix + ".PacketLoginOutSuccess");
}
}
}

View File

@ -115,10 +115,14 @@ public class GeyserSpigotInjector extends GeyserInjector {
ChannelFuture channelFuture = (new ServerBootstrap()
.channel(LocalServerChannelWrapper.class)
.childHandler(new ChannelInitializer<Channel>() {
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) throws Exception {
initChannel.invoke(childHandler, ch);
if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserSpigotCompressionDisabler.ENABLED) {
ch.pipeline().addAfter("encoder", "geyser-compression-disabler", new GeyserSpigotCompressionDisabler());
}
}
})
// Set to MAX_PRIORITY as MultithreadEventLoopGroup#newDefaultThreadFactory which DefaultEventLoopGroup implements does by default

View File

@ -25,14 +25,17 @@
package org.geysermc.geyser.platform.spigot.world.manager;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Lectern;
import org.bukkit.block.*;
import org.bukkit.block.banner.Pattern;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
@ -43,10 +46,14 @@ import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.nbt.BannerTranslator;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* The base world manager to use when there is no supported NMS revision
@ -173,6 +180,46 @@ public class GeyserSpigotWorldManager extends WorldManager {
return Bukkit.getPlayer(session.getPlayerEntity().getUsername()).hasPermission(permission);
}
@Nonnull
@Override
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
CompletableFuture<@Nullable CompoundTag> future = new CompletableFuture<>();
// Paper 1.19.3 complains about async access otherwise.
// java.lang.IllegalStateException: Tile is null, asynchronous access?
Bukkit.getScheduler().runTask(this.plugin, () -> {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) {
future.complete(null);
return;
}
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
BlockState state = block.getState();
if (state instanceof Banner banner) {
ListTag list = new ListTag("Patterns");
for (int i = 0; i < banner.numberOfPatterns(); i++) {
Pattern pattern = banner.getPattern(i);
list.add(BannerTranslator.getJavaPatternTag(pattern.getPattern().getIdentifier(), pattern.getColor().ordinal()));
}
CompoundTag root = addToBlockEntityTag(list);
future.complete(root);
return;
}
future.complete(null);
});
return future;
}
private CompoundTag addToBlockEntityTag(Tag tag) {
CompoundTag compoundTag = new CompoundTag("");
CompoundTag blockEntityTag = new CompoundTag("BlockEntityTag");
blockEntityTag.put(tag);
compoundTag.put(blockEntityTag);
return compoundTag;
}
/**
* This should be set to true if we are post-1.13 but before the latest version, and we should convert the old block state id
* to the current one.

View File

@ -0,0 +1,99 @@
/*
* Copyright (c) 2019-2021 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.platform.velocity;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import org.geysermc.geyser.GeyserImpl;
import java.lang.reflect.Method;
public class GeyserVelocityCompressionDisabler extends ChannelDuplexHandler {
static final boolean ENABLED;
private static final Class<?> COMPRESSION_PACKET_CLASS;
private static final Class<?> LOGIN_SUCCESS_PACKET_CLASS;
private static final Object COMPRESSION_ENABLED_EVENT;
private static final Method SET_COMPRESSION_METHOD;
static {
boolean enabled = false;
Class<?> compressionPacketClass = null;
Class<?> loginSuccessPacketClass = null;
Object compressionEnabledEvent = null;
Method setCompressionMethod = null;
try {
compressionPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.SetCompression");
loginSuccessPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess");
compressionEnabledEvent = Class.forName("com.velocitypowered.proxy.protocol.VelocityConnectionEvent")
.getDeclaredField("COMPRESSION_ENABLED").get(null);
setCompressionMethod = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection")
.getMethod("setCompressionThreshold", int.class);
enabled = true;
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Could not initialize compression disabler!", e);
}
ENABLED = enabled;
COMPRESSION_PACKET_CLASS = compressionPacketClass;
LOGIN_SUCCESS_PACKET_CLASS = loginSuccessPacketClass;
COMPRESSION_ENABLED_EVENT = compressionEnabledEvent;
SET_COMPRESSION_METHOD = setCompressionMethod;
}
public GeyserVelocityCompressionDisabler() {
if (!ENABLED) {
throw new RuntimeException("Geyser compression disabler cannot be initialized in its current state!");
}
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
Class<?> msgClass = msg.getClass();
if (!COMPRESSION_PACKET_CLASS.isAssignableFrom(msgClass)) {
if (LOGIN_SUCCESS_PACKET_CLASS.isAssignableFrom(msgClass)) {
// We're past the point that compression can be enabled
ctx.pipeline().remove(this);
}
super.write(ctx, msg, promise);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt != COMPRESSION_ENABLED_EVENT) {
super.userEventTriggered(ctx, evt);
return;
}
// Invoke the method as it calls a Netty event and handles removing cleaner than we could
Object minecraftConnection = ctx.pipeline().get("handler");
SET_COMPRESSION_METHOD.invoke(minecraftConnection, -1);
// Do not call super and let the new compression enabled event continue firing
}
}

View File

@ -34,6 +34,7 @@ import org.geysermc.geyser.network.netty.GeyserInjector;
import org.geysermc.geyser.network.netty.LocalServerChannelWrapper;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.function.Supplier;
public class GeyserVelocityInjector extends GeyserInjector {
@ -67,9 +68,23 @@ public class GeyserVelocityInjector extends GeyserInjector {
workerGroupField.setAccessible(true);
EventLoopGroup workerGroup = (EventLoopGroup) workerGroupField.get(connectionManager);
// This method is what initializes the connection in Java Edition, after Netty is all set.
Method initChannel = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
initChannel.setAccessible(true);
ChannelFuture channelFuture = (new ServerBootstrap()
.channel(LocalServerChannelWrapper.class)
.childHandler(channelInitializer)
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) throws Exception {
initChannel.invoke(channelInitializer, ch);
if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) {
ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler",
new GeyserVelocityCompressionDisabler());
}
}
})
.group(bossGroup, workerGroup) // Cannot be DefaultEventLoopGroup
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur
.localAddress(LocalAddress.ANY))

View File

@ -30,7 +30,6 @@ import java.net.URISyntaxException;
public final class Constants {
public static final URI GLOBAL_API_WS_URI;
public static final String NTP_SERVER = "time.cloudflare.com";
public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/";
public static final String NEWS_PROJECT_NAME = "geyser";

View File

@ -79,6 +79,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
import org.geysermc.geyser.session.SessionManager;
import org.geysermc.geyser.skin.FloodgateSkinUploader;
import org.geysermc.geyser.skin.ProvidedSkins;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
@ -95,6 +96,7 @@ import java.net.UnknownHostException;
import java.security.Key;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -195,7 +197,23 @@ public class GeyserImpl implements GeyserApi {
EntityDefinitions.init();
ItemTranslator.init();
MessageTranslator.init();
MinecraftLocale.init();
// Download the latest asset list and cache it
AssetUtils.generateAssetCache().whenComplete((aVoid, ex) -> {
if (ex != null) {
return;
}
MinecraftLocale.ensureEN_US();
String locale = GeyserLocale.getDefaultLocale();
if (!"en_us".equals(locale)) {
// English will be loaded after assets are downloaded, if necessary
MinecraftLocale.downloadAndLoadLocale(locale);
}
ProvidedSkins.init();
CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks);
});
startInstance();

View File

@ -182,6 +182,8 @@ public interface GeyserConfiguration {
boolean isUseDirectConnection();
boolean isDisableCompression();
int getConfigVersion();
static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) {

View File

@ -335,6 +335,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("use-direct-connection")
private boolean useDirectConnection = true;
@JsonProperty("disable-compression")
private boolean isDisableCompression = true;
@JsonProperty("config-version")
private int configVersion = 0;

View File

@ -44,8 +44,6 @@ import lombok.Setter;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.EntityUtils;

View File

@ -77,7 +77,6 @@ public class PlayerEntity extends LivingEntity {
}
private String username;
private boolean playerList = true; // Player is in the player list
/**
* The textures property from the GameProfile.
@ -417,4 +416,11 @@ public class PlayerEntity extends LivingEntity {
session.sendUpstreamPacket(packet);
}
}
/**
* @return the UUID that should be used when dealing with Bedrock's tab list.
*/
public UUID getTabListUuid() {
return getUuid();
}
}

View File

@ -250,4 +250,9 @@ public class SessionPlayerEntity extends PlayerEntity {
dirtyMetadata.put(EntityData.PLAYER_HAS_DIED, (byte) 0);
}
}
@Override
public UUID getTabListUuid() {
return session.getAuthData().uuid();
}
}

View File

@ -48,7 +48,6 @@ public class SkullPlayerEntity extends PlayerEntity {
public SkullPlayerEntity(GeyserSession session, long geyserId) {
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
setPlayerList(false);
}
@Override

View File

@ -27,12 +27,15 @@ package org.geysermc.geyser.level;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nullable;
import javax.annotation.Nonnull;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
/**
* Class that manages or retrieves various information
@ -166,4 +169,14 @@ public abstract class WorldManager {
public String[] getBiomeIdentifiers(boolean withTags) {
return null;
}
/**
* Used for pick block, so we don't need to cache more data than necessary.
*
* @return expected NBT for this item.
*/
@Nonnull
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
return CompletableFuture.completedFuture(null);
}
}

View File

@ -62,7 +62,6 @@ import com.github.steveice10.packetlib.event.session.*;
import com.github.steveice10.packetlib.packet.Packet;
import com.github.steveice10.packetlib.tcp.TcpClientSession;
import com.github.steveice10.packetlib.tcp.TcpSession;
import com.nukkitx.math.GenericMath;
import com.nukkitx.math.vector.*;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.protocol.bedrock.BedrockPacket;
@ -135,7 +134,6 @@ import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.geyser.util.LoginEncryptionUtils;
import org.geysermc.geyser.util.MathUtils;
import java.net.ConnectException;
import java.net.InetSocketAddress;
@ -539,6 +537,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private ScheduledFuture<?> lookBackScheduledFuture = null;
/**
* Used to return players back to their vehicles if the server doesn't want them unmounting.
*/
@Setter
private ScheduledFuture<?> mountVehicleScheduledFuture = null;
private MinecraftProtocol protocol;
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
@ -1073,6 +1077,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
closed = true;
}
/**
* Moves task to the session event loop if already not in it. Otherwise, the task is automatically ran.
*/
public void ensureInEventLoop(Runnable runnable) {
if (eventLoop.inEventLoop()) {
runnable.run();
return;
}
executeInEventLoop(runnable);
}
/**
* Executes a task and prints a stack trace if an error occurs.
*/
@ -1378,7 +1393,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
public void setServerRenderDistance(int renderDistance) {
renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle
this.serverRenderDistance = renderDistance;
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();

View File

@ -123,7 +123,8 @@ public class EntityCache {
}
public void addPlayerEntity(PlayerEntity entity) {
playerEntities.put(entity.getUuid(), entity);
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
playerEntities.putIfAbsent(entity.getUuid(), entity);
}
public PlayerEntity getPlayerEntity(UUID uuid) {

View File

@ -29,10 +29,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@ -45,7 +41,6 @@ import org.geysermc.geyser.text.GeyserLocale;
import javax.annotation.Nonnull;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -68,7 +63,7 @@ public class FakeHeadProvider {
SkinProvider.Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry().getGeometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
SkinProvider.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(
@ -111,7 +106,7 @@ public class FakeHeadProvider {
try {
SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity));
sendSkinPacket(session, entity, mergedSkinData);
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);
}
@ -133,50 +128,10 @@ public class FakeHeadProvider {
return;
}
sendSkinPacket(session, entity, skinData);
SkinManager.sendSkinPacket(session, entity, skinData);
});
}
private 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();
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
PlayerListPacket.Entry updatedEntry = SkinManager.buildEntryManually(
session,
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData(),
cape.getCapeId(),
cape.getCapeData(),
geometry
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
} else {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
}
}
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
return SerializedSkin.of(skinId, "", geometry.getGeometryName(),
ImageData.of(skin.getSkinData()), Collections.emptyList(),
ImageData.of(cape.getCapeData()), geometry.getGeometryData(),
"", true, false, false, cape.getCapeId(), skinId);
}
@AllArgsConstructor
@Getter
@Setter

View File

@ -1,63 +0,0 @@
/*
* Copyright (c) 2019-2022 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.skin;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ProvidedSkin {
@Getter private byte[] skin;
public ProvidedSkin(String internalUrl) {
try {
BufferedImage image;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(internalUrl)) {
image = ImageIO.read(stream);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(image.getWidth() * 4 + image.getHeight() * 4);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int rgba = image.getRGB(x, y);
outputStream.write((rgba >> 16) & 0xFF); // Red
outputStream.write((rgba >> 8) & 0xFF); // Green
outputStream.write(rgba & 0xFF); // Blue
outputStream.write((rgba >> 24) & 0xFF); // Alpha
}
}
image.flush();
skin = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (c) 2019-2022 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.skin;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.util.AssetUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Objects;
import java.util.UUID;
public final class ProvidedSkins {
private static final ProvidedSkin[] PROVIDED_SKINS = {
new ProvidedSkin("textures/entity/player/slim/alex.png", true),
new ProvidedSkin("textures/entity/player/slim/ari.png", true),
new ProvidedSkin("textures/entity/player/slim/efe.png", true),
new ProvidedSkin("textures/entity/player/slim/kai.png", true),
new ProvidedSkin("textures/entity/player/slim/makena.png", true),
new ProvidedSkin("textures/entity/player/slim/noor.png", true),
new ProvidedSkin("textures/entity/player/slim/steve.png", true),
new ProvidedSkin("textures/entity/player/slim/sunny.png", true),
new ProvidedSkin("textures/entity/player/slim/zuri.png", true),
new ProvidedSkin("textures/entity/player/wide/alex.png", false),
new ProvidedSkin("textures/entity/player/wide/ari.png", false),
new ProvidedSkin("textures/entity/player/wide/efe.png", false),
new ProvidedSkin("textures/entity/player/wide/kai.png", false),
new ProvidedSkin("textures/entity/player/wide/makena.png", false),
new ProvidedSkin("textures/entity/player/wide/noor.png", false),
new ProvidedSkin("textures/entity/player/wide/steve.png", false),
new ProvidedSkin("textures/entity/player/wide/sunny.png", false),
new ProvidedSkin("textures/entity/player/wide/zuri.png", false)
};
public static ProvidedSkin getDefaultPlayerSkin(UUID uuid) {
return PROVIDED_SKINS[Math.floorMod(uuid.hashCode(), PROVIDED_SKINS.length)];
}
private ProvidedSkins() {
}
public static final class ProvidedSkin {
private SkinProvider.Skin data;
private final boolean slim;
ProvidedSkin(String asset, boolean slim) {
this.slim = slim;
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
.resolve("cache")
.resolve("default_player_skins")
.resolve(slim ? "slim" : "wide");
String assetName = asset.substring(asset.lastIndexOf('/') + 1);
File location = folder.resolve(assetName).toFile();
AssetUtils.addTask(!location.exists(), new AssetUtils.ClientJarTask("assets/minecraft/" + asset,
(stream) -> AssetUtils.saveFile(location, stream),
() -> {
try {
// TODO lazy initialize?
BufferedImage image;
try (InputStream stream = new FileInputStream(location)) {
image = ImageIO.read(stream);
}
byte[] byteData = SkinProvider.bufferedImageToImageData(image);
image.flush();
String identifier = "geysermc:" + assetName + "_" + (slim ? "slim" : "wide");
this.data = new SkinProvider.Skin(-1, identifier, byteData);
} catch (IOException e) {
e.printStackTrace();
}
}));
}
public SkinProvider.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);
}
public boolean isSlim() {
return slim;
}
}
public static void init() {
// no-op
}
static {
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
.resolve("cache")
.resolve("default_player_skins");
folder.toFile().mkdirs();
// Two directories since there are two skins for each model: one slim, one wide
folder.resolve("slim").toFile().mkdir();
folder.resolve("wide").toFile().mkdir();
}
}

View File

@ -32,8 +32,8 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.auth.BedrockClientData;
@ -53,13 +53,30 @@ public class SkinManager {
* Builds a Bedrock player list entry from our existing, cached Bedrock skin information
*/
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.Cape cape = SkinProvider.getCachedCape(data.capeUrl());
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
SkinProvider.Skin skin = null;
SkinProvider.Cape cape = null;
SkinProvider.SkinGeometry geometry = SkinProvider.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;
}
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.skinUrl());
if (skin == null) {
skin = SkinProvider.EMPTY_SKIN;
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);
if (skin == null) {
skin = fallbackSkinData.skin();
geometry = fallbackSkinData.geometry();
}
if (cape == null) {
cape = fallbackSkinData.cape();
}
}
return buildEntryManually(
@ -67,10 +84,8 @@ public class SkinManager {
playerEntity.getUuid(),
playerEntity.getUsername(),
playerEntity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData(),
cape.getCapeId(),
cape.getCapeData(),
skin,
cape,
geometry
);
}
@ -79,14 +94,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,
String skinId, byte[] skinData,
String capeId, byte[] capeData,
SkinProvider.Skin skin,
SkinProvider.Cape cape,
SkinProvider.SkinGeometry geometry) {
SerializedSkin serializedSkin = SerializedSkin.of(
skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(capeData), geometry.getGeometryData(), "", true, false,
!capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
);
SerializedSkin serializedSkin = getSkin(skin.getTextureUrl(), skin, cape, geometry);
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
String xuid = "";
@ -116,6 +127,45 @@ 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();
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
// TODO is this special behavior needed?
PlayerListPacket.Entry updatedEntry = buildEntryManually(
session,
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin,
cape,
geometry
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
} else {
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
}
}
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
return SerializedSkin.of(skinId, "", geometry.geometryName(),
ImageData.of(skin.getSkinData()), 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) -> {
@ -128,34 +178,7 @@ public class SkinManager {
}
if (skinData.geometry() != null) {
SkinProvider.Skin skin = skinData.skin();
SkinProvider.Cape cape = skinData.cape();
SkinProvider.SkinGeometry geometry = skinData.geometry();
PlayerListPacket.Entry updatedEntry = buildEntryManually(
session,
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData(),
cape.getCapeId(),
cape.getCapeData(),
geometry
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
if (!entity.isPlayerList()) {
PlayerListPacket playerRemovePacket = new PlayerListPacket();
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
playerRemovePacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerRemovePacket);
}
sendSkinPacket(session, entity, skinData);
}
if (skinAndCapeConsumer != null) {
@ -186,7 +209,7 @@ public class SkinManager {
}
if (!clientData.getCapeId().equals("")) {
SkinProvider.storeBedrockCape(playerEntity.getUuid(), capeBytes);
SkinProvider.storeBedrockCape(clientData.getCapeId(), capeBytes);
}
} catch (Exception e) {
throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e);
@ -231,26 +254,21 @@ public class SkinManager {
* @param entity entity to build the GameProfileData from
* @return The built GameProfileData
*/
public static GameProfileData from(PlayerEntity entity) {
public static @Nullable GameProfileData from(PlayerEntity entity) {
try {
String texturesProperty = entity.getTexturesProperty();
if (texturesProperty == null) {
// Likely offline mode
return loadBedrockOrOfflineSkin(entity);
}
GameProfileData data = loadFromJson(texturesProperty);
if (data != null) {
return data;
} else {
return loadBedrockOrOfflineSkin(entity);
return null;
}
return loadFromJson(texturesProperty);
} catch (IOException exception) {
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername());
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
exception.printStackTrace();
}
return loadBedrockOrOfflineSkin(entity);
return null;
}
}
@ -279,27 +297,5 @@ public class SkinManager {
return new GameProfileData(skinUrl, capeUrl, isAlex);
}
/**
* @return default skin with default cape when texture data is invalid, or the Bedrock player's skin if this
* is a Bedrock player.
*/
private static GameProfileData loadBedrockOrOfflineSkin(PlayerEntity entity) {
// Fallback to the offline mode of working it out
UUID uuid = entity.getUuid();
boolean isAlex = (Math.abs(uuid.hashCode() % 2) == 1);
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
skinUrl = session.getClientData().getSkinId();
capeUrl = session.getClientData().getCapeId();
}
}
return new GameProfileData(skinUrl, capeUrl, isAlex);
}
}
}

View File

@ -26,22 +26,25 @@
package org.geysermc.geyser.skin;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.bytes.ByteArrays;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.WebUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
@ -57,28 +60,28 @@ import java.util.concurrent.*;
import java.util.function.Predicate;
public class SkinProvider {
public static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
public static final byte[] STEVE_SKIN = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin();
public static final Skin EMPTY_SKIN = new Skin(-1, "steve", STEVE_SKIN);
public static final byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin();
public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN);
private static final Map<String, Skin> permanentSkins = new HashMap<>() {{
put("steve", EMPTY_SKIN);
put("alex", EMPTY_SKIN_ALEX);
}};
private static final Cache<String, Skin> cachedSkins = CacheBuilder.newBuilder()
static final Skin EMPTY_SKIN;
static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, -1, true);
private static final Cache<String, Cape> CACHED_JAVA_CAPES = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Cache<String, Skin> CACHED_JAVA_SKINS = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true);
private static final Cache<String, Cape> cachedCapes = CacheBuilder.newBuilder()
private static final Cache<String, Cape> CACHED_BEDROCK_CAPES = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Cache<String, Skin> CACHED_BEDROCK_SKINS = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
@ -86,18 +89,36 @@ public class SkinProvider {
* Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and
* offline mode players use version 3.
*/
public static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
private static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
public static final String EARS_GEOMETRY;
public static final String EARS_GEOMETRY_SLIM;
public static final SkinGeometry SKULL_GEOMETRY;
public static final SkinGeometry WEARING_CUSTOM_SKULL;
public static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
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;
static {
// Generate the empty texture to use as an emergency fallback
final int pink = -524040;
final int black = -16777216;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(64 * 4 + 64 * 4);
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
int rgba;
if (y > 32) {
rgba = x >= 32 ? pink : black;
} else {
rgba = x >= 32 ? black : pink;
}
outputStream.write((rgba >> 16) & 0xFF); // Red
outputStream.write((rgba >> 8) & 0xFF); // Green
outputStream.write(rgba & 0xFF); // Blue
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);
@ -141,48 +162,104 @@ public class SkinProvider {
}
}
public static boolean hasCapeCached(String capeUrl) {
return cachedCapes.getIfPresent(capeUrl) != null;
/**
* Search our cached database for an already existing, translated skin of this Java URL.
*/
static Skin getCachedSkin(String skinUrl) {
return CACHED_JAVA_SKINS.getIfPresent(skinUrl);
}
public static Skin getCachedSkin(String skinUrl) {
return permanentSkins.getOrDefault(skinUrl, cachedSkins.getIfPresent(skinUrl));
/**
* If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback.
*/
static SkinData determineFallbackSkinData(PlayerEntity entity) {
Skin skin = null;
Cape cape = null;
SkinGeometry geometry = SkinGeometry.WIDE;
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
UUID uuid = entity.getUuid();
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
String skinId = session.getClientData().getSkinId();
skin = CACHED_BEDROCK_SKINS.getIfPresent(skinId);
String capeId = session.getClientData().getCapeId();
cape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
geometry = cachedGeometry.getOrDefault(uuid, geometry);
}
}
if (skin == null) {
// We don't have a skin for the player right now. Fall back to a default.
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(entity.getUuid());
skin = providedSkin.getData();
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
}
if (cape == null) {
cape = EMPTY_CAPE;
}
return new SkinData(skin, cape, geometry);
}
public static Cape getCachedCape(String capeUrl) {
Cape cape = capeUrl != null ? cachedCapes.getIfPresent(capeUrl) : EMPTY_CAPE;
return cape != null ? cape : EMPTY_CAPE;
/**
* Used as a fallback if an official Java cape doesn't exist for this user.
*/
@Nonnull
private static Cape getCachedBedrockCape(UUID uuid) {
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
if (session != null) {
String capeId = session.getClientData().getCapeId();
Cape bedrockCape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
if (bedrockCape != null) {
return bedrockCape;
}
}
return EMPTY_CAPE;
}
public static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
@Nullable
static Cape getCachedCape(String capeUrl) {
if (capeUrl == null) {
return null;
}
return CACHED_JAVA_CAPES.getIfPresent(capeUrl);
}
static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
if (data == null) {
// This player likely does not have a textures property
return CompletableFuture.completedFuture(determineFallbackSkinData(entity));
}
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
.thenApplyAsync(skinAndCape -> {
try {
Skin skin = skinAndCape.getSkin();
Cape cape = skinAndCape.getCape();
SkinGeometry geometry = SkinGeometry.getLegacy(data.isAlex());
Skin skin = skinAndCape.skin();
Cape cape = skinAndCape.cape();
SkinGeometry geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
if (cape.isFailed()) {
cape = getOrDefault(requestBedrockCape(entity.getUuid()),
EMPTY_CAPE, 3);
// Whether we should see if this player has a Bedrock skin we should check for on failure of
// any skin property
boolean checkForBedrock = entity.getUuid().version() != 4;
if (cape.failed() && checkForBedrock) {
cape = getCachedBedrockCape(entity.getUuid());
}
if (cape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
if (cape.failed() && ALLOW_THIRD_PARTY_CAPES) {
cape = getOrDefault(requestUnofficialCape(
cape, entity.getUuid(),
entity.getUsername(), false
), EMPTY_CAPE, CapeProvider.VALUES.length * 3);
}
geometry = getOrDefault(requestBedrockGeometry(
geometry, entity.getUuid()
), geometry, 3);
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername());
// Not a bedrock player check for ears
if (geometry.isFailed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
if (geometry.failed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
boolean isEars;
// Its deadmau5, gotta support his skin :)
@ -213,26 +290,17 @@ public class SkinProvider {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
}
return new SkinData(skinAndCape.getSkin(), skinAndCape.getCape(), null);
return new SkinData(skinAndCape.skin(), skinAndCape.cape(), null);
});
}
public static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
private static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
return CompletableFuture.supplyAsync(() -> {
long time = System.currentTimeMillis();
String newSkinUrl = skinUrl;
if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) {
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(playerId);
if (session != null) {
newSkinUrl = session.getClientData().getSkinId();
}
}
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
SkinAndCape skinAndCape = new SkinAndCape(
getOrDefault(requestSkin(playerId, newSkinUrl, false), EMPTY_SKIN, 5),
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5)
);
@ -241,7 +309,7 @@ public class SkinProvider {
}, EXECUTOR_SERVICE);
}
public static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN);
CompletableFuture<Skin> requestedSkin = requestedSkins.get(textureUrl);
if (requestedSkin != null) {
@ -249,7 +317,7 @@ public class SkinProvider {
return requestedSkin;
}
Skin cachedSkin = getCachedSkin(textureUrl);
Skin cachedSkin = CACHED_JAVA_SKINS.getIfPresent(textureUrl);
if (cachedSkin != null) {
return CompletableFuture.completedFuture(cachedSkin);
}
@ -259,23 +327,26 @@ public class SkinProvider {
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((skin, throwable) -> {
skin.updated = true;
cachedSkins.put(textureUrl, skin);
CACHED_JAVA_SKINS.put(textureUrl, skin);
requestedSkins.remove(textureUrl);
});
requestedSkins.put(textureUrl, future);
} else {
Skin skin = supplySkin(playerId, textureUrl);
future = CompletableFuture.completedFuture(skin);
cachedSkins.put(textureUrl, skin);
CACHED_JAVA_SKINS.put(textureUrl, skin);
}
return future;
}
public static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
private static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl);
if (requestedCape != null) {
return requestedCape;
}
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
Cape cachedCape = CACHED_JAVA_CAPES.getIfPresent(capeUrl);
if (cachedCape != null) {
return CompletableFuture.completedFuture(cachedCape);
}
@ -284,21 +355,21 @@ public class SkinProvider {
if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
.whenCompleteAsync((cape, throwable) -> {
cachedCapes.put(capeUrl, cape);
CACHED_JAVA_CAPES.put(capeUrl, cape);
requestedCapes.remove(capeUrl);
});
requestedCapes.put(capeUrl, future);
} else {
Cape cape = supplyCape(capeUrl, provider); // blocking
future = CompletableFuture.completedFuture(cape);
cachedCapes.put(capeUrl, cape);
CACHED_JAVA_CAPES.put(capeUrl, cape);
}
return future;
}
public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
private static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
String username, boolean newThread) {
if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
if (officialCape.failed() && ALLOW_THIRD_PARTY_CAPES) {
for (CapeProvider provider : CapeProvider.VALUES) {
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
continue;
@ -308,7 +379,7 @@ public class SkinProvider {
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
EMPTY_CAPE, 4
);
if (!cape1.isFailed()) {
if (!cape1.failed()) {
return CompletableFuture.completedFuture(cape1);
}
}
@ -316,7 +387,7 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialCape);
}
public static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
private static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
CompletableFuture<Skin> future;
@ -339,7 +410,7 @@ public class SkinProvider {
* @param newThread Should we start in a new thread
* @return The updated skin with ears
*/
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
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;
@ -357,30 +428,17 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialSkin);
}
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID) {
Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock");
if (bedrockCape == null) {
bedrockCape = EMPTY_CAPE;
}
return CompletableFuture.completedFuture(bedrockCape);
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);
}
public static CompletableFuture<SkinGeometry> requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID) {
SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry);
return CompletableFuture.completedFuture(bedrockGeometry);
static void storeBedrockCape(String capeId, byte[] capeData) {
Cape cape = new Cape(capeId, capeId, capeData, System.currentTimeMillis(), false);
CACHED_BEDROCK_CAPES.put(capeId, cape);
}
public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) {
Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false);
cachedSkins.put(skin.getTextureUrl(), skin);
}
public static void storeBedrockCape(UUID playerID, byte[] capeData) {
Cape cape = new Cape(playerID.toString() + ".Bedrock", playerID.toString(), capeData, System.currentTimeMillis(), false);
cachedCapes.put(playerID.toString() + ".Bedrock", cape);
}
public 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);
cachedGeometry.put(playerID, geometry);
}
@ -391,7 +449,7 @@ public class SkinProvider {
* @param skin The skin to cache
*/
public static void storeEarSkin(Skin skin) {
cachedSkins.put(skin.getTextureUrl(), skin);
CACHED_JAVA_SKINS.put(skin.getTextureUrl(), skin);
}
/**
@ -400,7 +458,7 @@ public class SkinProvider {
* @param playerID The UUID to cache it against
* @param isSlim If the player is using an slim base
*/
public static void storeEarGeometry(UUID playerID, boolean isSlim) {
private static void storeEarGeometry(UUID playerID, boolean isSlim) {
cachedGeometry.put(playerID, SkinGeometry.getEars(isSlim));
}
@ -414,7 +472,7 @@ public class SkinProvider {
}
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = EMPTY_CAPE.getCapeData();
byte[] cape = EMPTY_CAPE.capeData();
try {
cape = requestImage(capeUrl, provider);
} catch (Exception ignored) {
@ -604,7 +662,7 @@ public class SkinProvider {
}
private static BufferedImage readFiveZigCape(String url) throws IOException {
JsonNode element = OBJECT_MAPPER.readTree(WebUtils.getBody(url));
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;
@ -683,13 +741,12 @@ public class SkinProvider {
return defaultValue;
}
@AllArgsConstructor
@Getter
public static class SkinAndCape {
private final Skin skin;
private final Cape cape;
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) {
}
@ -703,29 +760,19 @@ public class SkinProvider {
private boolean updated;
private boolean ears;
private Skin(long requestedOn, String textureUrl, byte[] skinData) {
Skin(long requestedOn, String textureUrl, byte[] skinData) {
this.requestedOn = requestedOn;
this.textureUrl = textureUrl;
this.skinData = skinData;
}
}
@AllArgsConstructor
@Getter
public static class Cape {
private final String textureUrl;
private final String capeId;
private final byte[] capeData;
private final long requestedOn;
private final boolean failed;
public record Cape(String textureUrl, String capeId, byte[] capeData, long requestedOn, boolean failed) {
}
@AllArgsConstructor
@Getter
public static class SkinGeometry {
private final String geometryName;
private final String geometryData;
private final 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
@ -733,7 +780,7 @@ public class SkinProvider {
* @param isSlim Should it be the alex model
* @return The generic geometry object
*/
public static SkinGeometry getLegacy(boolean isSlim) {
private static SkinGeometry getLegacy(boolean isSlim) {
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "", true);
}
@ -743,7 +790,7 @@ public class SkinProvider {
* @param isSlim Should it be the alex model
* @return The generated geometry for the ears model
*/
public static SkinGeometry getEars(boolean isSlim) {
private static SkinGeometry getEars(boolean isSlim) {
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.ears" + (isSlim ? "Slim" : "") + "\"}}", (isSlim ? EARS_GEOMETRY_SLIM : EARS_GEOMETRY), false);
}
}

View File

@ -42,9 +42,9 @@ public class SkullSkinManager extends SkinManager {
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
skinId = skinId + "_skull";
return SerializedSkin.of(
skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
skinId, "", SkinProvider.SKULL_GEOMETRY.geometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.capeData()), SkinProvider.SKULL_GEOMETRY.geometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.capeId(), skinId
);
}

View File

@ -25,91 +25,45 @@
package org.geysermc.geyser.text;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.util.AssetUtils;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.WebUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
public class MinecraftLocale {
public static final Map<String, Map<String, String>> LOCALE_MAPPINGS = new HashMap<>();
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
private static VersionDownload clientJarInfo;
static {
// Create the locales folder
File localesFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales").toFile();
//noinspection ResultOfMethodCallIgnored
localesFolder.mkdir();
// Download the latest asset list and cache it
generateAssetCache().whenComplete((aVoid, ex) -> downloadAndLoadLocale(GeyserLocale.getDefaultLocale()));
// FIXME TEMPORARY
try {
Files.delete(localesFolder.toPath().resolve("en_us.hash"));
} catch (IOException ignored) {
}
}
/**
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
*/
private static CompletableFuture<Void> generateAssetCache() {
return CompletableFuture.supplyAsync(() -> {
try {
// Get the version manifest from Mojang
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
// Get the url for the latest version of the games manifest
String latestInfoURL = "";
for (Version version : versionManifest.getVersions()) {
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
latestInfoURL = version.getUrl();
break;
public static void ensureEN_US() {
File localeFile = getFile("en_us");
AssetUtils.addTask(!localeFile.exists(), new AssetUtils.ClientJarTask("assets/minecraft/lang/en_us.json",
(stream) -> AssetUtils.saveFile(localeFile, stream),
() -> {
if ("en_us".equals(GeyserLocale.getDefaultLocale())) {
loadLocale("en_us");
}
}
// Make sure we definitely got a version
if (latestInfoURL.isEmpty()) {
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
clientJarInfo = versionInfo.getDownloads().get("client");
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(clientJarInfo));
// Get the assets list
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
if (!entry.getKey().startsWith("minecraft/lang/")) {
// No need to cache non-language assets as we don't use them
continue;
}
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
}
return null;
});
}));
}
/**
@ -125,7 +79,7 @@ public class MinecraftLocale {
}
// Check the locale isn't already loaded
if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
if (!AssetUtils.isAssetKnown("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
if (loadLocale(locale)) {
GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
} else {
@ -148,33 +102,15 @@ public class MinecraftLocale {
* @param locale Locale to download
*/
private static void downloadLocale(String locale) {
File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
if (locale.equals("en_us")) {
return;
}
File localeFile = getFile(locale);
// Check if we have already downloaded the locale file
if (localeFile.exists()) {
String curHash = "";
String targetHash;
if (locale.equals("en_us")) {
try {
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toFile();
if (hashFile.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
curHash = br.readLine().trim();
}
}
} catch (IOException ignored) { }
if (clientJarInfo == null) {
// Likely failed to download
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
return;
}
targetHash = clientJarInfo.getSha1();
} else {
curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
targetHash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
}
String curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
String targetHash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
if (!curHash.equals(targetHash)) {
GeyserImpl.getInstance().getLogger().debug("Locale out of date; re-downloading: " + locale);
@ -184,22 +120,19 @@ public class MinecraftLocale {
}
}
// Create the en_us locale
if (locale.equals("en_us")) {
downloadEN_US(localeFile);
return;
}
try {
// Get the hash and download the locale
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
String hash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Unable to download locale file hash", e);
}
}
private static File getFile(String locale) {
return GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
}
/**
* Loads a locale already downloaded, if the file doesn't exist it just logs a warning
*
@ -254,51 +187,6 @@ public class MinecraftLocale {
}
}
/**
* Download then en_us locale by downloading the server jar and extracting it from there.
*
* @param localeFile File to save the locale to
*/
private static void downloadEN_US(File localeFile) {
try {
// Let the user know we are downloading the JAR
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
GeyserImpl.getInstance().getLogger().debug("Download URL: " + clientJarInfo.getUrl());
// Download the smallest JAR (client or server)
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString());
// Load in the JAR as a zip and extract the file
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) {
try (FileOutputStream outStream = new FileOutputStream(localeFile)) {
// Write the file to the locale dir
byte[] buf = new byte[fileStream.available()];
int length;
while ((length = fileStream.read(buf)) != -1) {
outStream.write(buf, 0, length);
}
// Flush all changes to disk and cleanup
outStream.flush();
}
}
}
// Store the latest jar hash
FileUtils.writeFile(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray());
// Delete the nolonger needed client/server jar
Files.delete(tmpFilePath);
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
}
}
/**
* Translate the given language string into the given locale, or falls back to the default locale
*
@ -333,111 +221,4 @@ public class MinecraftLocale {
}
return result.toString();
}
public static void init() {
// no-op
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionManifest {
@JsonProperty("latest")
private LatestVersion latestVersion;
@JsonProperty("versions")
private List<Version> versions;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class LatestVersion {
@JsonProperty("release")
private String release;
@JsonProperty("snapshot")
private String snapshot;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Version {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("url")
private String url;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionInfo {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
@JsonProperty("assetIndex")
private AssetIndex assetIndex;
@JsonProperty("downloads")
private Map<String, VersionDownload> downloads;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionDownload {
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class AssetIndex {
@JsonProperty("id")
private String id;
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("totalSize")
private int totalSize;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Asset {
@JsonProperty("hash")
private String hash;
@JsonProperty("size")
private int size;
}
}

View File

@ -56,17 +56,17 @@ public class BannerTranslator extends NbtItemStackTranslator {
static {
OMINOUS_BANNER_PATTERN = new ListTag("Patterns");
// Construct what an ominous banner is supposed to look like
OMINOUS_BANNER_PATTERN.add(getPatternTag("mr", 9));
OMINOUS_BANNER_PATTERN.add(getPatternTag("bs", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("cs", 7));
OMINOUS_BANNER_PATTERN.add(getPatternTag("bo", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("ms", 15));
OMINOUS_BANNER_PATTERN.add(getPatternTag("hh", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("mc", 8));
OMINOUS_BANNER_PATTERN.add(getPatternTag("bo", 15));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("mr", 9));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bs", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("cs", 7));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("ms", 15));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("hh", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("mc", 8));
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 15));
}
private static CompoundTag getPatternTag(String pattern, int color) {
public static CompoundTag getJavaPatternTag(String pattern, int color) {
StringTag patternType = new StringTag("Pattern", pattern);
IntTag colorTag = new IntTag("Color", color);
CompoundTag tag = new CompoundTag("");
@ -117,11 +117,7 @@ public class BannerTranslator extends NbtItemStackTranslator {
* @return The Java edition format pattern nbt
*/
public static CompoundTag getJavaBannerPattern(NbtMap pattern) {
Map<String, Tag> tags = new HashMap<>();
tags.put("Color", new IntTag("Color", 15 - pattern.getInt("Color")));
tags.put("Pattern", new StringTag("Pattern", pattern.getString("Pattern")));
return new CompoundTag("", tags);
return BannerTranslator.getJavaPatternTag(pattern.getString("Pattern"), 15 - pattern.getInt("Color"));
}
/**

View File

@ -25,12 +25,18 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.packet.BlockPickRequestPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -61,6 +67,41 @@ public class BedrockBlockPickRequestTranslator extends PacketTranslator<BlockPic
return;
}
InventoryUtils.findOrCreateItem(session, BlockRegistries.JAVA_BLOCKS.get(blockToPick).getPickItem());
BlockMapping blockMapping = BlockRegistries.JAVA_BLOCKS.getOrDefault(blockToPick, BlockMapping.AIR);
boolean addNbtData = packet.isAddUserData() && blockMapping.isBlockEntity(); // Holding down CTRL
if (BlockStateValues.getBannerColor(blockToPick) != -1 || addNbtData) {
session.getGeyser().getWorldManager().getPickItemNbt(session, vector.getX(), vector.getY(), vector.getZ(), addNbtData)
.whenComplete((tag, ex) -> {
if (tag == null) {
pickItem(session, blockMapping);
return;
}
session.ensureInEventLoop(() -> {
if (addNbtData) {
ListTag lore = new ListTag("Lore");
lore.add(new StringTag("", "\"(+NBT)\""));
CompoundTag display = tag.get("display");
if (display == null) {
display = new CompoundTag("display");
tag.put(display);
}
display.put(lore);
}
// I don't really like this... I'd rather get an ID from the block mapping I think
ItemMapping mapping = session.getItemMappings().getMapping(blockMapping.getPickItem());
ItemStack itemStack = new ItemStack(mapping.getJavaId(), 1, tag);
InventoryUtils.findOrCreateItem(session, itemStack);
});
});
return;
}
pickItem(session, blockMapping);
}
private void pickItem(GeyserSession session, BlockMapping blockToPick) {
InventoryUtils.findOrCreateItem(session, blockToPick.getPickItem());
}
}

View File

@ -25,7 +25,6 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.nukkitx.protocol.bedrock.packet.EntityPickRequestPacket;
import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.entity.type.Entity;
@ -45,7 +44,10 @@ public class BedrockEntityPickRequestTranslator extends PacketTranslator<EntityP
@Override
public void translate(GeyserSession session, EntityPickRequestPacket packet) {
if (session.getGameMode() != GameMode.CREATIVE) return; // Apparently Java behavior
if (!session.isInstabuild()) {
// As of Java Edition 1.19.3
return;
}
Entity entity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
if (entity == null) return;

View File

@ -32,15 +32,19 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
import com.nukkitx.protocol.bedrock.packet.InteractPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.concurrent.TimeUnit;
@Translator(packet = InteractPacket.class)
public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> {
@ -73,6 +77,23 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
case LEAVE_VEHICLE:
ServerboundPlayerCommandPacket sneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
session.sendDownstreamPacket(sneakPacket);
Entity currentVehicle = session.getPlayerEntity().getVehicle();
session.setMountVehicleScheduledFuture(session.scheduleInEventLoop(() -> {
if (session.getPlayerEntity().getVehicle() == null) {
return;
}
long vehicleBedrockId = currentVehicle.getGeyserId();
if (session.getPlayerEntity().getVehicle().getGeyserId() == vehicleBedrockId) {
// The Bedrock client, as of 1.19.51, dismounts on its end. The server may not agree with this.
// If the server doesn't agree with our dismount (sends a packet saying we dismounted),
// then remount the player.
SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
linkPacket.setEntityLink(new EntityLinkData(vehicleBedrockId, session.getPlayerEntity().getGeyserId(), EntityLinkData.Type.PASSENGER, true, false));
session.sendUpstreamPacket(linkPacket);
}
}, 1, TimeUnit.SECONDS));
break;
case MOUSEOVER:
// Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc

View File

@ -45,17 +45,15 @@ public class JavaPlayerInfoRemoveTranslator extends PacketTranslator<Clientbound
for (UUID id : packet.getProfileIds()) {
// As the player entity is no longer present, we can remove the entry
PlayerEntity entity = session.getEntityCache().removePlayerEntity(id);
UUID removeId;
if (entity != null) {
// Just remove the entity's player list status
// Don't despawn the entity - the Java server will also take care of that.
entity.setPlayerList(false);
}
if (entity == session.getPlayerEntity()) {
// If removing ourself we use our AuthData UUID
translate.getEntries().add(new PlayerListPacket.Entry(session.getAuthData().uuid()));
removeId = entity.getTabListUuid();
} else {
translate.getEntries().add(new PlayerListPacket.Entry(id));
removeId = id;
}
translate.getEntries().add(new PlayerListPacket.Entry(removeId));
}
session.sendUpstreamPacket(translate);

View File

@ -38,70 +38,87 @@ import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Translator(packet = ClientboundPlayerInfoUpdatePacket.class)
public class JavaPlayerInfoUpdateTranslator extends PacketTranslator<ClientboundPlayerInfoUpdatePacket> {
@Override
public void translate(GeyserSession session, ClientboundPlayerInfoUpdatePacket packet) {
if (!packet.getActions().contains(PlayerListEntryAction.ADD_PLAYER)) {
return;
}
Set<PlayerListEntryAction> actions = packet.getActions();
PlayerListPacket translate = new PlayerListPacket();
translate.setAction(PlayerListPacket.Action.ADD);
if (actions.contains(PlayerListEntryAction.ADD_PLAYER)) {
for (PlayerListEntry entry : packet.getEntries()) {
GameProfile profile = entry.getProfile();
PlayerEntity playerEntity;
boolean self = profile.getId().equals(session.getPlayerEntity().getUuid());
for (PlayerListEntry entry : packet.getEntries()) {
GameProfile profile = entry.getProfile();
PlayerEntity playerEntity;
boolean self = profile.getId().equals(session.getPlayerEntity().getUuid());
GameProfile.Property textures = profile.getProperty("textures");
String texturesProperty = textures == null ? null : textures.getValue();
if (self) {
// Entity is ourself
playerEntity = session.getPlayerEntity();
} else {
playerEntity = session.getEntityCache().getPlayerEntity(profile.getId());
}
if (self) {
// Entity is ourself
playerEntity = session.getPlayerEntity();
} else {
// It's a new player
playerEntity = new PlayerEntity(
session,
-1,
session.getEntityCache().getNextEntityId().incrementAndGet(),
profile.getId(),
Vector3f.ZERO,
Vector3f.ZERO,
0, 0, 0,
profile.getName(),
texturesProperty
);
GameProfile.Property textures = profile.getProperty("textures");
String texturesProperty = textures == null ? null : textures.getValue();
if (playerEntity == null) {
// It's a new player
playerEntity = new PlayerEntity(
session,
-1,
session.getEntityCache().getNextEntityId().incrementAndGet(),
profile.getId(),
Vector3f.ZERO,
Vector3f.ZERO,
0, 0, 0,
profile.getName(),
texturesProperty
);
session.getEntityCache().addPlayerEntity(playerEntity);
} else {
session.getEntityCache().addPlayerEntity(playerEntity);
}
playerEntity.setUsername(profile.getName());
playerEntity.setTexturesProperty(texturesProperty);
}
playerEntity.setPlayerList(true);
// We'll send our own PlayerListEntry in requestAndHandleSkinAndCape
// But we need to send other player's entries so they show up in the player list
// without processing their skin information - that'll be processed when they spawn in
if (self) {
SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername()));
} else {
playerEntity.setValid(true);
PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, playerEntity);
translate.getEntries().add(playerListEntry);
if (self) {
SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername()));
} else {
playerEntity.setValid(true);
}
}
}
if (!translate.getEntries().isEmpty()) {
session.sendUpstreamPacket(translate);
if (actions.contains(PlayerListEntryAction.UPDATE_LISTED)) {
List<PlayerListPacket.Entry> toAdd = new ArrayList<>();
List<PlayerListPacket.Entry> toRemove = new ArrayList<>();
for (PlayerListEntry entry : packet.getEntries()) {
PlayerEntity entity = session.getEntityCache().getPlayerEntity(entry.getProfileId());
if (entity == null) {
session.getGeyser().getLogger().debug("Ignoring player info update for " + entry.getProfileId());
continue;
}
if (entry.isListed()) {
PlayerListPacket.Entry playerListEntry = SkinManager.buildCachedEntry(session, entity);
toAdd.add(playerListEntry);
} else {
toRemove.add(new PlayerListPacket.Entry(entity.getTabListUuid()));
}
}
if (!toAdd.isEmpty()) {
PlayerListPacket tabListPacket = new PlayerListPacket();
tabListPacket.setAction(PlayerListPacket.Action.ADD);
tabListPacket.getEntries().addAll(toAdd);
session.sendUpstreamPacket(tabListPacket);
}
if (!toRemove.isEmpty()) {
PlayerListPacket tabListPacket = new PlayerListPacket();
tabListPacket.setAction(PlayerListPacket.Action.REMOVE);
tabListPacket.getEntries().addAll(toRemove);
session.sendUpstreamPacket(tabListPacket);
}
}
}
}

View File

@ -84,7 +84,7 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
acceptTeleport(session, packet.getX(), packet.getY(), packet.getZ(), packet.getYaw(), packet.getPitch(), packet.getTeleportId());
if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
if (session.getServerRenderDistance() > 32 && !session.isEmulatePost1_13Logic()) {
// See DimensionUtils for an explanation
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(session.getServerRenderDistance());
@ -113,6 +113,13 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
EntityUtils.updateRiderRotationLock(entity, null, false);
EntityUtils.updateMountOffset(entity, null, false, false, entity.getPassengers().size() > 1);
entity.updateBedrockMetadata();
if (session.getMountVehicleScheduledFuture() != null) {
// Cancel this task as it is now unnecessary.
// Note that this isn't present in JavaSetPassengersTranslator as that code is not called for players
// as of Java 1.19.3, but the scheduled future checks for the vehicle being null anyway.
session.getMountVehicleScheduledFuture().cancel(false);
}
}
// If coordinates are relative, then add to the existing coordinate

View File

@ -0,0 +1,329 @@
/*
* Copyright (c) 2019-2022 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.util;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.text.GeyserLocale;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile;
/**
* Implementation note: try to design processes to fail softly if the client jar can't be downloaded,
* either if Mojang is down or internet access to Mojang is spotty.
*/
public final class AssetUtils {
private static final String CLIENT_JAR_HASH_FILE = "client_jar.hash";
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
private static VersionDownload CLIENT_JAR_INFO;
private static final Queue<ClientJarTask> CLIENT_JAR_TASKS = new ArrayDeque<>();
/**
* Download the client jar even if the hash is correct
*/
private static boolean FORCE_DOWNLOAD_JAR = false;
public static Asset getAsset(String name) {
return ASSET_MAP.get(name);
}
public static boolean isAssetKnown(String name) {
return ASSET_MAP.containsKey(name);
}
/**
* Add task to be ran after the client jar is downloaded or found to be cached.
*
* @param required if set to true, the client jar will always be downloaded, even if a pre-existing hash is matched.
* This means an asset or texture is missing.
*/
public static void addTask(boolean required, ClientJarTask task) {
CLIENT_JAR_TASKS.add(task);
FORCE_DOWNLOAD_JAR |= required;
}
/**
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
*/
public static CompletableFuture<Void> generateAssetCache() {
return CompletableFuture.supplyAsync(() -> {
try {
// Get the version manifest from Mojang
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(
WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
// Get the url for the latest version of the games manifest
String latestInfoURL = "";
for (Version version : versionManifest.getVersions()) {
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
latestInfoURL = version.getUrl();
break;
}
}
// Make sure we definitely got a version
if (latestInfoURL.isEmpty()) {
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
CLIENT_JAR_INFO = versionInfo.getDownloads().get("client");
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(CLIENT_JAR_INFO));
// Get the assets list
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
if (!entry.getKey().startsWith("minecraft/lang/")) {
// No need to cache non-language assets as we don't use them
continue;
}
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
}
return null;
});
}
public static void downloadAndRunClientJarTasks() {
if (CLIENT_JAR_INFO == null) {
// Likely failed to download
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
return;
}
if (!FORCE_DOWNLOAD_JAR) { // Don't bother checking the hash if we need to download new files anyway.
String curHash = null;
try {
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve(CLIENT_JAR_HASH_FILE).toFile();
if (hashFile.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
curHash = br.readLine().trim();
}
}
} catch (IOException ignored) { }
String targetHash = CLIENT_JAR_INFO.getSha1();
if (targetHash.equals(curHash)) {
// Just run all tasks - no new download required
ClientJarTask task;
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
task.whenDone.run();
}
return;
}
}
try {
// Let the user know we are downloading the JAR
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
GeyserImpl.getInstance().getLogger().debug("Download URL: " + CLIENT_JAR_INFO.getUrl());
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
WebUtils.downloadFile(CLIENT_JAR_INFO.getUrl(), tmpFilePath.toString());
// Load in the JAR as a zip and extract the files
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
ClientJarTask task;
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry(task.asset))) {
task.ifNewDownload.accept(fileStream);
task.whenDone.run();
}
}
}
// Store the latest jar hash
Path cache = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
Files.createDirectories(cache);
FileUtils.writeFile(cache.resolve(CLIENT_JAR_HASH_FILE).toString(), CLIENT_JAR_INFO.getSha1().toCharArray());
// Delete the nolonger needed client/server jar
Files.delete(tmpFilePath);
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
}
}
public static void saveFile(File location, InputStream fileStream) throws IOException {
try (FileOutputStream outStream = new FileOutputStream(location)) {
// Write the file to the locale dir
byte[] buf = new byte[fileStream.available()];
int length;
while ((length = fileStream.read(buf)) != -1) {
outStream.write(buf, 0, length);
}
// Flush all changes to disk and cleanup
outStream.flush();
}
}
/**
* A process that requires we download the client jar.
* Designed to accommodate Geyser updates that require more assets from the jar.
*/
public record ClientJarTask(String asset, InputStreamConsumer ifNewDownload, Runnable whenDone) {
}
@FunctionalInterface
public interface InputStreamConsumer {
void accept(InputStream stream) throws IOException;
}
/* Classes that map to JSON files served by Mojang */
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionManifest {
@JsonProperty("latest")
private LatestVersion latestVersion;
@JsonProperty("versions")
private List<Version> versions;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class LatestVersion {
@JsonProperty("release")
private String release;
@JsonProperty("snapshot")
private String snapshot;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class Version {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("url")
private String url;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionInfo {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("time")
private String time;
@JsonProperty("releaseTime")
private String releaseTime;
@JsonProperty("assetIndex")
private AssetIndex assetIndex;
@JsonProperty("downloads")
private Map<String, VersionDownload> downloads;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class VersionDownload {
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
static class AssetIndex {
@JsonProperty("id")
private String id;
@JsonProperty("sha1")
private String sha1;
@JsonProperty("size")
private int size;
@JsonProperty("totalSize")
private int totalSize;
@JsonProperty("url")
private String url;
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public static class Asset {
@JsonProperty("hash")
private String hash;
@JsonProperty("size")
private int size;
}
private AssetUtils() {
}
}

View File

@ -75,18 +75,17 @@ public class DimensionUtils {
session.getPistonCache().clear();
session.getSkullCache().clear();
if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
if (session.getServerRenderDistance() > 32 && !session.isEmulatePost1_13Logic()) {
// The server-sided view distance wasn't a thing until Minecraft Java 1.14
// So ViaVersion compensates by sending a "view distance" of 64
// That's fine, except when the actual view distance sent from the server is five chunks
// The client locks up when switching dimensions, expecting more chunks than it's getting
// To solve this, we cap at 32 unless we know that the render distance actually exceeds 32
// 47 is the Bedrock equivalent of 32
// Also, as of 1.19: PS4 crashes with a ChunkRadiusUpdatedPacket too large
session.getGeyser().getLogger().debug("Applying dimension switching workaround for Bedrock render distance of "
+ session.getServerRenderDistance());
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(47);
chunkRadiusUpdatedPacket.setRadius(32);
session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
// Will be re-adjusted on spawn
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -111,7 +111,7 @@ debug-mode: false
# Allow third party capes to be visible. Currently allowing:
# OptiFine capes, LabyMod capes, 5Zig capes and MinecraftCapes
allow-third-party-capes: true
allow-third-party-capes: false
# Allow third party deadmau5 ears to be visible. Currently allowing:
# MinecraftCapes
@ -219,4 +219,9 @@ mtu: 1400
# If disabled on plugin versions, expect performance decrease and latency increase
use-direct-connection: true
# Whether Geyser should attempt to disable compression for Bedrock players. This should be a benefit as there is no need to compress data
# when Java packets aren't being handled over the network.
# This requires use-direct-connection to be true.
disable-compression: true
config-version: 4