Merge branch 'master' of https://github.com/GeyserMC/Geyser into feature/1.16.2

This commit is contained in:
DoctorMacc 2020-08-11 09:07:23 -04:00
commit 2dc71382e7
No known key found for this signature in database
GPG key ID: 6D6E7E059F186DB4
47 changed files with 890 additions and 237 deletions

1
.gitignore vendored
View file

@ -241,3 +241,4 @@ config.yml
logs/ logs/
public-key.pem public-key.pem
locales/ locales/
cache/

View file

@ -39,6 +39,9 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set
- [ ] Beacon - [ ] Beacon
- [ ] Cartography Table - [ ] Cartography Table
- [ ] Stonecutter - [ ] Stonecutter
- [ ] Command Block
- [ ] Structure Block
- [ ] Horse Inventory
- Some Entity Flags - Some Entity Flags
## Compiling ## Compiling

View file

@ -25,17 +25,19 @@
package org.geysermc.platform.spigot.world; package org.geysermc.platform.spigot.world;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.geysermc.connector.network.session.GeyserSession; 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.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_13_1to1_13.Protocol1_13_1To1_13;
import us.myles.ViaVersion.protocols.protocol1_16_2to1_16_1.data.MappingData; import us.myles.ViaVersion.protocols.protocol1_16_2to1_16_1.data.MappingData;
@AllArgsConstructor @AllArgsConstructor
public class GeyserSpigotWorldManager extends WorldManager { public class GeyserSpigotWorldManager extends GeyserWorldManager {
private final boolean isLegacy; private final boolean isLegacy;
// You need ViaVersion to connect to an older server with Geyser. // You need ViaVersion to connect to an older server with Geyser.
@ -70,4 +72,19 @@ public class GeyserSpigotWorldManager extends WorldManager {
return BlockTranslator.AIR; 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);
}
} }

View file

@ -149,6 +149,11 @@ public class GeyserSpongeConfiguration implements GeyserConfiguration {
return node.getNode("cache-chunks").getBoolean(false); return node.getNode("cache-chunks").getBoolean(false);
} }
@Override
public int getCacheImages() {
return node.getNode("cache-skins").getInt(0);
}
@Override @Override
public boolean isAboveBedrockNetherBuilding() { public boolean isAboveBedrockNetherBuilding() {
return node.getNode("above-bedrock-nether-building").getBoolean(false); return node.getNode("above-bedrock-nether-building").getBoolean(false);

View file

@ -96,6 +96,12 @@
<version>8.3.1</version> <version>8.3.1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>com.nukkitx.fastutil</groupId>
<artifactId>fastutil-object-object-maps</artifactId>
<version>8.3.1</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>

View file

@ -36,6 +36,9 @@ public interface GeyserLogger {
/** /**
* Logs a severe message and an exception to console * 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); void severe(String message, Throwable error);
@ -48,6 +51,9 @@ public interface GeyserLogger {
/** /**
* Logs an error message and an exception to console * 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); void error(String message, Throwable error);

View file

@ -30,14 +30,14 @@ import org.geysermc.connector.ping.IGeyserPingPassthrough;
import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.configuration.GeyserConfiguration;
import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.GeyserLogger;
import org.geysermc.connector.command.CommandManager; 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 org.geysermc.connector.network.translators.world.WorldManager;
import java.nio.file.Path; import java.nio.file.Path;
public interface GeyserBootstrap { public interface GeyserBootstrap {
CachedChunkManager DEFAULT_CHUNK_MANAGER = new CachedChunkManager(); GeyserWorldManager DEFAULT_CHUNK_MANAGER = new GeyserWorldManager();
/** /**
* Called when the GeyserBootstrap is enabled * Called when the GeyserBootstrap is enabled

View file

@ -77,6 +77,8 @@ public interface GeyserConfiguration {
boolean isCacheChunks(); boolean isCacheChunks();
int getCacheImages();
IMetricsInfo getMetrics(); IMetricsInfo getMetrics();
interface IBedrockConfiguration { interface IBedrockConfiguration {

View file

@ -87,6 +87,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("cache-chunks") @JsonProperty("cache-chunks")
private boolean cacheChunks; private boolean cacheChunks;
@JsonProperty("cache-images")
private int cacheImages = 0;
@JsonProperty("above-bedrock-nether-building") @JsonProperty("above-bedrock-nether-building")
private boolean aboveBedrockNetherBuilding; private boolean aboveBedrockNetherBuilding;

View file

@ -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.mc.protocol.data.message.TextMessage;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3f; 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.AttributeData;
import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission; 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 com.nukkitx.protocol.bedrock.packet.*;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.attribute.Attribute; import org.geysermc.connector.entity.attribute.Attribute;
import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.entity.attribute.AttributeType;
import org.geysermc.connector.entity.type.EntityType; 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.scoreboard.Team;
import org.geysermc.connector.utils.AttributeUtils; import org.geysermc.connector.utils.AttributeUtils;
import org.geysermc.connector.utils.MessageUtils; import org.geysermc.connector.utils.MessageUtils;
import org.geysermc.connector.utils.SkinUtils;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Getter @Setter @Getter @Setter
@ -61,7 +57,7 @@ public class PlayerEntity extends LivingEntity {
private UUID uuid; private UUID uuid;
private String username; private String username;
private long lastSkinUpdate = -1; private long lastSkinUpdate = -1;
private boolean playerList = true; private boolean playerList = true; // Player is in the player list
private final EntityEffectCache effectCache; private final EntityEffectCache effectCache;
private Entity leftParrot; private Entity leftParrot;
@ -97,7 +93,7 @@ public class PlayerEntity extends LivingEntity {
addPlayerPacket.setMotion(motion); addPlayerPacket.setMotion(motion);
addPlayerPacket.setHand(hand); addPlayerPacket.setHand(hand);
addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.NORMAL); addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.NORMAL);
addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.VISITOR); addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
addPlayerPacket.setDeviceId(""); addPlayerPacket.setDeviceId("");
addPlayerPacket.setPlatformChatId(""); addPlayerPacket.setPlatformChatId("");
addPlayerPacket.getMetadata().putAll(metadata); addPlayerPacket.getMetadata().putAll(metadata);
@ -117,30 +113,12 @@ public class PlayerEntity extends LivingEntity {
public void sendPlayer(GeyserSession session) { public void sendPlayer(GeyserSession session) {
if(session.getEntityCache().getPlayerEntity(uuid) == null) if(session.getEntityCache().getPlayerEntity(uuid) == null)
return; 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) { if (session.getUpstream().isInitialized() && session.getEntityCache().getEntityByGeyserId(geyserId) == null) {
session.getEntityCache().spawnEntity(this); session.getEntityCache().spawnEntity(this);
} else { } else {
spawnEntity(session); 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 @Override
@ -232,7 +210,7 @@ public class PlayerEntity extends LivingEntity {
if (entityMetadata.getId() == 2) { if (entityMetadata.getId() == 2) {
// System.out.println(session.getScoreboardCache().getScoreboard().getObjectives().keySet()); // 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 name " + team.getName());
// session.getConnector().getLogger().info("team entities " + team.getEntities()); // session.getConnector().getLogger().info("team entities " + team.getEntities());
} }
@ -241,7 +219,7 @@ public class PlayerEntity extends LivingEntity {
if (name != null) { if (name != null) {
username = MessageUtils.getBedrockMessage(name); username = MessageUtils.getBedrockMessage(name);
} }
Team team = session.getScoreboardCache().getScoreboard().getTeamFor(username); Team team = session.getWorldCache().getScoreboard().getTeamFor(username);
if (team != null) { if (team != null) {
// session.getConnector().getLogger().info("team name es " + team.getName() + " with prefix " + team.getPrefix() + " and suffix " + team.getSuffix()); // 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()); metadata.put(EntityData.NAMETAG, team.getPrefix() + MessageUtils.toChatColor(team.getColor()) + username + team.getSuffix());

View file

@ -53,14 +53,21 @@ public class ShulkerEntity extends GolemEntity {
metadata.put(EntityData.SHULKER_ATTACH_POS, Vector3i.from(position.getX(), position.getY(), position.getZ())); 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) { if (entityMetadata.getId() == 17) {
// int height = (byte) entityMetadata.getValue(); int height = (byte) entityMetadata.getValue();
// metadata.put(EntityData.SHULKER_PEAK_HEIGHT, height); metadata.put(EntityData.SHULKER_PEEK_ID, height);
// } }
if (entityMetadata.getId() == 18) { if (entityMetadata.getId() == 18) {
int color = Math.abs((byte) entityMetadata.getValue() - 15); byte color = (byte) entityMetadata.getValue();
metadata.put(EntityData.VARIANT, color); 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); super.updateBedrockMetadata(entityMetadata, session);
} }

View file

@ -34,6 +34,7 @@ import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslatorRegistry; import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
import org.geysermc.connector.utils.LoginEncryptionUtils; import org.geysermc.connector.utils.LoginEncryptionUtils;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.utils.SettingsUtils;
public class UpstreamPacketHandler extends LoggingPacketHandler { public class UpstreamPacketHandler extends LoggingPacketHandler {
@ -91,6 +92,10 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
@Override @Override
public boolean handle(ModalFormResponsePacket packet) { 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()); return LoginEncryptionUtils.authenticateFromForm(session, connector, packet.getFormId(), packet.getFormData());
} }

View file

@ -46,6 +46,7 @@ import com.nukkitx.math.vector.*;
import com.nukkitx.protocol.bedrock.BedrockPacket; import com.nukkitx.protocol.bedrock.BedrockPacket;
import com.nukkitx.protocol.bedrock.BedrockServerSession; import com.nukkitx.protocol.bedrock.BedrockServerSession;
import com.nukkitx.protocol.bedrock.data.*; import com.nukkitx.protocol.bedrock.data.*;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.packet.*;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; 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 it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.geysermc.common.window.CustomFormWindow;
import org.geysermc.common.window.FormWindow; import org.geysermc.common.window.FormWindow;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.CommandSender;
@ -79,9 +81,7 @@ import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Getter @Getter
@ -102,7 +102,7 @@ public class GeyserSession implements CommandSender {
private ChunkCache chunkCache; private ChunkCache chunkCache;
private EntityCache entityCache; private EntityCache entityCache;
private InventoryCache inventoryCache; private InventoryCache inventoryCache;
private ScoreboardCache scoreboardCache; private WorldCache worldCache;
private WindowCache windowCache; private WindowCache windowCache;
@Setter @Setter
private TeleportCache teleportCache; private TeleportCache teleportCache;
@ -191,6 +191,41 @@ public class GeyserSession implements CommandSender {
private MinecraftProtocol protocol; 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) { public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) {
this.connector = connector; this.connector = connector;
this.upstream = new UpstreamSession(bedrockServerSession); this.upstream = new UpstreamSession(bedrockServerSession);
@ -198,7 +233,7 @@ public class GeyserSession implements CommandSender {
this.chunkCache = new ChunkCache(this); this.chunkCache = new ChunkCache(this);
this.entityCache = new EntityCache(this); this.entityCache = new EntityCache(this);
this.inventoryCache = new InventoryCache(this); this.inventoryCache = new InventoryCache(this);
this.scoreboardCache = new ScoreboardCache(this); this.worldCache = new WorldCache(this);
this.windowCache = new WindowCache(this); this.windowCache = new WindowCache(this);
this.playerEntity = new PlayerEntity(new GameProfile(UUID.randomUUID(), "unknown"), 1, 1, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO); 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)); attributes.add(new AttributeData("minecraft:movement", 0.0f, 1024f, 0.1f, 0.1f));
attributesPacket.setAttributes(attributes); attributesPacket.setAttributes(attributes);
upstream.sendPacket(attributesPacket); upstream.sendPacket(attributesPacket);
}
public void fetchOurSkin(PlayerListPacket.Entry entry) { // Only allow the server to send health information
PlayerSkinPacket playerSkinPacket = new PlayerSkinPacket(); // Setting this to false allows natural regeneration to work false but doesn't break it being true
playerSkinPacket.setUuid(authData.getUUID()); GameRulesChangedPacket gamerulePacket = new GameRulesChangedPacket();
playerSkinPacket.setSkin(entry.getSkin()); gamerulePacket.getGameRules().add(new GameRuleData<>("naturalregeneration", false));
playerSkinPacket.setOldSkinName("OldName"); upstream.sendPacket(gamerulePacket);
playerSkinPacket.setNewSkinName("NewName");
playerSkinPacket.setTrustedSkin(true);
upstream.sendPacket(playerSkinPacket);
getConnector().getLogger().debug("Sending skin for " + playerEntity.getUsername() + " " + authData.getUUID());
} }
public void login() { public void login() {
@ -445,7 +475,7 @@ public class GeyserSession implements CommandSender {
this.chunkCache = null; this.chunkCache = null;
this.entityCache = null; this.entityCache = null;
this.scoreboardCache = null; this.worldCache = null;
this.inventoryCache = null; this.inventoryCache = null;
this.windowCache = 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"); 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<AdventureSetting> 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);
}
} }

View file

@ -25,7 +25,9 @@
package org.geysermc.connector.network.session.cache; package org.geysermc.connector.network.session.cache;
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Objective;
import org.geysermc.connector.scoreboard.Scoreboard; import org.geysermc.connector.scoreboard.Scoreboard;
@ -33,11 +35,18 @@ import org.geysermc.connector.scoreboard.Scoreboard;
import java.util.Collection; import java.util.Collection;
@Getter @Getter
public class ScoreboardCache { public class WorldCache {
private GeyserSession session; private GeyserSession session;
@Setter
private Difficulty difficulty = Difficulty.EASY;
private boolean showCoordinates = true;
private Scoreboard scoreboard; private Scoreboard scoreboard;
public ScoreboardCache(GeyserSession session) { public WorldCache(GeyserSession session) {
this.session = session; this.session = session;
this.scoreboard = new Scoreboard(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);
}
} }

View file

@ -23,15 +23,25 @@
* @link https://github.com/GeyserMC/Geyser * @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.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<ServerSettingsRequestPacket> {
@Override @Override
public int getBlockAt(GeyserSession session, int x, int y, int z) { public void translate(ServerSettingsRequestPacket packet, GeyserSession session) {
return session.getChunkCache().getBlockAt(new Position(x, y, z)); SettingsUtils.buildForm(session);
ServerSettingsResponsePacket serverSettingsResponsePacket = new ServerSettingsResponsePacket();
serverSettingsResponsePacket.setFormData(session.getSettingsForm().getJSONData());
serverSettingsResponsePacket.setFormId(SettingsUtils.SETTINGS_FORM_ID);
session.sendUpstreamPacket(serverSettingsResponsePacket);
} }
} }

View file

@ -44,8 +44,8 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat
for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) { for (PlayerEntity entity : session.getEntityCache().getEntitiesByType(PlayerEntity.class)) {
if (!entity.isValid()) { if (!entity.isValid()) {
// async skin loading SkinUtils.requestAndHandleSkinAndCape(entity, session, null);
SkinUtils.requestAndHandleSkinAndCape(entity, session, skinAndCape -> entity.sendPlayer(session)); entity.sendPlayer(session);
} }
} }
} }

View file

@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser * @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.VillagerTrade;
import com.github.steveice10.mc.protocol.data.game.window.WindowType; import com.github.steveice10.mc.protocol.data.game.window.WindowType;

View file

@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser * @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.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
@ -116,6 +116,18 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
// Handled in BedrockInventoryTransactionTranslator // Handled in BedrockInventoryTransactionTranslator
break; break;
case START_BREAK: case START_BREAK:
if (session.getConnector().getConfig().isCacheChunks()) {
if (packet.getFace() == BlockFace.UP.ordinal()) {
int blockUp = session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition().add(0, 1, 0));
String identifier = BlockTranslator.getJavaIdBlockMap().inverse().get(blockUp);
if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) {
ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(packet.getBlockPosition().getX(),
packet.getBlockPosition().getY() + 1, packet.getBlockPosition().getZ()), BlockFace.values()[packet.getFace()]);
session.sendDownstreamPacket(startBreakingPacket);
break;
}
}
}
ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(packet.getBlockPosition().getX(), ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(packet.getBlockPosition().getX(),
packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getFace()]); packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getFace()]);
session.sendDownstreamPacket(startBreakingPacket); session.sendDownstreamPacket(startBreakingPacket);

View file

@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser * @link https://github.com/GeyserMC/Geyser
*/ */
package org.geysermc.connector.network.translators.bedrock; package org.geysermc.connector.network.translators.bedrock.entity.player;
import com.nukkitx.protocol.bedrock.packet.EmotePacket; import com.nukkitx.protocol.bedrock.packet.EmotePacket;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;

View file

@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser * @link https://github.com/GeyserMC/Geyser
*/ */
package org.geysermc.connector.network.translators.bedrock; package org.geysermc.connector.network.translators.bedrock.entity.player;
import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap; import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;

View file

@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser * @link https://github.com/GeyserMC/Geyser
*/ */
package org.geysermc.connector.network.translators.bedrock; package org.geysermc.connector.network.translators.bedrock.entity.player;
import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3d;
import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.common.ChatColor;

View file

@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser * @link https://github.com/GeyserMC/Geyser
*/ */
package org.geysermc.connector.network.translators.bedrock; package org.geysermc.connector.network.translators.bedrock.world;
import com.nukkitx.protocol.bedrock.data.SoundEvent; import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket; import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;

View file

@ -214,13 +214,6 @@ public class ItemRegistry {
return itemEntry; return itemEntry;
} }
} }
// If item find was unsuccessful first time, we try again while ignoring damage
// Fixes piston, sticky pistons, dispensers and droppers turning into air from creative inventory
for (ItemEntry itemEntry : ITEM_ENTRIES.values()) {
if (itemEntry.getBedrockId() == data.getId()) {
return itemEntry;
}
}
// This will hide the message when the player clicks with an empty hand // This will hide the message when the player clicks with an empty hand
if (data.getId() != 0 && data.getDamage() != 0) { if (data.getId() != 0 && data.getDamage() != 0) {

View file

@ -40,5 +40,7 @@ public class JavaDifficultyTranslator extends PacketTranslator<ServerDifficultyP
SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket(); SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket();
setDifficultyPacket.setDifficulty(packet.getDifficulty().ordinal()); setDifficultyPacket.setDifficulty(packet.getDifficulty().ordinal());
session.sendUpstreamPacket(setDifficultyPacket); session.sendUpstreamPacket(setDifficultyPacket);
session.getWorldCache().setDifficulty(packet.getDifficulty());
} }
} }

View file

@ -57,7 +57,7 @@ public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacke
DimensionUtils.switchDimension(session, fakeDim); DimensionUtils.switchDimension(session, fakeDim);
DimensionUtils.switchDimension(session, newDimension); DimensionUtils.switchDimension(session, newDimension);
session.getScoreboardCache().removeScoreboard(); session.getWorldCache().removeScoreboard();
} }
AdventureSettingsPacket bedrockPacket = new AdventureSettingsPacket(); AdventureSettingsPacket bedrockPacket = new AdventureSettingsPacket();

View file

@ -31,6 +31,7 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
@ -51,6 +52,33 @@ public class JavaEntityStatusTranslator extends PacketTranslator<ServerEntitySta
EntityEventPacket entityEventPacket = new EntityEventPacket(); EntityEventPacket entityEventPacket = new EntityEventPacket();
entityEventPacket.setRuntimeEntityId(entity.getGeyserId()); entityEventPacket.setRuntimeEntityId(entity.getGeyserId());
switch (packet.getStatus()) { switch (packet.getStatus()) {
case PLAYER_ENABLE_REDUCED_DEBUG:
session.setReducedDebugInfo(true);
return;
case PLAYER_DISABLE_REDUCED_DEBUG:
session.setReducedDebugInfo(false);
return;
case PLAYER_OP_PERMISSION_LEVEL_0:
session.setOpPermissionLevel(0);
session.sendAdventureSettings();
return;
case PLAYER_OP_PERMISSION_LEVEL_1:
session.setOpPermissionLevel(1);
session.sendAdventureSettings();
return;
case PLAYER_OP_PERMISSION_LEVEL_2:
session.setOpPermissionLevel(2);
session.sendAdventureSettings();
return;
case PLAYER_OP_PERMISSION_LEVEL_3:
session.setOpPermissionLevel(3);
session.sendAdventureSettings();
return;
case PLAYER_OP_PERMISSION_LEVEL_4:
session.setOpPermissionLevel(4);
session.sendAdventureSettings();
return;
// EntityEventType.HURT sends extra data depending on the type of damage. However this appears to have no visual changes // EntityEventType.HURT sends extra data depending on the type of damage. However this appears to have no visual changes
case LIVING_BURN: case LIVING_BURN:
case LIVING_DROWN: case LIVING_DROWN:

View file

@ -32,6 +32,7 @@ import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.packet.AdventureSettingsPacket; import com.nukkitx.protocol.bedrock.packet.AdventureSettingsPacket;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.geysermc.connector.entity.Entity; 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.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
@ -43,24 +44,12 @@ public class JavaPlayerAbilitiesTranslator extends PacketTranslator<ServerPlayer
@Override @Override
public void translate(ServerPlayerAbilitiesPacket packet, GeyserSession session) { public void translate(ServerPlayerAbilitiesPacket packet, GeyserSession session) {
Entity entity = session.getPlayerEntity(); PlayerEntity entity = session.getPlayerEntity();
if (entity == null) if (entity == null)
return; return;
Set<AdventureSetting> playerFlags = new ObjectOpenHashSet<>(); session.setCanFly(packet.isCanFly());
playerFlags.add(AdventureSetting.AUTO_JUMP); session.setFlying(packet.isFlying());
if (packet.isCanFly()) session.sendAdventureSettings();
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);
} }
} }

View file

@ -82,18 +82,7 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
playerEntity.setPlayerList(true); playerEntity.setPlayerList(true);
playerEntity.setValid(true); playerEntity.setValid(true);
PlayerListPacket.Entry playerListEntry = SkinUtils.buildCachedEntry(entry.getProfile(), playerEntity.getGeyserId()); PlayerListPacket.Entry playerListEntry = SkinUtils.buildCachedEntry(session, entry.getProfile(), playerEntity.getGeyserId());
if (self) {
// Copy the entry with our identity instead.
PlayerListPacket.Entry copy = new PlayerListPacket.Entry(session.getAuthData().getUUID());
copy.setName(playerListEntry.getName());
copy.setEntityId(playerListEntry.getEntityId());
copy.setSkin(playerListEntry.getSkin());
copy.setXuid(playerListEntry.getXuid());
copy.setPlatformChatId(playerListEntry.getPlatformChatId());
copy.setTeacher(playerListEntry.isTeacher());
playerListEntry = copy;
}
translate.getEntries().add(playerListEntry); translate.getEntries().add(playerListEntry);
break; break;
@ -103,15 +92,20 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
// remove from tablist but player entity is still there // remove from tablist but player entity is still there
entity.setPlayerList(false); entity.setPlayerList(false);
} else { } else {
// just remove it from caching
if (entity == null) { if (entity == null) {
// just remove it from caching
session.getEntityCache().removePlayerEntity(entry.getProfile().getId()); session.getEntityCache().removePlayerEntity(entry.getProfile().getId());
} else { } else {
entity.setPlayerList(false); entity.setPlayerList(false);
session.getEntityCache().removeEntity(entity, false); session.getEntityCache().removeEntity(entity, false);
} }
} }
translate.getEntries().add(new PlayerListPacket.Entry(entry.getProfile().getId())); if (entity == session.getPlayerEntity()) {
// If removing ourself we use our AuthData UUID
translate.getEntries().add(new PlayerListPacket.Entry(session.getAuthData().getUUID()));
} else {
translate.getEntries().add(new PlayerListPacket.Entry(entry.getProfile().getId()));
}
break; break;
} }
} }

View file

@ -54,9 +54,9 @@ public class JavaSpawnPlayerTranslator extends PacketTranslator<ServerSpawnPlaye
entity.setRotation(rotation); entity.setRotation(rotation);
session.getEntityCache().cacheEntity(entity); session.getEntityCache().cacheEntity(entity);
// async skin loading
if (session.getUpstream().isInitialized()) { if (session.getUpstream().isInitialized()) {
SkinUtils.requestAndHandleSkinAndCape(entity, session, skinAndCape -> entity.sendPlayer(session)); entity.sendPlayer(session);
SkinUtils.requestAndHandleSkinAndCape(entity, session, null);
} }
} }
} }

View file

@ -36,7 +36,7 @@ public class JavaDisplayScoreboardTranslator extends PacketTranslator<ServerDisp
@Override @Override
public void translate(ServerDisplayScoreboardPacket packet, GeyserSession session) { public void translate(ServerDisplayScoreboardPacket packet, GeyserSession session) {
session.getScoreboardCache().getScoreboard().registerNewObjective( session.getWorldCache().getScoreboard().registerNewObjective(
packet.getName(), packet.getPosition() packet.getName(), packet.getPosition()
); );
} }

View file

@ -26,7 +26,7 @@
package org.geysermc.connector.network.translators.java.scoreboard; package org.geysermc.connector.network.translators.java.scoreboard;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.cache.ScoreboardCache; import org.geysermc.connector.network.session.cache.WorldCache;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Objective;
@ -41,7 +41,7 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerSc
@Override @Override
public void translate(ServerScoreboardObjectivePacket packet, GeyserSession session) { public void translate(ServerScoreboardObjectivePacket packet, GeyserSession session) {
ScoreboardCache cache = session.getScoreboardCache(); WorldCache cache = session.getWorldCache();
Scoreboard scoreboard = cache.getScoreboard(); Scoreboard scoreboard = cache.getScoreboard();
Objective objective = scoreboard.getObjective(packet.getName()); Objective objective = scoreboard.getObjective(packet.getName());

View file

@ -47,7 +47,7 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
public void translate(ServerTeamPacket packet, GeyserSession session) { public void translate(ServerTeamPacket packet, GeyserSession session) {
GeyserConnector.getInstance().getLogger().debug("Team packet " + packet.getTeamName() + " " + packet.getAction() + " " + Arrays.toString(packet.getPlayers())); 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()); Team team = scoreboard.getTeam(packet.getTeamName());
switch (packet.getAction()) { switch (packet.getAction()) {
case CREATE: case CREATE:
@ -65,21 +65,21 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
.setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode())) .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode()))
.setUpdateType(UpdateType.UPDATE); .setUpdateType(UpdateType.UPDATE);
} else { } 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; break;
case ADD_PLAYER: case ADD_PLAYER:
if(team != null){ if (team != null) {
team.addEntities(packet.getPlayers()); team.addEntities(packet.getPlayers());
} else { } 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; break;
case REMOVE_PLAYER: case REMOVE_PLAYER:
if(team != null){ if (team != null) {
team.removeEntities(packet.getPlayers()); team.removeEntities(packet.getPlayers());
} else { } 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; break;
case REMOVE: case REMOVE:

View file

@ -42,7 +42,7 @@ public class JavaUpdateScoreTranslator extends PacketTranslator<ServerUpdateScor
@Override @Override
public void translate(ServerUpdateScorePacket packet, GeyserSession session) { public void translate(ServerUpdateScorePacket packet, GeyserSession session) {
try { try {
Scoreboard scoreboard = session.getScoreboardCache().getScoreboard(); Scoreboard scoreboard = session.getWorldCache().getScoreboard();
Objective objective = scoreboard.getObjective(packet.getObjective()); Objective objective = scoreboard.getObjective(packet.getObjective());
if (objective == null && packet.getAction() != ScoreboardAction.REMOVE) { if (objective == null && packet.getAction() != ScoreboardAction.REMOVE) {

View file

@ -41,6 +41,7 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.packet.*;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.geysermc.connector.entity.Entity; 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.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
@ -55,7 +56,7 @@ public class JavaNotifyClientTranslator extends PacketTranslator<ServerNotifyCli
@Override @Override
public void translate(ServerNotifyClientPacket packet, GeyserSession session) { public void translate(ServerNotifyClientPacket packet, GeyserSession session) {
Entity entity = session.getPlayerEntity(); PlayerEntity entity = session.getPlayerEntity();
if (entity == null) if (entity == null)
return; return;
@ -75,39 +76,17 @@ public class JavaNotifyClientTranslator extends PacketTranslator<ServerNotifyCli
session.sendUpstreamPacket(stopRainPacket); session.sendUpstreamPacket(stopRainPacket);
break; break;
case CHANGE_GAMEMODE: case CHANGE_GAMEMODE:
Set<AdventureSetting> playerFlags = new ObjectOpenHashSet<>();
GameMode gameMode = (GameMode) packet.getValue(); GameMode gameMode = (GameMode) packet.getValue();
if (gameMode == GameMode.ADVENTURE)
playerFlags.add(AdventureSetting.WORLD_IMMUTABLE);
if (gameMode == GameMode.CREATIVE) session.setNoClip(gameMode == GameMode.SPECTATOR);
playerFlags.add(AdventureSetting.MAY_FLY); session.setWorldImmutable(gameMode == GameMode.ADVENTURE || gameMode == GameMode.SPECTATOR);
session.sendAdventureSettings();
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);
SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket(); SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket();
playerGameTypePacket.setGamemode(gameMode.ordinal()); playerGameTypePacket.setGamemode(gameMode.ordinal());
session.sendUpstreamPacket(playerGameTypePacket); session.sendUpstreamPacket(playerGameTypePacket);
session.setGameMode(gameMode); 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 // Update the crafting grid to add/remove barriers for creative inventory
PlayerInventoryTranslator.updateCraftingGrid(session, session.getInventory()); PlayerInventoryTranslator.updateCraftingGrid(session, session.getInventory());
break; break;

View file

@ -78,7 +78,7 @@ public class JavaSpawnParticleTranslator extends PacketTranslator<ServerSpawnPar
int r = (int) (data.getRed()*255); int r = (int) (data.getRed()*255);
int g = (int) (data.getGreen()*255); int g = (int) (data.getGreen()*255);
int b = (int) (data.getBlue()*255); int b = (int) (data.getBlue()*255);
particle.setType(LevelEventType.PARTICLE_FALLING_DUST); particle.setType(LevelEventType.PARTICLE_REDSTONE);
particle.setData(((0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff)); particle.setData(((0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff));
particle.setPosition(Vector3f.from(packet.getX(), packet.getY(), packet.getZ())); particle.setPosition(Vector3f.from(packet.getX(), packet.getY(), packet.getZ()));
session.sendUpstreamPacket(particle); session.sendUpstreamPacket(particle);

View file

@ -67,9 +67,7 @@ public class JavaUpdateTimeTranslator extends PacketTranslator<ServerUpdateTimeP
} }
private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) { private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) {
GameRulesChangedPacket gameRulesChangedPacket = new GameRulesChangedPacket(); session.sendGameRule("dodaylightcycle", doCycle);
gameRulesChangedPacket.getGameRules().add(new GameRuleData<>("dodaylightcycle", doCycle));
session.sendUpstreamPacket(gameRulesChangedPacket);
} }
} }

View file

@ -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<String, String> 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;
}
}

View file

@ -26,8 +26,11 @@
package org.geysermc.connector.network.translators.world; 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.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 com.nukkitx.math.vector.Vector3i;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.GameRule;
/** /**
* Class that manages or retrieves various information * Class that manages or retrieves various information
@ -70,4 +73,56 @@ public abstract class WorldManager {
* @return the block state at the specified location * @return the block state at the specified location
*/ */
public abstract int getBlockAt(GeyserSession session, int x, int y, int z); 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);
} }

View file

@ -42,6 +42,7 @@ public class FileUtils {
* *
* @param src File to load * @param src File to load
* @param valueType Class to load file into * @param valueType Class to load file into
* @param <T> the type
* @return The data as the given class * @return The data as the given class
* @throws IOException if the config could not be loaded * @throws IOException if the config could not be loaded
*/ */

View file

@ -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;
}
}

View file

@ -136,6 +136,9 @@ public class InventoryUtils {
/** /**
* Returns a barrier block with custom name and lore to explain why * Returns a barrier block with custom name and lore to explain why
* part of the inventory is unusable. * part of the inventory is unusable.
*
* @param description the description
* @return the unusable space block
*/ */
public static ItemData createUnusableSpaceBlock(String description) { public static ItemData createUnusableSpaceBlock(String description) {
NbtMapBuilder root = NbtMap.builder(); NbtMapBuilder root = NbtMap.builder();

View file

@ -122,7 +122,7 @@ public class LanguageUtils {
formatString = key; formatString = key;
} }
return MessageFormat.format(formatString.replace("&", "\u00a7"), values); return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values);
} }
/** /**

View file

@ -95,7 +95,7 @@ public class MessageUtils {
* @param messages A {@link List} of {@link Message} to parse * @param messages A {@link List} of {@link Message} to parse
* @param locale A locale loaded to get the message for * @param locale A locale loaded to get the message for
* @param parent A {@link Message} to use as the parent (can be null) * @param parent A {@link Message} to use as the parent (can be null)
* @return * @return the translation parameters
*/ */
public static List<String> getTranslationParams(List<Message> messages, String locale, Message parent) { public static List<String> getTranslationParams(List<Message> messages, String locale, Message parent) {
List<String> strings = new ArrayList<>(); List<String> strings = new ArrayList<>();
@ -160,10 +160,10 @@ public class MessageUtils {
* Translate a given {@link TranslationMessage} to the given locale * Translate a given {@link TranslationMessage} to the given locale
* *
* @param message The {@link Message} to send * @param message The {@link Message} to send
* @param locale * @param locale the locale
* @param shouldTranslate * @param shouldTranslate if the message should be translated
* @param parent * @param parent the parent message
* @return * @return the given translation message translated from the given locale
*/ */
public static String getTranslatedBedrockMessage(Message message, String locale, boolean shouldTranslate, Message parent) { public static String getTranslatedBedrockMessage(Message message, String locale, boolean shouldTranslate, Message parent) {
JsonParser parser = new JsonParser(); JsonParser parser = new JsonParser();

View file

@ -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;
}
}

View file

@ -27,6 +27,8 @@ package org.geysermc.connector.utils;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -40,9 +42,11 @@ import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.*; 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 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 byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin();
public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN); public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN);
private static Map<UUID, Skin> cachedSkins = new ConcurrentHashMap<>(); private static final Cache<String, Skin> cachedSkins = CacheBuilder.newBuilder()
private static Map<UUID, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>(); .expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true); public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true);
private static Map<String, Cape> cachedCapes = new ConcurrentHashMap<>(); private static final Cache<String, Cape> cachedCapes = CacheBuilder.newBuilder()
private static Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>(); .expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false); public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false);
private static Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>(); private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars();
public static String EARS_GEOMETRY; public static String EARS_GEOMETRY;
public static String EARS_GEOMETRY_SLIM; public static String EARS_GEOMETRY_SLIM;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes
static { static {
/* Load in the normal ears geometry */ /* Load in the normal ears geometry */
@ -102,22 +110,44 @@ public class SkinProvider {
} }
EARS_GEOMETRY_SLIM = earsDataBuilder.toString(); EARS_GEOMETRY_SLIM = earsDataBuilder.toString();
}
public static boolean hasSkinCached(UUID uuid) { // Schedule Daily Image Expiry if we are caching them
return cachedSkins.containsKey(uuid); 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) { public static boolean hasCapeCached(String capeUrl) {
return cachedCapes.containsKey(capeUrl); return cachedCapes.getIfPresent(capeUrl) != null;
} }
public static Skin getCachedSkin(UUID uuid) { public static Skin getCachedSkin(String skinUrl) {
return cachedSkins.getOrDefault(uuid, EMPTY_SKIN); Skin skin = cachedSkins.getIfPresent(skinUrl);
return skin != null ? skin : EMPTY_SKIN;
} }
public static Cape getCachedCape(String capeUrl) { 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<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) { public static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
@ -137,28 +167,26 @@ public class SkinProvider {
public static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) { public static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN); 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()) { Skin cachedSkin = cachedSkins.getIfPresent(textureUrl);
// no need to update, still cached if (cachedSkin != null) {
return CompletableFuture.completedFuture(cachedSkins.get(playerId)); return CompletableFuture.completedFuture(cachedSkin);
} }
CompletableFuture<Skin> future; CompletableFuture<Skin> future;
if (newThread) { if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE) future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((skin, throwable) -> { .whenCompleteAsync((skin, throwable) -> {
if (!cachedSkins.getOrDefault(playerId, EMPTY_SKIN).getTextureUrl().equals(textureUrl)) { skin.updated = true;
skin.updated = true; cachedSkins.put(textureUrl, skin);
cachedSkins.put(playerId, skin); requestedSkins.remove(textureUrl);
}
requestedSkins.remove(skin.getSkinOwner());
}); });
requestedSkins.put(playerId, future); requestedSkins.put(textureUrl, future);
} else { } else {
Skin skin = supplySkin(playerId, textureUrl); Skin skin = supplySkin(playerId, textureUrl);
future = CompletableFuture.completedFuture(skin); future = CompletableFuture.completedFuture(skin);
cachedSkins.put(playerId, skin); cachedSkins.put(textureUrl, skin);
} }
return future; return future;
} }
@ -168,11 +196,9 @@ public class SkinProvider {
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
boolean officialCape = provider == CapeProvider.MINECRAFT; boolean officialCape = provider == CapeProvider.MINECRAFT;
boolean validCache = (System.currentTimeMillis() - CACHE_INTERVAL) < cachedCapes.getOrDefault(capeUrl, EMPTY_CAPE).getRequestedOn(); Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
if (cachedCape != null) {
if ((cachedCapes.containsKey(capeUrl) && officialCape) || validCache) { return CompletableFuture.completedFuture(cachedCape);
// the cape is an official cape (static) or the cape doesn't need a update yet
return CompletableFuture.completedFuture(cachedCapes.get(capeUrl));
} }
CompletableFuture<Cape> future; CompletableFuture<Cape> future;
@ -245,7 +271,10 @@ public class SkinProvider {
} }
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID, boolean newThread) { public static CompletableFuture<Cape> 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); return CompletableFuture.completedFuture(bedrockCape);
} }
@ -256,7 +285,7 @@ public class SkinProvider {
public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) { public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) {
Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false); 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) { public static void storeBedrockCape(UUID playerID, byte[] capeData) {
@ -276,7 +305,7 @@ public class SkinProvider {
* @param skin The skin to cache * @param skin The skin to cache
*/ */
public static void storeEarSkin(UUID playerID, Skin skin) { 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) { private static Skin supplySkin(UUID uuid, String textureUrl) {
byte[] skin = EMPTY_SKIN.getSkinData();
try { 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 } 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) { private static Cape supplyCape(String capeUrl, CapeProvider provider) {
@ -356,11 +386,38 @@ public class SkinProvider {
return existingSkin; return existingSkin;
} }
@SuppressWarnings("ResultOfMethodCallIgnored")
private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception { private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception {
BufferedImage image = downloadImage(imageUrl, provider); BufferedImage image = null;
GeyserConnector.getInstance().getLogger().debug("Downloaded " + imageUrl);
// 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) { if (provider != null) {
while(image.getWidth() > 64) { while(image.getWidth() > 64) {
image = scale(image); image = scale(image);

View file

@ -35,6 +35,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import org.geysermc.connector.common.AuthType; import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.PlayerEntity; import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.auth.BedrockClientData; import org.geysermc.connector.network.session.auth.BedrockClientData;
@ -47,18 +48,21 @@ import java.util.function.Consumer;
public class SkinUtils { 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); GameProfileData data = GameProfileData.from(profile);
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl()); SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl());
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex()); SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.getSkinUrl());
return buildEntryManually( return buildEntryManually(
session,
profile.getId(), profile.getId(),
profile.getName(), profile.getName(),
geyserId, geyserId,
profile.getIdAsString(), skin.getTextureUrl(),
SkinProvider.getCachedSkin(profile.getId()).getSkinData(), skin.getSkinData(),
cape.getCapeId(), cape.getCapeId(),
cape.getCapeData(), cape.getCapeData(),
geometry.getGeometryName(), 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( return buildEntryManually(
session,
profile.getId(), profile.getId(),
profile.getName(), profile.getName(),
geyserId, geyserId,
profile.getIdAsString(), "default",
SkinProvider.STEVE_SKIN, SkinProvider.STEVE_SKIN,
SkinProvider.EMPTY_CAPE.getCapeId(), SkinProvider.EMPTY_CAPE.getCapeId(),
SkinProvider.EMPTY_CAPE.getCapeData(), 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 skinId, byte[] skinData,
String capeId, byte[] capeData, String capeId, byte[] capeData,
String geometryName, String geometryData) { String geometryName, String geometryData) {
SerializedSkin serializedSkin = SerializedSkin.of( SerializedSkin serializedSkin = SerializedSkin.of(
skinId, geometryName, ImageData.of(skinData), Collections.emptyList(), 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.setName(username);
entry.setEntityId(geyserId); entry.setEntityId(geyserId);
entry.setSkin(serializedSkin); entry.setSkin(serializedSkin);
entry.setXuid(""); entry.setXuid(xuid);
entry.setPlatformChatId(""); entry.setPlatformChatId("");
entry.setTeacher(false); entry.setTeacher(false);
entry.setTrustedSkin(true); 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()) { if (session.getUpstream().isInitialized()) {
PlayerListPacket.Entry updatedEntry = buildEntryManually( PlayerListPacket.Entry updatedEntry = buildEntryManually(
entity.getUuid(), session,
entity.getUsername(), entity.getUuid(),
entity.getGeyserId(), entity.getUsername(),
entity.getUuid().toString(), entity.getGeyserId(),
skin.getSkinData(), skin.getTextureUrl(),
cape.getCapeId(), skin.getSkinData(),
cape.getCapeData(), cape.getCapeId(),
geometry.getGeometryName(), cape.getCapeData(),
geometry.getGeometryData() 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(); PlayerListPacket playerRemovePacket = new PlayerListPacket();
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE); playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
playerRemovePacket.getEntries().add(updatedEntry); playerRemovePacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerRemovePacket); 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) { } catch (Exception e) {

View file

@ -94,6 +94,10 @@ show-cooldown: true
# Geyser has direct access to the server itself. # Geyser has direct access to the server itself.
cache-chunks: false 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 - # Bedrock prevents building and displaying blocks above Y127 in the Nether -
# enabling this config option works around that by changing the Nether dimension ID # 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 # to the End ID. The main downside to this is that the sky will resemble that of