diff --git a/.gitignore b/.gitignore index 88b8bc73..c4c878af 100644 --- a/.gitignore +++ b/.gitignore @@ -241,3 +241,4 @@ config.yml logs/ public-key.pem locales/ +cache/ \ No newline at end of file diff --git a/README.md b/README.md index 0d474d99..1f456225 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set - [ ] Beacon - [ ] Cartography Table - [ ] Stonecutter + - [ ] Command Block + - [ ] Structure Block + - [ ] Horse Inventory - Some Entity Flags ## Compiling diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java index 55cd7116..c43d9eab 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java @@ -25,17 +25,19 @@ package org.geysermc.platform.spigot.world; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import lombok.AllArgsConstructor; import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.WorldManager; +import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.utils.GameRule; import us.myles.ViaVersion.protocols.protocol1_13_1to1_13.Protocol1_13_1To1_13; import us.myles.ViaVersion.protocols.protocol1_16_2to1_16_1.data.MappingData; @AllArgsConstructor -public class GeyserSpigotWorldManager extends WorldManager { +public class GeyserSpigotWorldManager extends GeyserWorldManager { private final boolean isLegacy; // You need ViaVersion to connect to an older server with Geyser. @@ -70,4 +72,19 @@ public class GeyserSpigotWorldManager extends WorldManager { return BlockTranslator.AIR; } } + + @Override + public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { + return Boolean.parseBoolean(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID())); + } + + @Override + public int getGameRuleInt(GeyserSession session, GameRule gameRule) { + return Integer.parseInt(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID())); + } + + @Override + public boolean hasPermission(GeyserSession session, String permission) { + return Bukkit.getPlayer(session.getPlayerEntity().getUsername()).hasPermission(permission); + } } diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java index 279aca54..734fcca6 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java @@ -149,6 +149,11 @@ public class GeyserSpongeConfiguration implements GeyserConfiguration { return node.getNode("cache-chunks").getBoolean(false); } + @Override + public int getCacheImages() { + return node.getNode("cache-skins").getInt(0); + } + @Override public boolean isAboveBedrockNetherBuilding() { return node.getNode("above-bedrock-nether-building").getBoolean(false); diff --git a/connector/pom.xml b/connector/pom.xml index ff6101b3..3d5c098a 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -96,6 +96,12 @@ 8.3.1 compile + + com.nukkitx.fastutil + fastutil-object-object-maps + 8.3.1 + compile + com.google.guava guava diff --git a/connector/src/main/java/org/geysermc/connector/GeyserLogger.java b/connector/src/main/java/org/geysermc/connector/GeyserLogger.java index 2ea45a49..0ba2a689 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserLogger.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserLogger.java @@ -36,6 +36,9 @@ public interface GeyserLogger { /** * Logs a severe message and an exception to console + * + * @param message the message to log + * @param error the error to throw */ void severe(String message, Throwable error); @@ -48,6 +51,9 @@ public interface GeyserLogger { /** * Logs an error message and an exception to console + * + * @param message the message to log + * @param error the error to throw */ void error(String message, Throwable error); diff --git a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java index eb8bf967..b6a766a3 100644 --- a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java +++ b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java @@ -30,14 +30,14 @@ import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.command.CommandManager; -import org.geysermc.connector.network.translators.world.CachedChunkManager; +import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.WorldManager; import java.nio.file.Path; public interface GeyserBootstrap { - CachedChunkManager DEFAULT_CHUNK_MANAGER = new CachedChunkManager(); + GeyserWorldManager DEFAULT_CHUNK_MANAGER = new GeyserWorldManager(); /** * Called when the GeyserBootstrap is enabled diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index 94322db9..4d9933ff 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -77,6 +77,8 @@ public interface GeyserConfiguration { boolean isCacheChunks(); + int getCacheImages(); + IMetricsInfo getMetrics(); interface IBedrockConfiguration { diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index d6eaf934..3873db3c 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -87,6 +87,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("cache-chunks") private boolean cacheChunks; + @JsonProperty("cache-images") + private int cacheImages = 0; + @JsonProperty("above-bedrock-nether-building") private boolean aboveBedrockNetherBuilding; diff --git a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java index fe4dc905..52b27351 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java @@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadat import com.github.steveice10.mc.protocol.data.message.TextMessage; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.AdventureSetting; import com.nukkitx.protocol.bedrock.data.AttributeData; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.command.CommandPermission; @@ -38,7 +39,6 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; import com.nukkitx.protocol.bedrock.packet.*; import lombok.Getter; import lombok.Setter; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.attribute.Attribute; import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.entity.type.EntityType; @@ -47,12 +47,8 @@ import org.geysermc.connector.network.session.cache.EntityEffectCache; import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.utils.AttributeUtils; import org.geysermc.connector.utils.MessageUtils; -import org.geysermc.connector.utils.SkinUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.TimeUnit; @Getter @Setter @@ -61,7 +57,7 @@ public class PlayerEntity extends LivingEntity { private UUID uuid; private String username; private long lastSkinUpdate = -1; - private boolean playerList = true; + private boolean playerList = true; // Player is in the player list private final EntityEffectCache effectCache; private Entity leftParrot; @@ -97,7 +93,7 @@ public class PlayerEntity extends LivingEntity { addPlayerPacket.setMotion(motion); addPlayerPacket.setHand(hand); addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.NORMAL); - addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.VISITOR); + addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); addPlayerPacket.setDeviceId(""); addPlayerPacket.setPlatformChatId(""); addPlayerPacket.getMetadata().putAll(metadata); @@ -117,30 +113,12 @@ public class PlayerEntity extends LivingEntity { public void sendPlayer(GeyserSession session) { if(session.getEntityCache().getPlayerEntity(uuid) == null) return; - if (getLastSkinUpdate() == -1) { - if (playerList) { - PlayerListPacket playerList = new PlayerListPacket(); - playerList.setAction(PlayerListPacket.Action.ADD); - playerList.getEntries().add(SkinUtils.buildDefaultEntry(profile, geyserId)); - session.sendUpstreamPacket(playerList); - } - } if (session.getUpstream().isInitialized() && session.getEntityCache().getEntityByGeyserId(geyserId) == null) { session.getEntityCache().spawnEntity(this); } else { spawnEntity(session); } - - if (!playerList) { - // remove from playerlist if player isn't on playerlist - GeyserConnector.getInstance().getGeneralThreadPool().execute(() -> { - PlayerListPacket playerList = new PlayerListPacket(); - playerList.setAction(PlayerListPacket.Action.REMOVE); - playerList.getEntries().add(new PlayerListPacket.Entry(uuid)); - session.sendUpstreamPacket(playerList); - }); - } } @Override @@ -232,7 +210,7 @@ public class PlayerEntity extends LivingEntity { if (entityMetadata.getId() == 2) { // System.out.println(session.getScoreboardCache().getScoreboard().getObjectives().keySet()); - for (Team team : session.getScoreboardCache().getScoreboard().getTeams().values()) { + for (Team team : session.getWorldCache().getScoreboard().getTeams().values()) { // session.getConnector().getLogger().info("team name " + team.getName()); // session.getConnector().getLogger().info("team entities " + team.getEntities()); } @@ -241,7 +219,7 @@ public class PlayerEntity extends LivingEntity { if (name != null) { username = MessageUtils.getBedrockMessage(name); } - Team team = session.getScoreboardCache().getScoreboard().getTeamFor(username); + Team team = session.getWorldCache().getScoreboard().getTeamFor(username); if (team != null) { // session.getConnector().getLogger().info("team name es " + team.getName() + " with prefix " + team.getPrefix() + " and suffix " + team.getSuffix()); metadata.put(EntityData.NAMETAG, team.getPrefix() + MessageUtils.toChatColor(team.getColor()) + username + team.getSuffix()); diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java index 8728547f..a0bd5bc2 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java @@ -53,14 +53,21 @@ public class ShulkerEntity extends GolemEntity { metadata.put(EntityData.SHULKER_ATTACH_POS, Vector3i.from(position.getX(), position.getY(), position.getZ())); } } - //TODO Outdated metadata flag SHULKER_PEAK_HEIGHT -// if (entityMetadata.getId() == 17) { -// int height = (byte) entityMetadata.getValue(); -// metadata.put(EntityData.SHULKER_PEAK_HEIGHT, height); -// } + + if (entityMetadata.getId() == 17) { + int height = (byte) entityMetadata.getValue(); + metadata.put(EntityData.SHULKER_PEEK_ID, height); + } + if (entityMetadata.getId() == 18) { - int color = Math.abs((byte) entityMetadata.getValue() - 15); - metadata.put(EntityData.VARIANT, color); + byte color = (byte) entityMetadata.getValue(); + if (color == 16) { + // 16 is default on both editions + metadata.put(EntityData.VARIANT, 16); + } else { + // Every other shulker color is offset 15 in bedrock edition + metadata.put(EntityData.VARIANT, Math.abs(color - 15)); + } } super.updateBedrockMetadata(entityMetadata, session); } diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index dd9f48d6..357e870f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -34,6 +34,7 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; import org.geysermc.connector.utils.LoginEncryptionUtils; import org.geysermc.connector.utils.LanguageUtils; +import org.geysermc.connector.utils.SettingsUtils; public class UpstreamPacketHandler extends LoggingPacketHandler { @@ -91,6 +92,10 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(ModalFormResponsePacket packet) { + if (packet.getFormId() == SettingsUtils.SETTINGS_FORM_ID) { + return SettingsUtils.handleSettingsForm(session, packet.getFormData()); + } + return LoginEncryptionUtils.authenticateFromForm(session, connector, packet.getFormId(), packet.getFormData()); } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index b861f64c..05465c46 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -46,6 +46,7 @@ import com.nukkitx.math.vector.*; import com.nukkitx.protocol.bedrock.BedrockPacket; import com.nukkitx.protocol.bedrock.BedrockServerSession; import com.nukkitx.protocol.bedrock.data.*; +import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.packet.*; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; @@ -54,6 +55,7 @@ import it.unimi.dsi.fastutil.objects.Object2LongMap; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import lombok.Getter; import lombok.Setter; +import org.geysermc.common.window.CustomFormWindow; import org.geysermc.common.window.FormWindow; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; @@ -79,9 +81,7 @@ import java.net.InetSocketAddress; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @Getter @@ -102,7 +102,7 @@ public class GeyserSession implements CommandSender { private ChunkCache chunkCache; private EntityCache entityCache; private InventoryCache inventoryCache; - private ScoreboardCache scoreboardCache; + private WorldCache worldCache; private WindowCache windowCache; @Setter private TeleportCache teleportCache; @@ -191,6 +191,41 @@ public class GeyserSession implements CommandSender { private MinecraftProtocol protocol; + private boolean reducedDebugInfo = false; + + @Setter + private CustomFormWindow settingsForm; + + /** + * The op permission level set by the server + */ + @Setter + private int opPermissionLevel = 0; + + /** + * If the current player can fly + */ + @Setter + private boolean canFly = false; + + /** + * If the current player is flying + */ + @Setter + private boolean flying = false; + + /** + * If the current player is in noclip + */ + @Setter + private boolean noClip = false; + + /** + * If the current player can not interact with the world + */ + @Setter + private boolean worldImmutable = false; + public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { this.connector = connector; this.upstream = new UpstreamSession(bedrockServerSession); @@ -198,7 +233,7 @@ public class GeyserSession implements CommandSender { this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); this.inventoryCache = new InventoryCache(this); - this.scoreboardCache = new ScoreboardCache(this); + this.worldCache = new WorldCache(this); this.windowCache = new WindowCache(this); this.playerEntity = new PlayerEntity(new GameProfile(UUID.randomUUID(), "unknown"), 1, 1, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO); @@ -249,17 +284,12 @@ public class GeyserSession implements CommandSender { attributes.add(new AttributeData("minecraft:movement", 0.0f, 1024f, 0.1f, 0.1f)); attributesPacket.setAttributes(attributes); upstream.sendPacket(attributesPacket); - } - public void fetchOurSkin(PlayerListPacket.Entry entry) { - PlayerSkinPacket playerSkinPacket = new PlayerSkinPacket(); - playerSkinPacket.setUuid(authData.getUUID()); - playerSkinPacket.setSkin(entry.getSkin()); - playerSkinPacket.setOldSkinName("OldName"); - playerSkinPacket.setNewSkinName("NewName"); - playerSkinPacket.setTrustedSkin(true); - upstream.sendPacket(playerSkinPacket); - getConnector().getLogger().debug("Sending skin for " + playerEntity.getUsername() + " " + authData.getUUID()); + // Only allow the server to send health information + // Setting this to false allows natural regeneration to work false but doesn't break it being true + GameRulesChangedPacket gamerulePacket = new GameRulesChangedPacket(); + gamerulePacket.getGameRules().add(new GameRuleData<>("naturalregeneration", false)); + upstream.sendPacket(gamerulePacket); } public void login() { @@ -445,7 +475,7 @@ public class GeyserSession implements CommandSender { this.chunkCache = null; this.entityCache = null; - this.scoreboardCache = null; + this.worldCache = null; this.inventoryCache = null; this.windowCache = null; @@ -610,4 +640,69 @@ public class GeyserSession implements CommandSender { connector.getLogger().debug("Tried to send downstream packet " + packet.getClass().getSimpleName() + " before connected to the server"); } } + + /** + * Update the cached value for the reduced debug info gamerule. + * This also toggles the coordinates display + * + * @param value The new value for reducedDebugInfo + */ + public void setReducedDebugInfo(boolean value) { + worldCache.setShowCoordinates(!value); + reducedDebugInfo = value; + } + + /** + * Send a gamerule value to the client + * + * @param gameRule The gamerule to send + * @param value The value of the gamerule + */ + public void sendGameRule(String gameRule, Object value) { + GameRulesChangedPacket gameRulesChangedPacket = new GameRulesChangedPacket(); + gameRulesChangedPacket.getGameRules().add(new GameRuleData<>(gameRule, value)); + upstream.sendPacket(gameRulesChangedPacket); + } + + /** + * Checks if the given session's player has a permission + * + * @param permission The permission node to check + * @return true if the player has the requested permission, false if not + */ + public Boolean hasPermission(String permission) { + return connector.getWorldManager().hasPermission(this, permission); + } + + /** + * Send an AdventureSettingsPacket to the client with the latest flags + */ + public void sendAdventureSettings() { + AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); + adventureSettingsPacket.setUniqueEntityId(playerEntity.getGeyserId()); + adventureSettingsPacket.setCommandPermission(CommandPermission.NORMAL); + adventureSettingsPacket.setPlayerPermission(PlayerPermission.MEMBER); + + Set flags = new HashSet<>(); + if (canFly) { + flags.add(AdventureSetting.MAY_FLY); + } + + if (flying) { + flags.add(AdventureSetting.FLYING); + } + + if (worldImmutable) { + flags.add(AdventureSetting.WORLD_IMMUTABLE); + } + + if (noClip) { + flags.add(AdventureSetting.NO_CLIP); + } + + flags.add(AdventureSetting.AUTO_JUMP); + + adventureSettingsPacket.getSettings().addAll(flags); + sendUpstreamPacket(adventureSettingsPacket); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java similarity index 78% rename from connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java rename to connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java index 9a692407..310e5f9d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/ScoreboardCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java @@ -25,7 +25,9 @@ package org.geysermc.connector.network.session.cache; +import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; import lombok.Getter; +import lombok.Setter; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Scoreboard; @@ -33,11 +35,18 @@ import org.geysermc.connector.scoreboard.Scoreboard; import java.util.Collection; @Getter -public class ScoreboardCache { +public class WorldCache { + private GeyserSession session; + + @Setter + private Difficulty difficulty = Difficulty.EASY; + + private boolean showCoordinates = true; + private Scoreboard scoreboard; - public ScoreboardCache(GeyserSession session) { + public WorldCache(GeyserSession session) { this.session = session; this.scoreboard = new Scoreboard(session); } @@ -52,4 +61,14 @@ public class ScoreboardCache { } } } + + /** + * Tell the client to hide or show the coordinates + * + * @param value True to show, false to hide + */ + public void setShowCoordinates(boolean value) { + showCoordinates = value; + session.sendGameRule("showcoordinates", value); + } } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/CachedChunkManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java similarity index 55% rename from connector/src/main/java/org/geysermc/connector/network/translators/world/CachedChunkManager.java rename to connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java index 0580fcff..a8591cd7 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/CachedChunkManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java @@ -23,15 +23,25 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.world; +package org.geysermc.connector.network.translators.bedrock; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.nukkitx.protocol.bedrock.packet.ServerSettingsRequestPacket; +import com.nukkitx.protocol.bedrock.packet.ServerSettingsResponsePacket; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.utils.SettingsUtils; -public class CachedChunkManager extends WorldManager { +@Translator(packet = ServerSettingsRequestPacket.class) +public class BedrockServerSettingsRequestTranslator extends PacketTranslator { @Override - public int getBlockAt(GeyserSession session, int x, int y, int z) { - return session.getChunkCache().getBlockAt(new Position(x, y, z)); + public void translate(ServerSettingsRequestPacket packet, GeyserSession session) { + SettingsUtils.buildForm(session); + + ServerSettingsResponsePacket serverSettingsResponsePacket = new ServerSettingsResponsePacket(); + serverSettingsResponsePacket.setFormData(session.getSettingsForm().getJSONData()); + serverSettingsResponsePacket.setFormId(SettingsUtils.SETTINGS_FORM_ID); + session.sendUpstreamPacket(serverSettingsResponsePacket); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index 87da2d00..c1ad7409 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -44,8 +44,8 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) { if (!entity.isValid()) { - // async skin loading - SkinUtils.requestAndHandleSkinAndCape(entity, session, skinAndCape -> entity.sendPlayer(session)); + SkinUtils.requestAndHandleSkinAndCape(entity, session, null); + entity.sendPlayer(session); } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityEventTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java similarity index 98% rename from connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityEventTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java index 620e2b8a..18fd6614 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityEventTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.bedrock; +package org.geysermc.connector.network.translators.bedrock.entity; import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; import com.github.steveice10.mc.protocol.data.game.window.WindowType; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java similarity index 88% rename from connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockActionTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java index 63862768..0d4f7371 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockActionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.bedrock; +package org.geysermc.connector.network.translators.bedrock.entity.player; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; @@ -116,6 +116,18 @@ public class BedrockActionTranslator extends PacketTranslator playerFlags = new ObjectOpenHashSet<>(); - playerFlags.add(AdventureSetting.AUTO_JUMP); - if (packet.isCanFly()) - playerFlags.add(AdventureSetting.MAY_FLY); - - if (packet.isFlying()) - playerFlags.add(AdventureSetting.FLYING); - - AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); - adventureSettingsPacket.setPlayerPermission(PlayerPermission.MEMBER); - // Required or the packet simply is not sent - adventureSettingsPacket.setCommandPermission(CommandPermission.NORMAL); - adventureSettingsPacket.setUniqueEntityId(entity.getGeyserId()); - adventureSettingsPacket.getSettings().addAll(playerFlags); - session.sendUpstreamPacket(adventureSettingsPacket); + session.setCanFly(packet.isCanFly()); + session.setFlying(packet.isFlying()); + session.sendAdventureSettings(); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java index f387daec..10b2ba9a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java @@ -82,18 +82,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator entity.sendPlayer(session)); + entity.sendPlayer(session); + SkinUtils.requestAndHandleSkinAndCape(entity, session, null); } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java index 5a722953..3ee174d7 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaDisplayScoreboardTranslator.java @@ -36,7 +36,7 @@ public class JavaDisplayScoreboardTranslator extends PacketTranslator { public void translate(ServerTeamPacket packet, GeyserSession session) { GeyserConnector.getInstance().getLogger().debug("Team packet " + packet.getTeamName() + " " + packet.getAction() + " " + Arrays.toString(packet.getPlayers())); - Scoreboard scoreboard = session.getScoreboardCache().getScoreboard(); + Scoreboard scoreboard = session.getWorldCache().getScoreboard(); Team team = scoreboard.getTeam(packet.getTeamName()); switch (packet.getAction()) { case CREATE: @@ -65,21 +65,21 @@ public class JavaTeamTranslator extends PacketTranslator { .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode())) .setUpdateType(UpdateType.UPDATE); } else { - GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); + GeyserConnector.getInstance().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); } break; case ADD_PLAYER: - if(team != null){ + if (team != null) { team.addEntities(packet.getPlayers()); } else { - GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); + GeyserConnector.getInstance().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); } break; case REMOVE_PLAYER: - if(team != null){ + if (team != null) { team.removeEntities(packet.getPlayers()); } else { - GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); + GeyserConnector.getInstance().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); } break; case REMOVE: diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java index 827e4c7f..8d7d59a8 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java @@ -42,7 +42,7 @@ public class JavaUpdateScoreTranslator extends PacketTranslator playerFlags = new ObjectOpenHashSet<>(); GameMode gameMode = (GameMode) packet.getValue(); - if (gameMode == GameMode.ADVENTURE) - playerFlags.add(AdventureSetting.WORLD_IMMUTABLE); - if (gameMode == GameMode.CREATIVE) - playerFlags.add(AdventureSetting.MAY_FLY); - - if (gameMode == GameMode.SPECTATOR) { - playerFlags.add(AdventureSetting.MAY_FLY); - playerFlags.add(AdventureSetting.NO_CLIP); - playerFlags.add(AdventureSetting.FLYING); - playerFlags.add(AdventureSetting.WORLD_IMMUTABLE); - gameMode = GameMode.CREATIVE; // spectator doesnt exist on bedrock - } - - playerFlags.add(AdventureSetting.AUTO_JUMP); + session.setNoClip(gameMode == GameMode.SPECTATOR); + session.setWorldImmutable(gameMode == GameMode.ADVENTURE || gameMode == GameMode.SPECTATOR); + session.sendAdventureSettings(); SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket(); playerGameTypePacket.setGamemode(gameMode.ordinal()); session.sendUpstreamPacket(playerGameTypePacket); session.setGameMode(gameMode); - // We need to delay this because otherwise it's overridden by the adventure settings from the abilities packet - session.getConnector().getGeneralThreadPool().schedule(() -> { - AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); - adventureSettingsPacket.setPlayerPermission(PlayerPermission.MEMBER); - adventureSettingsPacket.setCommandPermission(CommandPermission.NORMAL); - adventureSettingsPacket.setUniqueEntityId(entity.getGeyserId()); - adventureSettingsPacket.getSettings().addAll(playerFlags); - session.sendUpstreamPacket(adventureSettingsPacket); - }, 50, TimeUnit.MILLISECONDS); - // Update the crafting grid to add/remove barriers for creative inventory PlayerInventoryTranslator.updateCraftingGrid(session, session.getInventory()); break; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaSpawnParticleTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaSpawnParticleTranslator.java index 323f57f0..453e0844 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaSpawnParticleTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaSpawnParticleTranslator.java @@ -78,7 +78,7 @@ public class JavaSpawnParticleTranslator extends PacketTranslator("dodaylightcycle", doCycle)); - session.sendUpstreamPacket(gameRulesChangedPacket); + session.sendGameRule("dodaylightcycle", doCycle); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java new file mode 100644 index 00000000..83f1a778 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2020 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.connector.network.translators.world; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +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.mc.protocol.packet.ingame.client.ClientChatPacket; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.GameRule; + +public class GeyserWorldManager extends WorldManager { + + private static final Object2ObjectMap gameruleCache = new Object2ObjectOpenHashMap<>(); + + @Override + public int getBlockAt(GeyserSession session, int x, int y, int z) { + return session.getChunkCache().getBlockAt(new Position(x, y, z)); + } + + @Override + public void setGameRule(GeyserSession session, String name, Object value) { + session.sendDownstreamPacket(new ClientChatPacket("/gamerule " + name + " " + value)); + gameruleCache.put(name, String.valueOf(value)); + } + + @Override + public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { + String value = gameruleCache.get(gameRule.getJavaID()); + if (value != null) { + return Boolean.parseBoolean(value); + } + + return gameRule.getDefaultValue() != null ? (Boolean) gameRule.getDefaultValue() : false; + } + + @Override + public int getGameRuleInt(GeyserSession session, GameRule gameRule) { + String value = gameruleCache.get(gameRule.getJavaID()); + if (value != null) { + return Integer.parseInt(value); + } + + return gameRule.getDefaultValue() != null ? (int) gameRule.getDefaultValue() : 0; + } + + @Override + public void setPlayerGameMode(GeyserSession session, GameMode gameMode) { + session.sendDownstreamPacket(new ClientChatPacket("/gamemode " + gameMode.name().toLowerCase())); + } + + @Override + public void setDifficulty(GeyserSession session, Difficulty difficulty) { + session.sendDownstreamPacket(new ClientChatPacket("/difficulty " + difficulty.name().toLowerCase())); + } + + @Override + public boolean hasPermission(GeyserSession session, String permission) { + return false; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java index 325e6860..32601227 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java @@ -26,8 +26,11 @@ package org.geysermc.connector.network.translators.world; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; import com.nukkitx.math.vector.Vector3i; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.GameRule; /** * Class that manages or retrieves various information @@ -70,4 +73,56 @@ public abstract class WorldManager { * @return the block state at the specified location */ public abstract int getBlockAt(GeyserSession session, int x, int y, int z); + + /** + * Updates a gamerule value on the Java server + * + * @param session The session of the user that requested the change + * @param name The gamerule to change + * @param value The new value for the gamerule + */ + public abstract void setGameRule(GeyserSession session, String name, Object value); + + /** + * Get a gamerule value as a boolean + * + * @param session The session of the user that requested the value + * @param gameRule The gamerule to fetch the value of + * @return The boolean representation of the value + */ + public abstract Boolean getGameRuleBool(GeyserSession session, GameRule gameRule); + + /** + * Get a gamerule value as an integer + * + * @param session The session of the user that requested the value + * @param gameRule The gamerule to fetch the value of + * @return The integer representation of the value + */ + public abstract int getGameRuleInt(GeyserSession session, GameRule gameRule); + + /** + * Change the game mode of the given session + * + * @param session The session of the player to change the game mode of + * @param gameMode The game mode to change the player to + */ + public abstract void setPlayerGameMode(GeyserSession session, GameMode gameMode); + + /** + * Change the difficulty of the Java server + * + * @param session The session of the user that requested the change + * @param difficulty The difficulty to change to + */ + public abstract void setDifficulty(GeyserSession session, Difficulty difficulty); + + /** + * Checks if the given session's player has a permission + * + * @param session The session of the player to check the permission of + * @param permission The permission node to check + * @return True if the player has the requested permission, false if not + */ + public abstract boolean hasPermission(GeyserSession session, String permission); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java index 0b7b5c5c..38369d6c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -42,6 +42,7 @@ public class FileUtils { * * @param src File to load * @param valueType Class to load file into + * @param the type * @return The data as the given class * @throws IOException if the config could not be loaded */ diff --git a/connector/src/main/java/org/geysermc/connector/utils/GameRule.java b/connector/src/main/java/org/geysermc/connector/utils/GameRule.java new file mode 100644 index 00000000..48feb1c1 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/GameRule.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019-2020 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.connector.utils; + +import lombok.Getter; + +/** + * This enum stores each gamerule along with the value type and the default. + * It is used to construct the list for the settings menu + */ +public enum GameRule { + ANNOUNCEADVANCEMENTS("announceAdvancements", Boolean.class, true), // JE only + COMMANDBLOCKOUTPUT("commandBlockOutput", Boolean.class, true), + DISABLEELYTRAMOVEMENTCHECK("disableElytraMovementCheck", Boolean.class, false), // JE only + DISABLERAIDS("disableRaids", Boolean.class, false), // JE only + DODAYLIGHTCYCLE("doDaylightCycle", Boolean.class, true), + DOENTITYDROPS("doEntityDrops", Boolean.class, true), + DOFIRETICK("doFireTick", Boolean.class, true), + DOIMMEDIATERESPAWN("doImmediateRespawn", Boolean.class, false), + DOINSOMNIA("doInsomnia", Boolean.class, true), + DOLIMITEDCRAFTING("doLimitedCrafting", Boolean.class, false), // JE only + DOMOBLOOT("doMobLoot", Boolean.class, true), + DOMOBSPAWNING("doMobSpawning", Boolean.class, true), + DOPATROLSPAWNING("doPatrolSpawning", Boolean.class, true), // JE only + DOTILEDROPS("doTileDrops", Boolean.class, true), + DOTRADERSPAWNING("doTraderSpawning", Boolean.class, true), // JE only + DOWEATHERCYCLE("doWeatherCycle", Boolean.class, true), + DROWNINGDAMAGE("drowningDamage", Boolean.class, true), + FALLDAMAGE("fallDamage", Boolean.class, true), + FIREDAMAGE("fireDamage", Boolean.class, true), + FORGIVEDEADPLAYERS("forgiveDeadPlayers", Boolean.class, true), // JE only + KEEPINVENTORY("keepInventory", Boolean.class, false), + LOGADMINCOMMANDS("logAdminCommands", Boolean.class, true), // JE only + MAXCOMMANDCHAINLENGTH("maxCommandChainLength", Integer.class, 65536), + MAXENTITYCRAMMING("maxEntityCramming", Integer.class, 24), // JE only + MOBGRIEFING("mobGriefing", Boolean.class, true), + NATURALREGENERATION("naturalRegeneration", Boolean.class, true), + RANDOMTICKSPEED("randomTickSpeed", Integer.class, 3), + REDUCEDDEBUGINFO("reducedDebugInfo", Boolean.class, false), // JE only + SENDCOMMANDFEEDBACK("sendCommandFeedback", Boolean.class, true), + SHOWDEATHMESSAGES("showDeathMessages", Boolean.class, true), + SPAWNRADIUS("spawnRadius", Integer.class, 10), + SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", Boolean.class, true), // JE only + UNIVERSALANGER("universalAnger", Boolean.class, false), // JE only + + UNKNOWN("unknown", Object.class); + + private static final GameRule[] VALUES = values(); + + @Getter + private String javaID; + + @Getter + private Class type; + + @Getter + private Object defaultValue; + + GameRule(String javaID, Class type) { + this(javaID, type, null); + } + + GameRule(String javaID, Class type, Object defaultValue) { + this.javaID = javaID; + this.type = type; + this.defaultValue = defaultValue; + } + + /** + * Convert a string to an object of the correct type for the current gamerule + * + * @param value The string value to convert + * @return The converted and formatted value + */ + public Object convertValue(String value) { + if (type.equals(Boolean.class)) { + return Boolean.parseBoolean(value); + } else if (type.equals(Integer.class)) { + return Integer.parseInt(value); + } + + return null; + } + + /** + * Fetch a game rule by the given Java ID + * + * @param id The ID of the gamerule + * @return A {@link GameRule} object representing the requested ID or {@link GameRule#UNKNOWN} + */ + public static GameRule fromJavaID(String id) { + for (GameRule gamerule : VALUES) { + if (gamerule.javaID.equals(id)) { + return gamerule; + } + } + + return UNKNOWN; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java index 6d83da0a..cb51e2f3 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java @@ -136,6 +136,9 @@ public class InventoryUtils { /** * Returns a barrier block with custom name and lore to explain why * part of the inventory is unusable. + * + * @param description the description + * @return the unusable space block */ public static ItemData createUnusableSpaceBlock(String description) { NbtMapBuilder root = NbtMap.builder(); diff --git a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java index 61e23470..de6796a2 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -122,7 +122,7 @@ public class LanguageUtils { formatString = key; } - return MessageFormat.format(formatString.replace("&", "\u00a7"), values); + return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values); } /** diff --git a/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java index 0b495895..36cdbc42 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/MessageUtils.java @@ -95,7 +95,7 @@ public class MessageUtils { * @param messages A {@link List} of {@link Message} to parse * @param locale A locale loaded to get the message for * @param parent A {@link Message} to use as the parent (can be null) - * @return + * @return the translation parameters */ public static List getTranslationParams(List messages, String locale, Message parent) { List strings = new ArrayList<>(); @@ -160,10 +160,10 @@ public class MessageUtils { * Translate a given {@link TranslationMessage} to the given locale * * @param message The {@link Message} to send - * @param locale - * @param shouldTranslate - * @param parent - * @return + * @param locale the locale + * @param shouldTranslate if the message should be translated + * @param parent the parent message + * @return the given translation message translated from the given locale */ public static String getTranslatedBedrockMessage(Message message, String locale, boolean shouldTranslate, Message parent) { JsonParser parser = new JsonParser(); diff --git a/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java new file mode 100644 index 00000000..89e9fe67 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2019-2020 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.connector.utils; + +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; +import org.geysermc.common.window.CustomFormBuilder; +import org.geysermc.common.window.CustomFormWindow; +import org.geysermc.common.window.button.FormImage; +import org.geysermc.common.window.component.DropdownComponent; +import org.geysermc.common.window.component.InputComponent; +import org.geysermc.common.window.component.LabelComponent; +import org.geysermc.common.window.component.ToggleComponent; +import org.geysermc.common.window.response.CustomFormResponse; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; + +import java.util.ArrayList; + +public class SettingsUtils { + + // Used in UpstreamPacketHandler.java + public static final int SETTINGS_FORM_ID = 1338; + + /** + * Build a settings form for the given session and store it for later + * + * @param session The session to build the form for + */ + public static void buildForm(GeyserSession session) { + // Cache the language for cleaner access + String language = session.getClientData().getLanguageCode(); + + CustomFormBuilder builder = new CustomFormBuilder(LanguageUtils.getPlayerLocaleString("geyser.settings.title.main", language)); + builder.setIcon(new FormImage(FormImage.FormImageType.PATH, "textures/ui/settings_glyph_color_2x.png")); + + builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.client", language))); + builder.addComponent(new ToggleComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.option.coordinates", language, session.getWorldCache().isShowCoordinates()))); + + + if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) { + builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.server", language))); + + DropdownComponent gamemodeDropdown = new DropdownComponent(); + gamemodeDropdown.setText("%createWorldScreen.gameMode.personal"); + gamemodeDropdown.setOptions(new ArrayList<>()); + for (GameMode gamemode : GameMode.values()) { + gamemodeDropdown.addOption(LocaleUtils.getLocaleString("selectWorld.gameMode." + gamemode.name().toLowerCase(), language), session.getGameMode() == gamemode); + } + builder.addComponent(gamemodeDropdown); + + DropdownComponent difficultyDropdown = new DropdownComponent(); + difficultyDropdown.setText("%options.difficulty"); + difficultyDropdown.setOptions(new ArrayList<>()); + for (Difficulty difficulty : Difficulty.values()) { + difficultyDropdown.addOption("%options.difficulty." + difficulty.name().toLowerCase(), session.getWorldCache().getDifficulty() == difficulty); + } + builder.addComponent(difficultyDropdown); + } + + if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) { + builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.game_rules", language))); + for (GameRule gamerule : GameRule.values()) { + if (gamerule.equals(GameRule.UNKNOWN)) { + continue; + } + + // Add the relevant form item based on the gamerule type + if (Boolean.class.equals(gamerule.getType())) { + builder.addComponent(new ToggleComponent(LocaleUtils.getLocaleString("gamerule." + gamerule.getJavaID(), language), GeyserConnector.getInstance().getWorldManager().getGameRuleBool(session, gamerule))); + } else if (Integer.class.equals(gamerule.getType())) { + builder.addComponent(new InputComponent(LocaleUtils.getLocaleString("gamerule." + gamerule.getJavaID(), language), "", String.valueOf(GeyserConnector.getInstance().getWorldManager().getGameRuleInt(session, gamerule)))); + } + } + } + + session.setSettingsForm(builder.build()); + } + + /** + * Handle the settings form response + * + * @param session The session that sent the response + * @param response The response string to parse + * @return True if the form was parsed correctly, false if not + */ + public static boolean handleSettingsForm(GeyserSession session, String response) { + CustomFormWindow settingsForm = session.getSettingsForm(); + settingsForm.setResponse(response); + + CustomFormResponse settingsResponse = (CustomFormResponse) settingsForm.getResponse(); + int offset = 0; + + offset++; // Client settings title + + session.getWorldCache().setShowCoordinates(settingsResponse.getToggleResponses().get(offset)); + offset++; + + if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) { + offset++; // Server settings title + + GameMode gameMode = GameMode.values()[settingsResponse.getDropdownResponses().get(offset).getElementID()]; + if (gameMode != null && gameMode != session.getGameMode()) { + session.getConnector().getWorldManager().setPlayerGameMode(session, gameMode); + } + offset++; + + Difficulty difficulty = Difficulty.values()[settingsResponse.getDropdownResponses().get(offset).getElementID()]; + if (difficulty != null && difficulty != session.getWorldCache().getDifficulty()) { + session.getConnector().getWorldManager().setDifficulty(session, difficulty); + } + offset++; + } + + if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) { + offset++; // Game rule title + + for (GameRule gamerule : GameRule.values()) { + if (gamerule.equals(GameRule.UNKNOWN)) { + continue; + } + + if (Boolean.class.equals(gamerule.getType())) { + Boolean value = settingsResponse.getToggleResponses().get(offset).booleanValue(); + if (value != session.getConnector().getWorldManager().getGameRuleBool(session, gamerule)) { + session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); + } + } else if (Integer.class.equals(gamerule.getType())) { + int value = Integer.parseInt(settingsResponse.getInputResponses().get(offset)); + if (value != session.getConnector().getWorldManager().getGameRuleInt(session, gamerule)) { + session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); + } + } + offset++; + } + } + + return true; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java index 36507db0..352a0b0f 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -27,6 +27,8 @@ package org.geysermc.connector.utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -40,9 +42,11 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Base64; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.*; @@ -54,22 +58,26 @@ public class SkinProvider { 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 Map cachedSkins = new ConcurrentHashMap<>(); - private static Map> requestedSkins = new ConcurrentHashMap<>(); + private static final Cache cachedSkins = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .build(); + + private static final Map> requestedSkins = new ConcurrentHashMap<>(); public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true); - private static Map cachedCapes = new ConcurrentHashMap<>(); - private static Map> requestedCapes = new ConcurrentHashMap<>(); + private static final Cache cachedCapes = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .build(); + private static final Map> requestedCapes = new ConcurrentHashMap<>(); public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false); - private static Map cachedGeometry = new ConcurrentHashMap<>(); + private static final Map cachedGeometry = new ConcurrentHashMap<>(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); public static String EARS_GEOMETRY; public static String EARS_GEOMETRY_SLIM; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes static { /* Load in the normal ears geometry */ @@ -102,22 +110,44 @@ public class SkinProvider { } EARS_GEOMETRY_SLIM = earsDataBuilder.toString(); - } - public static boolean hasSkinCached(UUID uuid) { - return cachedSkins.containsKey(uuid); + // Schedule Daily Image Expiry if we are caching them + if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) { + GeyserConnector.getInstance().getGeneralThreadPool().scheduleAtFixedRate(() -> { + File cacheFolder = Paths.get("cache", "images").toFile(); + if (!cacheFolder.exists()) { + return; + } + + int count = 0; + final long expireTime = ((long)GeyserConnector.getInstance().getConfig().getCacheImages()) * ((long)1000 * 60 * 60 * 24); + for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) { + if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) { + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + count++; + } + } + + if (count > 0) { + GeyserConnector.getInstance().getLogger().debug(String.format("Removed %d cached image files as they have expired", count)); + } + }, 10, 1440, TimeUnit.MINUTES); + } } public static boolean hasCapeCached(String capeUrl) { - return cachedCapes.containsKey(capeUrl); + return cachedCapes.getIfPresent(capeUrl) != null; } - public static Skin getCachedSkin(UUID uuid) { - return cachedSkins.getOrDefault(uuid, EMPTY_SKIN); + public static Skin getCachedSkin(String skinUrl) { + Skin skin = cachedSkins.getIfPresent(skinUrl); + return skin != null ? skin : EMPTY_SKIN; } public static Cape getCachedCape(String capeUrl) { - return capeUrl != null ? cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE) : EMPTY_CAPE; + Cape cape = capeUrl != null ? cachedCapes.getIfPresent(capeUrl) : EMPTY_CAPE; + return cape != null ? cape : EMPTY_CAPE; } public static CompletableFuture requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) { @@ -137,28 +167,26 @@ public class SkinProvider { public static CompletableFuture requestSkin(UUID playerId, String textureUrl, boolean newThread) { if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN); - if (requestedSkins.containsKey(playerId)) return requestedSkins.get(playerId); // already requested + if (requestedSkins.containsKey(textureUrl)) return requestedSkins.get(textureUrl); // already requested - if ((System.currentTimeMillis() - CACHE_INTERVAL) < cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getRequestedOn()) { - // no need to update, still cached - return CompletableFuture.completedFuture(cachedSkins.get(playerId)); + Skin cachedSkin = cachedSkins.getIfPresent(textureUrl); + if (cachedSkin != null) { + return CompletableFuture.completedFuture(cachedSkin); } CompletableFuture future; if (newThread) { future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE) .whenCompleteAsync((skin, throwable) -> { - if (!cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getTextureUrl().equals(textureUrl)) { - skin.updated = true; - cachedSkins.put(playerId, skin); - } - requestedSkins.remove(skin.getSkinOwner()); + skin.updated = true; + cachedSkins.put(textureUrl, skin); + requestedSkins.remove(textureUrl); }); - requestedSkins.put(playerId, future); + requestedSkins.put(textureUrl, future); } else { Skin skin = supplySkin(playerId, textureUrl); future = CompletableFuture.completedFuture(skin); - cachedSkins.put(playerId, skin); + cachedSkins.put(textureUrl, skin); } return future; } @@ -168,11 +196,9 @@ public class SkinProvider { if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested boolean officialCape = provider == CapeProvider.MINECRAFT; - boolean validCache = (System.currentTimeMillis() - CACHE_INTERVAL) < cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE).getRequestedOn(); - - if ((cachedCapes.containsKey(capeUrl) && officialCape) || validCache) { - // the cape is an official cape (static) or the cape doesn't need a update yet - return CompletableFuture.completedFuture(cachedCapes.get(capeUrl)); + Cape cachedCape = cachedCapes.getIfPresent(capeUrl); + if (cachedCape != null) { + return CompletableFuture.completedFuture(cachedCape); } CompletableFuture future; @@ -245,7 +271,10 @@ public class SkinProvider { } public static CompletableFuture requestBedrockCape(UUID playerID, boolean newThread) { - Cape bedrockCape = cachedCapes.getOrDefault(playerID.toString() + ".Bedrock", EMPTY_CAPE); + Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock"); + if (bedrockCape == null) { + bedrockCape = EMPTY_CAPE; + } return CompletableFuture.completedFuture(bedrockCape); } @@ -256,7 +285,7 @@ public class SkinProvider { public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) { Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false); - cachedSkins.put(playerID, skin); + cachedSkins.put(skin.getTextureUrl(), skin); } public static void storeBedrockCape(UUID playerID, byte[] capeData) { @@ -276,7 +305,7 @@ public class SkinProvider { * @param skin The skin to cache */ public static void storeEarSkin(UUID playerID, Skin skin) { - cachedSkins.put(playerID, skin); + cachedSkins.put(skin.getTextureUrl(), skin); } /** @@ -290,11 +319,12 @@ public class SkinProvider { } private static Skin supplySkin(UUID uuid, String textureUrl) { - byte[] skin = EMPTY_SKIN.getSkinData(); try { - skin = requestImage(textureUrl, null); + byte[] skin = requestImage(textureUrl, null); + return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); } catch (Exception ignored) {} // just ignore I guess - return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false); + + return new Skin(uuid, "empty", EMPTY_SKIN.getSkinData(), System.currentTimeMillis(), false, false); } private static Cape supplyCape(String capeUrl, CapeProvider provider) { @@ -356,11 +386,38 @@ public class SkinProvider { return existingSkin; } + @SuppressWarnings("ResultOfMethodCallIgnored") private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception { - BufferedImage image = downloadImage(imageUrl, provider); - GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl); + BufferedImage image = null; - // if the requested image is an cape + // First see if we have a cached file. We also update the modification stamp so we know when the file was last used + File imageFile = Paths.get("cache", "images", UUID.nameUUIDFromBytes(imageUrl.getBytes()).toString() + ".png").toFile(); + if (imageFile.exists()) { + try { + GeyserConnector.getInstance().getLogger().debug("Reading cached image from file " + imageFile.getPath() + " for " + imageUrl); + imageFile.setLastModified(System.currentTimeMillis()); + image = ImageIO.read(imageFile); + } catch (IOException ignored) {} + } + + // If no image we download it + if (image == null) { + image = downloadImage(imageUrl, provider); + GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl); + + // Write to cache if we are allowed + if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) { + imageFile.getParentFile().mkdirs(); + try { + ImageIO.write(image, "png", imageFile); + GeyserConnector.getInstance().getLogger().debug("Writing cached skin to file " + imageFile.getPath() + " for " + imageUrl); + } catch (IOException e) { + GeyserConnector.getInstance().getLogger().error("Failed to write cached skin to file " + imageFile.getPath() + " for " + imageUrl); + } + } + } + + // if the requested image is a cape if (provider != null) { while(image.getWidth() > 64) { image = scale(image); diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java index 22588576..1d5e4073 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java @@ -35,6 +35,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.auth.BedrockClientData; @@ -47,18 +48,21 @@ import java.util.function.Consumer; public class SkinUtils { - public static PlayerListPacket.Entry buildCachedEntry(GameProfile profile, long geyserId) { + public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, GameProfile profile, long geyserId) { GameProfileData data = GameProfileData.from(profile); SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl()); SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex()); + SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl()); + return buildEntryManually( + session, profile.getId(), profile.getName(), geyserId, - profile.getIdAsString(), - SkinProvider.getCachedSkin(profile.getId()).getSkinData(), + skin.getTextureUrl(), + skin.getSkinData(), cape.getCapeId(), cape.getCapeData(), geometry.getGeometryName(), @@ -66,12 +70,13 @@ public class SkinUtils { ); } - public static PlayerListPacket.Entry buildDefaultEntry(GameProfile profile, long geyserId) { + public static PlayerListPacket.Entry buildDefaultEntry(GeyserSession session, GameProfile profile, long geyserId) { return buildEntryManually( + session, profile.getId(), profile.getName(), geyserId, - profile.getIdAsString(), + "default", SkinProvider.STEVE_SKIN, SkinProvider.EMPTY_CAPE.getCapeId(), SkinProvider.EMPTY_CAPE.getCapeData(), @@ -80,20 +85,38 @@ public class SkinUtils { ); } - public static PlayerListPacket.Entry buildEntryManually(UUID uuid, String username, long geyserId, + public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, String skinId, byte[] skinData, String capeId, byte[] capeData, String geometryName, String geometryData) { SerializedSkin serializedSkin = SerializedSkin.of( skinId, geometryName, ImageData.of(skinData), Collections.emptyList(), - ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, uuid.toString() + ImageData.of(capeData), geometryData, "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId ); - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid); + // This attempts to find the xuid of the player so profile images show up for xbox accounts + String xuid = ""; + for (GeyserSession player : GeyserConnector.getInstance().getPlayers()) { + if (player.getPlayerEntity().getUuid().equals(uuid)) { + xuid = player.getAuthData().getXboxUUID(); + break; + } + } + + PlayerListPacket.Entry entry; + + // If we are building a PlayerListEntry for our own session we use our AuthData UUID instead of the Java UUID + // as bedrock expects to get back its own provided uuid + if (session.getPlayerEntity().getUuid().equals(uuid)) { + entry = new PlayerListPacket.Entry(session.getAuthData().getUUID()); + } else { + entry = new PlayerListPacket.Entry(uuid); + } + entry.setName(username); entry.setEntityId(geyserId); entry.setSkin(serializedSkin); - entry.setXuid(""); + entry.setXuid(xuid); entry.setPlatformChatId(""); entry.setTeacher(false); entry.setTrustedSkin(true); @@ -201,48 +224,34 @@ public class SkinUtils { } } - if (entity.getLastSkinUpdate() < skin.getRequestedOn()) { - entity.setLastSkinUpdate(skin.getRequestedOn()); + entity.setLastSkinUpdate(skin.getRequestedOn()); - if (session.getUpstream().isInitialized()) { - PlayerListPacket.Entry updatedEntry = buildEntryManually( - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - entity.getUuid().toString(), - skin.getSkinData(), - cape.getCapeId(), - cape.getCapeData(), - geometry.getGeometryName(), - geometry.getGeometryData() - ); + if (session.getUpstream().isInitialized()) { + PlayerListPacket.Entry updatedEntry = buildEntryManually( + session, + entity.getUuid(), + entity.getUsername(), + entity.getGeyserId(), + skin.getTextureUrl(), + skin.getSkinData(), + cape.getCapeId(), + cape.getCapeData(), + geometry.getGeometryName(), + geometry.getGeometryData() + ); - // If it is our skin we replace the UUID with the authdata UUID - if (session.getPlayerEntity() == entity) { - // Copy the entry with our identity instead. - PlayerListPacket.Entry copy = new PlayerListPacket.Entry(session.getAuthData().getUUID()); - copy.setName(updatedEntry.getName()); - copy.setEntityId(updatedEntry.getEntityId()); - copy.setSkin(updatedEntry.getSkin()); - copy.setXuid(updatedEntry.getXuid()); - copy.setPlatformChatId(updatedEntry.getPlatformChatId()); - copy.setTeacher(updatedEntry.isTeacher()); - updatedEntry = copy; - } + 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); - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - - if(entity.getUuid().equals(session.getPlayerEntity().getUuid())) { - session.fetchOurSkin(updatedEntry); - } } } } catch (Exception e) { diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index d8961270..6ccaa306 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -94,6 +94,10 @@ show-cooldown: true # Geyser has direct access to the server itself. cache-chunks: false +# Specify how many days images will be cached to disk to save downloading them from the internet. +# A value of 0 is disabled. (Default: 0) +cache-images: 0 + # Bedrock prevents building and displaying blocks above Y127 in the Nether - # enabling this config option works around that by changing the Nether dimension ID # to the End ID. The main downside to this is that the sky will resemble that of