diff --git a/.gitignore b/.gitignore
index 88b8bc730..c4c878af4 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 0d474d990..1f4562258 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 55cd71164..c43d9eaba 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 279aca544..734fcca67 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 ff6101b32..3d5c098ae 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 2ea45a495..0ba2a689f 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 eb8bf967e..b6a766a3b 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 94322db9b..4d9933ff5 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 d6eaf9343..3873db3cc 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 fe4dc905c..52b273513 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 8728547fc..a0bd5bc2b 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 dd9f48d6a..357e870f6 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 b861f64c4..05465c46b 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 9a6924075..310e5f9d7 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 0580fcffd..a8591cd7f 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 87da2d00c..c1ad7409a 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 620e2b8a5..18fd6614e 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 638627684..0d4f73715 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 f387daecf..10b2ba9ad 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 5a722953a..3ee174d7a 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 827e4c7f4..8d7d59a89 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 323f57f07..453e08445 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 000000000..83f1a7783
--- /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 325e68609..326012277 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 0b7b5c5cf..38369d6c8 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 000000000..48feb1c18
--- /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 6d83da0ad..cb51e2f3b 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 61e23470d..de6796a26 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 0b4958950..36cdbc422 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 000000000..89e9fe67b
--- /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 36507db08..352a0b0f9 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 225885769..1d5e4073d 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 d8961270e..6ccaa3065 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