Merge remote-tracking branch 'origin/master' into floodgate-2.0

# Conflicts:
#	common/src/main/java/org/geysermc/common/window/CustomFormWindow.java
#	common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java
#	connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
#	connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
#	connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java
#	connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java
This commit is contained in:
Tim203 2020-10-30 01:25:52 +01:00
commit 36419e5931
No known key found for this signature in database
GPG Key ID: 064EE9F5BF7C3EE8
109 changed files with 2559 additions and 804 deletions

3
.gitignore vendored
View File

@ -242,4 +242,5 @@ logs/
public-key.pem
locales/
/cache/
/packs/
/packs/
/dump.json

35
Jenkinsfile vendored
View File

@ -5,7 +5,7 @@ pipeline {
jdk 'Java 8'
}
options {
buildDiscarder(logRotator(artifactNumToKeepStr: '5'))
buildDiscarder(logRotator(artifactNumToKeepStr: '20'))
}
stages {
stage ('Build') {
@ -32,9 +32,40 @@ pipeline {
post {
always {
script {
def changeLogSets = currentBuild.changeSets
def message = "**Changes:**"
if (changeLogSets.size() == 0) {
message += "\n*No changes.*"
} else {
def repositoryUrl = scm.userRemoteConfigs[0].url.replace(".git", "")
def count = 0;
def extra = 0;
for (int i = 0; i < changeLogSets.size(); i++) {
def entries = changeLogSets[i].items
for (int j = 0; j < entries.length; j++) {
if (count <= 10) {
def entry = entries[j]
def commitId = entry.commitId.substring(0, 6)
message += "\n - [`${commitId}`](${repositoryUrl}/commit/${entry.commitId}) ${entry.msg}"
count++
} else {
extra++;
}
}
}
if (extra != 0) {
message += "\n - ${extra} more commits"
}
}
env.changes = message
}
deleteDir()
withCredentials([string(credentialsId: 'geyser-discord-webhook', variable: 'DISCORD_WEBHOOK')]) {
discordSend description: "**Build:** [${currentBuild.id}](${env.BUILD_URL})\n**Status:** [${currentBuild.currentResult}](${env.BUILD_URL})\n\n[**Artifacts on Jenkins**](https://ci.nukkitx.com/job/Geyser)", footer: 'NukkitX Jenkins', link: env.BUILD_URL, successful: currentBuild.resultIsBetterOrEqualTo('SUCCESS'), title: "${env.JOB_NAME} #${currentBuild.id}", webhookURL: DISCORD_WEBHOOK
discordSend description: "**Build:** [${currentBuild.id}](${env.BUILD_URL})\n**Status:** [${currentBuild.currentResult}](${env.BUILD_URL})\n${changes}\n\n[**Artifacts on Jenkins**](https://ci.nukkitx.com/job/Geyser)", footer: 'Cloudburst Jenkins', link: env.BUILD_URL, successful: currentBuild.resultIsBetterOrEqualTo('SUCCESS'), title: "${env.JOB_NAME} #${currentBuild.id}", webhookURL: DISCORD_WEBHOOK
}
}
}

View File

@ -31,7 +31,7 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set
- Download: http://ci.geysermc.org
- Discord: http://discord.geysermc.org/
- ~~Donate: https://patreon.com/GeyserMC~~ Currently disabled.
- Test Server: test.geysermc.org port 25565 for Java and 19132 for Bedrock
- Test Server: `test.geysermc.org` port `25565` for Java and `19132` for Bedrock
## What's Left to be Added/Fixed
- The Following Inventories

View File

@ -25,17 +25,20 @@
package org.geysermc.platform.bungeecord.command;
import lombok.AllArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.utils.LanguageUtils;
@AllArgsConstructor
public class BungeeCommandSender implements CommandSender {
private net.md_5.bungee.api.CommandSender handle;
private final net.md_5.bungee.api.CommandSender handle;
public BungeeCommandSender(net.md_5.bungee.api.CommandSender handle) {
this.handle = handle;
// Ensure even Java players' languages are loaded
LanguageUtils.loadGeyserLocale(getLocale());
}
@Override
public String getName() {
@ -51,4 +54,14 @@ public class BungeeCommandSender implements CommandSender {
public boolean isConsole() {
return !(handle instanceof ProxiedPlayer);
}
@Override
public String getLocale() {
if (handle instanceof ProxiedPlayer) {
ProxiedPlayer player = (ProxiedPlayer) handle;
String locale = player.getLocale().getLanguage() + "_" + player.getLocale().getCountry();
return LanguageUtils.formatLocale(locale);
}
return LanguageUtils.getDefaultLocale();
}
}

View File

@ -27,13 +27,10 @@ package org.geysermc.platform.bungeecord.command;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.ArrayList;
@ -41,7 +38,7 @@ import java.util.Arrays;
public class GeyserBungeeCommandExecutor extends Command implements TabExecutor {
private GeyserConnector connector;
private final GeyserConnector connector;
public GeyserBungeeCommandExecutor(GeyserConnector connector) {
super("geyser");
@ -54,20 +51,16 @@ public class GeyserBungeeCommandExecutor extends Command implements TabExecutor
if (args.length > 0) {
if (getCommand(args[0]) != null) {
if (!sender.hasPermission(getCommand(args[0]).getPermission())) {
String message = "";
if (sender instanceof GeyserSession) {
message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", ((GeyserSession) sender).getClientData().getLanguageCode());
} else {
message = LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.permission_fail");
}
BungeeCommandSender commandSender = new BungeeCommandSender(sender);
String message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.getLocale());
sender.sendMessage(TextComponent.fromLegacyText(ChatColor.RED + message));
commandSender.sendMessage(ChatColor.RED + message);
return;
}
getCommand(args[0]).execute(new BungeeCommandSender(sender), args);
getCommand(args[0]).execute(new BungeeCommandSender(sender), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]);
}
} else {
getCommand("help").execute(new BungeeCommandSender(sender), args);
getCommand("help").execute(new BungeeCommandSender(sender), new String[0]);
}
}

View File

@ -40,6 +40,7 @@ import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor;
import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager;
import org.geysermc.platform.spigot.command.SpigotCommandSender;
import org.geysermc.platform.spigot.world.GeyserSpigotBlockPlaceListener;
import org.geysermc.platform.spigot.world.GeyserSpigotWorldManager;
@ -130,6 +131,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.debug("Legacy version of Minecraft (1.15.2 or older) detected; not using 3D biomes.");
}
// Set if we need to use a different method for getting a player's locale
SpigotCommandSender.setUseLegacyLocaleMethod(!isCompatible(Bukkit.getServer().getVersion(), "1.12.0"));
this.geyserWorldManager = new GeyserSpigotWorldManager(isLegacy, use3dBiomes, isViaVersion);
GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, isLegacy, isViaVersion);

View File

@ -32,7 +32,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.ArrayList;
@ -42,28 +41,24 @@ import java.util.List;
@AllArgsConstructor
public class GeyserSpigotCommandExecutor implements TabExecutor {
private GeyserConnector connector;
private final GeyserConnector connector;
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0) {
if (getCommand(args[0]) != null) {
if (!sender.hasPermission(getCommand(args[0]).getPermission())) {
String message = "";
if (sender instanceof GeyserSession) {
message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", ((GeyserSession) sender).getClientData().getLanguageCode());
} else {
message = LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.permission_fail");
}
SpigotCommandSender commandSender = new SpigotCommandSender(sender);
String message = LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.getLocale());;
sender.sendMessage(ChatColor.RED + message);
commandSender.sendMessage(ChatColor.RED + message);
return true;
}
getCommand(args[0]).execute(new SpigotCommandSender(sender), args);
getCommand(args[0]).execute(new SpigotCommandSender(sender), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]);
return true;
}
} else {
getCommand("help").execute(new SpigotCommandSender(sender), args);
getCommand("help").execute(new SpigotCommandSender(sender), new String[0]);
return true;
}
return true;

View File

@ -25,15 +25,33 @@
package org.geysermc.platform.spigot.command;
import lombok.AllArgsConstructor;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.utils.LanguageUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@AllArgsConstructor
public class SpigotCommandSender implements CommandSender {
private org.bukkit.command.CommandSender handle;
/**
* Whether to use {@code Player.getLocale()} or {@code Player.spigot().getLocale()}, depending on version.
* 1.12 or greater should not use the legacy method.
*/
private static boolean USE_LEGACY_METHOD = false;
private static Method LOCALE_METHOD;
private final org.bukkit.command.CommandSender handle;
private final String locale;
public SpigotCommandSender(org.bukkit.command.CommandSender handle) {
this.handle = handle;
this.locale = getSpigotLocale();
// Ensure even Java players' languages are loaded
LanguageUtils.loadGeyserLocale(locale);
}
@Override
public String getName() {
@ -49,4 +67,49 @@ public class SpigotCommandSender implements CommandSender {
public boolean isConsole() {
return handle instanceof ConsoleCommandSender;
}
@Override
public String getLocale() {
return locale;
}
/**
* Set if we are on pre-1.12, and therefore {@code player.getLocale()} doesn't exist and we have to get
* {@code player.spigot().getLocale()}.
*
* @param useLegacyMethod if we are running pre-1.12 and therefore need to use reflection to get the player locale
*/
public static void setUseLegacyLocaleMethod(boolean useLegacyMethod) {
USE_LEGACY_METHOD = useLegacyMethod;
if (USE_LEGACY_METHOD) {
try {
//noinspection JavaReflectionMemberAccess - of course it doesn't exist; that's why we're doing it
LOCALE_METHOD = Player.Spigot.class.getMethod("getLocale");
} catch (NoSuchMethodException e) {
GeyserConnector.getInstance().getLogger().debug("Player.Spigot.getLocale() doesn't exist? Not a big deal but if you're seeing this please report it to the developers!");
}
}
}
/**
* So we only have to do nasty reflection stuff once per command
*
* @return the locale of the Spigot player
*/
private String getSpigotLocale() {
if (handle instanceof Player) {
Player player = (Player) handle;
if (USE_LEGACY_METHOD) {
try {
// sigh
// This was the only option on older Spigot instances and now it's gone
return (String) LOCALE_METHOD.invoke(player.spigot());
} catch (IllegalAccessException | InvocationTargetException ignored) {
}
} else {
return player.getLocale();
}
}
return LanguageUtils.getDefaultLocale();
}
}

View File

@ -26,12 +26,14 @@
package org.geysermc.platform.spigot.world;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.GeyserWorldManager;
@ -93,23 +95,32 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
@Override
public int getBlockAt(GeyserSession session, int x, int y, int z) {
if (session.getPlayerEntity() == null) {
return BlockTranslator.AIR;
}
if (Bukkit.getPlayer(session.getPlayerEntity().getUsername()) == null) {
Player bukkitPlayer;
if ((this.isLegacy && !this.isViaVersion)
|| session.getPlayerEntity() == null
|| (bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
return BlockTranslator.AIR;
}
World world = bukkitPlayer.getWorld();
if (isLegacy) {
return getLegacyBlock(session, x, y, z, isViaVersion);
return getLegacyBlock(session, x, y, z, true);
}
//TODO possibly: detect server version for all versions and use ViaVersion for block state mappings like below
return BlockTranslator.getJavaIdBlockMap().getOrDefault(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getBlockAt(x, y, z).getBlockData().getAsString(), 0);
return BlockTranslator.getJavaIdBlockMap().getOrDefault(world.getBlockAt(x, y, z).getBlockData().getAsString(), 0);
}
public static int getLegacyBlock(GeyserSession session, int x, int y, int z, boolean isViaVersion) {
if (isViaVersion) {
return getLegacyBlock(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld(), x, y, z, true);
} else {
return BlockTranslator.AIR;
}
}
@SuppressWarnings("deprecation")
public static int getLegacyBlock(GeyserSession session, int x, int y, int z, boolean isViaVersion) {
public static int getLegacyBlock(World world, int x, int y, int z, boolean isViaVersion) {
if (isViaVersion) {
Block block = Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getBlockAt(x, y, z);
Block block = world.getBlockAt(x, y, z);
// Black magic that gets the old block state ID
int oldBlockId = (block.getType().getId() << 4) | (block.getData() & 0xF);
// Convert block state from old version -> 1.13 -> 1.13.1 -> 1.14 -> 1.15 -> 1.16 -> 1.16.2
@ -124,6 +135,42 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
}
}
@Override
public void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk chunk) {
Player bukkitPlayer;
if ((this.isLegacy && !this.isViaVersion)
|| session.getPlayerEntity() == null
|| (bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
return;
}
World world = bukkitPlayer.getWorld();
if (this.isLegacy) {
for (int blockY = 0; blockY < 16; blockY++) { // Cache-friendly iteration order
for (int blockZ = 0; blockZ < 16; blockZ++) {
for (int blockX = 0; blockX < 16; blockX++) {
chunk.set(blockX, blockY, blockZ, getLegacyBlock(world, (x << 4) + blockX, (y << 4) + blockY, (z << 4) + blockZ, true));
}
}
}
} else {
//TODO: see above TODO in getBlockAt
for (int blockY = 0; blockY < 16; blockY++) { // Cache-friendly iteration order
for (int blockZ = 0; blockZ < 16; blockZ++) {
for (int blockX = 0; blockX < 16; blockX++) {
Block block = world.getBlockAt((x << 4) + blockX, (y << 4) + blockY, (z << 4) + blockZ);
int id = BlockTranslator.getJavaIdBlockMap().getOrDefault(block.getBlockData().getAsString(), 0);
chunk.set(blockX, blockY, blockZ, id);
}
}
}
}
}
@Override
public boolean hasMoreBlockDataThanChunkCache() {
return true;
}
@Override
@SuppressWarnings("deprecation")
public int[] getBiomeDataAt(GeyserSession session, int x, int z) {

View File

@ -59,10 +59,10 @@ public class GeyserSpongeCommandExecutor implements CommandCallable {
source.sendMessage(Text.of(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.permission_fail")));
return CommandResult.success();
}
getCommand(args[0]).execute(new SpongeCommandSender(source), args);
getCommand(args[0]).execute(new SpongeCommandSender(source), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]);
}
} else {
getCommand("help").execute(new SpongeCommandSender(source), args);
getCommand("help").execute(new SpongeCommandSender(source), new String[0]);
}
return CommandResult.success();
}

View File

@ -261,14 +261,30 @@ public class GeyserStandaloneGUI {
for (Map.Entry<String, GeyserCommand> command : geyserCommandManager.getCommands().entrySet()) {
// Remove the offhand command and any alias commands to prevent duplicates in the list
if ("offhand".equals(command.getValue().getName()) || command.getValue().getAliases().contains(command.getKey())) {
if (!command.getValue().isExecutableOnConsole() || command.getValue().getAliases().contains(command.getKey())) {
continue;
}
// Create the button that runs the command
JMenuItem commandButton = new JMenuItem(command.getValue().getName());
boolean hasSubCommands = command.getValue().hasSubCommands();
// Add an extra menu if there are more commands that can be run
JMenuItem commandButton = hasSubCommands ? new JMenu(command.getValue().getName()) : new JMenuItem(command.getValue().getName());
commandButton.getAccessibleContext().setAccessibleDescription(command.getValue().getDescription());
commandButton.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{ }));
if (!hasSubCommands) {
commandButton.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{ }));
} else {
// Add a submenu that's the same name as the menu can't be pressed
JMenuItem otherCommandButton = new JMenuItem(command.getValue().getName());
otherCommandButton.getAccessibleContext().setAccessibleDescription(command.getValue().getDescription());
otherCommandButton.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{ }));
commandButton.add(otherCommandButton);
// Add a menu option for all possible subcommands
for (String subCommandName : command.getValue().getSubCommands()) {
JMenuItem item = new JMenuItem(subCommandName);
item.addActionListener(e -> command.getValue().execute(geyserStandaloneLogger, new String[]{subCommandName}));
commandButton.add(item);
}
}
commandsMenu.add(commandButton);
}
@ -291,7 +307,7 @@ public class GeyserStandaloneGUI {
playerTableModel.getDataVector().removeAllElements();
for (GeyserSession player : GeyserConnector.getInstance().getPlayers()) {
Vector row = new Vector();
Vector<String> row = new Vector<>();
row.add(player.getSocketAddress().getHostName());
row.add(player.getPlayerEntity().getUsername());

View File

@ -27,34 +27,33 @@ package org.geysermc.platform.velocity.command;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import lombok.AllArgsConstructor;
import net.kyori.text.TextComponent;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.Arrays;
@AllArgsConstructor
public class GeyserVelocityCommandExecutor implements Command {
private GeyserConnector connector;
private final GeyserConnector connector;
@Override
public void execute(CommandSource source, String[] args) {
if (args.length > 0) {
if (getCommand(args[0]) != null) {
if (!source.hasPermission(getCommand(args[0]).getPermission())) {
// Not ideal to use log here but we dont get a session
source.sendMessage(TextComponent.of(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.bootstrap.command.permission_fail")));
CommandSender sender = new VelocityCommandSender(source);
sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.getLocale()));
return;
}
getCommand(args[0]).execute(new VelocityCommandSender(source), args);
getCommand(args[0]).execute(new VelocityCommandSender(source), args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]);
}
} else {
getCommand("help").execute(new VelocityCommandSender(source), args);
getCommand("help").execute(new VelocityCommandSender(source), new String[0]);
}
}

View File

@ -28,17 +28,21 @@ package org.geysermc.platform.velocity.command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.api.proxy.Player;
import lombok.AllArgsConstructor;
import net.kyori.text.TextComponent;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.Locale;
@AllArgsConstructor
public class VelocityCommandSender implements CommandSender {
private CommandSource handle;
private final CommandSource handle;
public VelocityCommandSender(CommandSource handle) {
this.handle = handle;
// Ensure even Java players' languages are loaded
LanguageUtils.loadGeyserLocale(getLocale());
}
@Override
public String getName() {
@ -59,4 +63,13 @@ public class VelocityCommandSender implements CommandSender {
public boolean isConsole() {
return handle instanceof ConsoleCommandSource;
}
@Override
public String getLocale() {
if (handle instanceof Player) {
Locale locale = ((Player) handle).getPlayerSettings().getLocale();
return LanguageUtils.formatLocale(locale.getLanguage() + "_" + locale.getCountry());
}
return LanguageUtils.getDefaultLocale();
}
}

View File

@ -33,7 +33,7 @@
<dependency>
<groupId>com.github.CloudburstMC.Protocol</groupId>
<artifactId>bedrock-v408</artifactId>
<version>250beb2a94</version>
<version>02f46a8700</version>
<scope>compile</scope>
<exclusions>
<exclusion>
@ -109,9 +109,9 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.GeyserMC</groupId>
<groupId>com.github.steveice10</groupId>
<artifactId>mcprotocollib</artifactId>
<version>e4a3aa636a</version>
<version>1b01b1ffef</version>
<scope>compile</scope>
<exclusions>
<exclusion>
@ -137,21 +137,21 @@
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.github.kyoripowered.adventure</groupId>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>557865caef</version>
<version>4.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.kyoripowered.adventure</groupId>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>557865caef</version>
<version>4.1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.kyoripowered.adventure</groupId>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>557865caef</version>
<version>4.1.1</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -71,7 +71,9 @@ import java.net.UnknownHostException;
import java.security.Key;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -217,8 +219,39 @@ public class GeyserConnector {
metrics = new Metrics(this, "GeyserMC", config.getMetrics().getUniqueId(), false, java.util.logging.Logger.getLogger(""));
metrics.addCustomChart(new Metrics.SingleLineChart("servers", () -> 1));
metrics.addCustomChart(new Metrics.SingleLineChart("players", players::size));
metrics.addCustomChart(new Metrics.SimplePie("authMode", authType.name()::toLowerCase));
// Prevent unwanted words best we can
metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> AuthType.getByName(config.getRemote().getAuthType()).toString().toLowerCase()));
metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::getPlatformName));
metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", LanguageUtils::getDefaultLocale));
metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserConnector.VERSION));
metrics.addCustomChart(new Metrics.AdvancedPie("playerPlatform", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : players) {
if (session == null) continue;
if (session.getClientData() == null) continue;
String os = session.getClientData().getDeviceOS().toString();
if (!valueMap.containsKey(os)) {
valueMap.put(os, 1);
} else {
valueMap.put(os, valueMap.get(os) + 1);
}
}
return valueMap;
}));
metrics.addCustomChart(new Metrics.AdvancedPie("playerVersion", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : players) {
if (session == null) continue;
if (session.getClientData() == null) continue;
String version = session.getClientData().getGameVersion();
if (!valueMap.containsKey(version)) {
valueMap.put(version, 1);
} else {
valueMap.put(version, valueMap.get(version) + 1);
}
}
return valueMap;
}));
}
boolean isGui = false;
@ -256,7 +289,7 @@ public class GeyserConnector {
// Make a copy to prevent ConcurrentModificationException
final List<GeyserSession> tmpPlayers = new ArrayList<>(players);
for (GeyserSession playerSession : tmpPlayers) {
playerSession.disconnect(LanguageUtils.getPlayerLocaleString("geyser.core.shutdown.kick.message", playerSession.getClientData().getLanguageCode()));
playerSession.disconnect(LanguageUtils.getPlayerLocaleString("geyser.core.shutdown.kick.message", playerSession.getLocale()));
}
CompletableFuture<Void> future = CompletableFuture.runAsync(new Runnable() {
@ -330,15 +363,16 @@ public class GeyserConnector {
}
/**
* Get the production status of the current runtime.
* Will return true if the version number is not 'DEV'.
* Should only happen in compiled jars.
* Whether to use XML reflections in the jar or manually find the reflections.
* Will return true if the version number is not 'DEV' and the platform is not Fabric.
* On Fabric - it complains about being unable to create a default XMLReader.
* On other platforms this should only be true in compiled jars.
*
* @return If we are in a production build/environment
* @return whether to use XML reflections
*/
public boolean isProduction() {
public boolean useXmlReflections() {
//noinspection ConstantConditions
return !"DEV".equals(GeyserConnector.VERSION);
return !this.getPlatformType().equals(PlatformType.FABRIC) && !"DEV".equals(GeyserConnector.VERSION);
}
public static GeyserConnector getInstance() {

View File

@ -40,18 +40,19 @@ public abstract class CommandManager {
@Getter
private final Map<String, GeyserCommand> commands = Collections.synchronizedMap(new HashMap<>());
private GeyserConnector connector;
private final GeyserConnector connector;
public CommandManager(GeyserConnector connector) {
this.connector = connector;
registerCommand(new HelpCommand(connector, "help", LanguageUtils.getLocaleStringLog("geyser.commands.help.desc"), "geyser.command.help"));
registerCommand(new ListCommand(connector, "list", LanguageUtils.getLocaleStringLog("geyser.commands.list.desc"), "geyser.command.list"));
registerCommand(new ReloadCommand(connector, "reload", LanguageUtils.getLocaleStringLog("geyser.commands.reload.desc"), "geyser.command.reload"));
registerCommand(new StopCommand(connector, "stop", LanguageUtils.getLocaleStringLog("geyser.commands.stop.desc"), "geyser.command.stop"));
registerCommand(new OffhandCommand(connector, "offhand", LanguageUtils.getLocaleStringLog("geyser.commands.offhand.desc"), "geyser.command.offhand"));
registerCommand(new DumpCommand(connector, "dump", LanguageUtils.getLocaleStringLog("geyser.commands.dump.desc"), "geyser.command.dump"));
registerCommand(new VersionCommand(connector, "version", LanguageUtils.getLocaleStringLog("geyser.commands.version.desc"), "geyser.command.version"));
registerCommand(new HelpCommand(connector, "help", "geyser.commands.help.desc", "geyser.command.help"));
registerCommand(new ListCommand(connector, "list", "geyser.commands.list.desc", "geyser.command.list"));
registerCommand(new ReloadCommand(connector, "reload", "geyser.commands.reload.desc", "geyser.command.reload"));
registerCommand(new StopCommand(connector, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
registerCommand(new OffhandCommand(connector, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand"));
registerCommand(new DumpCommand(connector, "dump", "geyser.commands.dump.desc", "geyser.command.dump"));
registerCommand(new VersionCommand(connector, "version", "geyser.commands.version.desc", "geyser.command.version"));
registerCommand(new StatisticsCommand(connector, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics"));
}
public void registerCommand(GeyserCommand command) {

View File

@ -25,6 +25,12 @@
package org.geysermc.connector.command;
import org.geysermc.connector.utils.LanguageUtils;
/**
* Implemented on top of any class that can send a command.
* For example, it wraps around Spigot's CommandSender class.
*/
public interface CommandSender {
String getName();
@ -37,5 +43,17 @@ public interface CommandSender {
void sendMessage(String message);
/**
* @return true if the specified sender is from the console.
*/
boolean isConsole();
/**
* Returns the locale of the command sender. Defaults to the default locale at {@link LanguageUtils#getDefaultLocale()}.
*
* @return the locale of the command sender.
*/
default String getLocale() {
return LanguageUtils.getDefaultLocale();
}
}

View File

@ -30,6 +30,7 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Getter
@ -37,6 +38,9 @@ import java.util.List;
public abstract class GeyserCommand {
protected final String name;
/**
* The description of the command - will attempt to be translated.
*/
protected final String description;
protected final String permission;
@ -44,4 +48,31 @@ public abstract class GeyserCommand {
private List<String> aliases = new ArrayList<>();
public abstract void execute(CommandSender sender, String[] args);
/**
* If false, hides the command from being shown on the Geyser Standalone GUI.
*
* @return true if the command can be run on the server console
*/
public boolean isExecutableOnConsole() {
return true;
}
/**
* Used in the GUI to know what subcommands can be run
*
* @return a list of all possible subcommands, or empty if none.
*/
public List<String> getSubCommands() {
return Collections.emptyList();
}
/**
* Shortcut to {@link #getSubCommands()}{@code .isEmpty()}.
*
* @return true if there are subcommand present for this command.
*/
public boolean hasSubCommands() {
return !getSubCommands().isEmpty();
}
}

View File

@ -27,12 +27,10 @@ package org.geysermc.connector.command.defaults;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.common.serializer.AsteriskSerializer;
import org.geysermc.connector.dump.DumpInfo;
import org.geysermc.connector.utils.LanguageUtils;
@ -40,6 +38,8 @@ import org.geysermc.connector.utils.WebUtils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class DumpCommand extends GeyserCommand {
@ -73,7 +73,7 @@ public class DumpCommand extends GeyserCommand {
AsteriskSerializer.showSensitive = showSensitive;
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.dump.collecting"));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.dump.collecting", sender.getLocale()));
String dumpData = "";
try {
if (offlineDump) {
@ -82,7 +82,7 @@ public class DumpCommand extends GeyserCommand {
dumpData = MAPPER.writeValueAsString(new DumpInfo());
}
} catch (IOException e) {
sender.sendMessage(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.commands.dump.collect_error"));
sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.commands.dump.collect_error", sender.getLocale()));
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.commands.dump.collect_error_short"), e);
return;
}
@ -90,21 +90,21 @@ public class DumpCommand extends GeyserCommand {
String uploadedDumpUrl = "";
if (offlineDump) {
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.dump.writing"));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.dump.writing", sender.getLocale()));
try {
FileOutputStream outputStream = new FileOutputStream(GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("dump.json").toFile());
outputStream.write(dumpData.getBytes());
outputStream.close();
} catch (IOException e) {
sender.sendMessage(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.commands.dump.write_error"));
sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.commands.dump.write_error", sender.getLocale()));
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.commands.dump.write_error_short"), e);
return;
}
uploadedDumpUrl = "dump.json";
} else {
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.dump.uploading"));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.dump.uploading", sender.getLocale()));
String response;
JsonNode responseNode;
@ -112,22 +112,27 @@ public class DumpCommand extends GeyserCommand {
response = WebUtils.post(DUMP_URL + "documents", dumpData);
responseNode = MAPPER.readTree(response);
} catch (IOException e) {
sender.sendMessage(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.commands.dump.upload_error"));
sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.commands.dump.upload_error", sender.getLocale()));
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.commands.dump.upload_error_short"), e);
return;
}
if (!responseNode.has("key")) {
sender.sendMessage(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.commands.dump.upload_error_short") + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response));
sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.commands.dump.upload_error_short", sender.getLocale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response));
return;
}
uploadedDumpUrl = DUMP_URL + responseNode.get("key").asText();
}
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.dump.message") + " " + ChatColor.DARK_AQUA + uploadedDumpUrl);
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.dump.message", sender.getLocale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl);
if (!sender.isConsole()) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.commands.dump.created", sender.getName(), uploadedDumpUrl));
}
}
@Override
public List<String> getSubCommands() {
return Arrays.asList("offline", "full");
}
}

View File

@ -25,11 +25,10 @@
package org.geysermc.connector.command.defaults;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.common.ChatColor;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.Collections;
@ -52,17 +51,12 @@ public class HelpCommand extends GeyserCommand {
public void execute(CommandSender sender, String[] args) {
int page = 1;
int maxPage = 1;
String header = "";
if (sender instanceof GeyserSession) {
header = LanguageUtils.getPlayerLocaleString("geyser.commands.help.header", ((GeyserSession) sender).getClientData().getLanguageCode(), page, maxPage);
} else {
header = LanguageUtils.getLocaleStringLog("geyser.commands.help.header", page, maxPage);
}
String header = LanguageUtils.getPlayerLocaleString("geyser.commands.help.header", sender.getLocale(), page, maxPage);
sender.sendMessage(header);
Map<String, GeyserCommand> cmds = connector.getCommandManager().getCommands();
List<String> commands = connector.getCommandManager().getCommands().keySet().stream().sorted().collect(Collectors.toList());
commands.forEach(cmd -> sender.sendMessage(ChatColor.YELLOW + "/geyser " + cmd + ChatColor.WHITE + ": " + cmds.get(cmd).getDescription()));
commands.forEach(cmd -> sender.sendMessage(ChatColor.YELLOW + "/geyser " + cmd + ChatColor.WHITE + ": " +
LanguageUtils.getPlayerLocaleString(cmds.get(cmd).getDescription(), sender.getLocale())));
}
}

View File

@ -35,7 +35,7 @@ import java.util.stream.Collectors;
public class ListCommand extends GeyserCommand {
private GeyserConnector connector;
private final GeyserConnector connector;
public ListCommand(GeyserConnector connector, String name, String description, String permission) {
super(name, description, permission);
@ -46,11 +46,9 @@ public class ListCommand extends GeyserCommand {
@Override
public void execute(CommandSender sender, String[] args) {
String message = "";
if (sender instanceof GeyserSession) {
message = LanguageUtils.getPlayerLocaleString("geyser.commands.list.message", ((GeyserSession) sender).getClientData().getLanguageCode(), connector.getPlayers().size(), connector.getPlayers().stream().map(GeyserSession::getName).collect(Collectors.joining(" ")));
} else {
message = LanguageUtils.getLocaleStringLog("geyser.commands.list.message", connector.getPlayers().size(), connector.getPlayers().stream().map(GeyserSession::getName).collect(Collectors.joining(" ")));
}
message = LanguageUtils.getPlayerLocaleString("geyser.commands.list.message", sender.getLocale(),
connector.getPlayers().size(),
connector.getPlayers().stream().map(GeyserSession::getName).collect(Collectors.joining(" ")));
sender.sendMessage(message);
}

View File

@ -36,7 +36,7 @@ import org.geysermc.connector.network.session.GeyserSession;
public class OffhandCommand extends GeyserCommand {
private GeyserConnector connector;
private final GeyserConnector connector;
public OffhandCommand(GeyserConnector connector, String name, String description, String permission) {
super(name, description, permission);
@ -58,7 +58,7 @@ public class OffhandCommand extends GeyserCommand {
session.sendDownstreamPacket(releaseItemPacket);
return;
}
// Needed for Bukkit - sender is not an instance of GeyserSession
// Needed for Spigot - sender is not an instance of GeyserSession
for (GeyserSession session : connector.getPlayers()) {
if (sender.getName().equals(session.getPlayerEntity().getUsername())) {
ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.SWAP_HANDS, new Position(0,0,0),
@ -68,4 +68,9 @@ public class OffhandCommand extends GeyserCommand {
}
}
}
@Override
public boolean isExecutableOnConsole() {
return false;
}
}

View File

@ -47,17 +47,12 @@ public class ReloadCommand extends GeyserCommand {
return;
}
String message = "";
if (sender instanceof GeyserSession) {
message = LanguageUtils.getPlayerLocaleString("geyser.commands.reload.message", ((GeyserSession) sender).getClientData().getLanguageCode());
} else {
message = LanguageUtils.getLocaleStringLog("geyser.commands.reload.message");
}
String message = LanguageUtils.getPlayerLocaleString("geyser.commands.reload.message", sender.getLocale());
sender.sendMessage(message);
for (GeyserSession session : connector.getPlayers()) {
session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.commands.reload.kick", session.getClientData().getLanguageCode()));
session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.commands.reload.kick", session.getLocale()));
}
connector.reload();
}

View File

@ -0,0 +1,74 @@
/*
* 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.command.defaults;
import com.github.steveice10.mc.protocol.data.game.ClientRequest;
import com.github.steveice10.mc.protocol.packet.ingame.client.ClientRequestPacket;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.network.session.GeyserSession;
public class StatisticsCommand extends GeyserCommand {
private final GeyserConnector connector;
public StatisticsCommand(GeyserConnector connector, String name, String description, String permission) {
super(name, description, permission);
this.connector = connector;
}
@Override
public void execute(CommandSender sender, String[] args) {
if (sender.isConsole()) {
return;
}
// Make sure the sender is a Bedrock edition client
GeyserSession session = null;
if (sender instanceof GeyserSession) {
session = (GeyserSession) sender;
} else {
// Needed for Spigot - sender is not an instance of GeyserSession
for (GeyserSession otherSession : connector.getPlayers()) {
if (sender.getName().equals(otherSession.getPlayerEntity().getUsername())) {
session = otherSession;
break;
}
}
}
if (session == null) return;
session.setWaitingForStatistics(true);
ClientRequestPacket clientRequestPacket = new ClientRequestPacket(ClientRequest.STATS);
session.sendDownstreamPacket(clientRequestPacket);
}
@Override
public boolean isExecutableOnConsole() {
return false;
}
}

View File

@ -61,12 +61,12 @@ public class VersionCommand extends GeyserCommand {
bedrockVersions = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion();
}
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.version.version", GeyserConnector.NAME, GeyserConnector.VERSION, MinecraftConstants.GAME_VERSION, bedrockVersions));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.version.version", sender.getLocale(), GeyserConnector.NAME, GeyserConnector.VERSION, MinecraftConstants.GAME_VERSION, bedrockVersions));
// Disable update checking in dev mode
//noinspection ConstantConditions - changes in production
if (!GeyserConnector.VERSION.equals("DEV")) {
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.version.checking"));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.version.checking", sender.getLocale()));
try {
Properties gitProp = new Properties();
gitProp.load(FileUtils.getResource("git.properties"));
@ -76,16 +76,16 @@ public class VersionCommand extends GeyserCommand {
int latestBuildNum = Integer.parseInt(buildXML.replaceAll("<(\\\\)?(/)?buildNumber>", "").trim());
int buildNum = Integer.parseInt(gitProp.getProperty("git.build.number"));
if (latestBuildNum == buildNum) {
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.version.no_updates"));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.version.no_updates", sender.getLocale()));
} else {
sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.version.outdated", (latestBuildNum - buildNum), "http://ci.geysermc.org/"));
sender.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.commands.version.outdated", sender.getLocale(), (latestBuildNum - buildNum), "https://ci.geysermc.org/"));
}
} else {
throw new AssertionError("buildNumber missing");
}
} catch (IOException | AssertionError | NumberFormatException e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.commands.version.failed"), e);
sender.sendMessage(ChatColor.RED + LanguageUtils.getLocaleStringLog("geyser.commands.version.failed"));
sender.sendMessage(ChatColor.RED + LanguageUtils.getPlayerLocaleString("geyser.commands.version.failed", sender.getLocale()));
}
}
}

View File

@ -34,10 +34,11 @@ public enum PlatformType {
ANDROID("Android"),
BUNGEECORD("BungeeCord"),
FABRIC("Fabric"),
SPIGOT("Spigot"),
SPONGE("Sponge"),
STANDALONE("Standalone"),
VELOCITY("Velocity");
private String platformName;
private final String platformName;
}

View File

@ -34,6 +34,7 @@ import org.geysermc.connector.common.serializer.AsteriskSerializer;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
@ -45,8 +46,8 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@Setter
private boolean autoconfiguredRemote = false;
private BedrockConfiguration bedrock;
private RemoteConfiguration remote;
private BedrockConfiguration bedrock = new BedrockConfiguration();
private RemoteConfiguration remote = new RemoteConfiguration();
@JsonProperty("floodgate-key-file")
private String floodgateKeyFile = "public-key.pem";
@ -106,7 +107,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("force-resource-packs")
private boolean forceResourcePacks = true;
private MetricsInfo metrics;
private MetricsInfo metrics = new MetricsInfo();
@Getter
public static class BedrockConfiguration implements IBedrockConfiguration {
@ -154,7 +155,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
private boolean enabled = true;
@JsonProperty("uuid")
private String uniqueId = "generateuuid";
private String uniqueId = UUID.randomUUID().toString();
}
@JsonProperty("scoreboard-packet-threshold")

View File

@ -43,6 +43,8 @@ public class AreaEffectCloudEntity extends Entity {
// This disabled client side shrink of the cloud
metadata.put(EntityData.AREA_EFFECT_CLOUD_RADIUS, 0.0f);
metadata.put(EntityData.AREA_EFFECT_CLOUD_CHANGE_RATE, -0.005f);
metadata.put(EntityData.AREA_EFFECT_CLOUD_CHANGE_ON_PICKUP, -0.5f);
}
@Override
@ -50,11 +52,14 @@ public class AreaEffectCloudEntity extends Entity {
if (entityMetadata.getId() == 7) {
metadata.put(EntityData.AREA_EFFECT_CLOUD_RADIUS, entityMetadata.getValue());
metadata.put(EntityData.BOUNDING_BOX_WIDTH, 2.0f * (float) entityMetadata.getValue());
} else if (entityMetadata.getId() == 8) {
metadata.put(EntityData.EFFECT_COLOR, entityMetadata.getValue());
} else if (entityMetadata.getId() == 10) {
Particle particle = (Particle) entityMetadata.getValue();
metadata.put(EntityData.AREA_EFFECT_CLOUD_PARTICLE_ID, EffectRegistry.getParticleString(particle.getType()));
} else if (entityMetadata.getId() == 8) {
metadata.put(EntityData.POTION_AUX_VALUE, entityMetadata.getValue());
int particleId = EffectRegistry.getParticleId(particle.getType());
if (particleId != -1) {
metadata.put(EntityData.AREA_EFFECT_CLOUD_PARTICLE_ID, particleId);
}
}
super.updateBedrockMetadata(entityMetadata, session);
}

View File

@ -321,7 +321,7 @@ public class Entity {
Message message = (Message) entityMetadata.getValue();
if (message != null)
// Always translate even if it's a TextMessage since there could be translatable parameters
metadata.put(EntityData.NAMETAG, MessageUtils.getTranslatedBedrockMessage(message, session.getClientData().getLanguageCode(), true));
metadata.put(EntityData.NAMETAG, MessageUtils.getTranslatedBedrockMessage(message, session.getLocale(), true));
}
break;
case 3: // is custom name visible

View File

@ -26,11 +26,65 @@
package org.geysermc.connector.entity;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class ThrowableEntity extends Entity {
private Vector3f lastPosition;
private ScheduledFuture<?> positionUpdater;
public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
this.lastPosition = position;
}
@Override
public void spawnEntity(GeyserSession session) {
super.spawnEntity(session);
positionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> {
super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround);
if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) {
float gravity = 0.03f; // Snowball, Egg, and Ender Pearl
if (entityType == EntityType.THROWN_POTION || entityType == EntityType.LINGERING_POTION) {
gravity = 0.05f;
} else if (entityType == EntityType.THROWN_EXP_BOTTLE) {
gravity = 0.07f;
}
motion = motion.down(gravity);
}
}, 0, 50, TimeUnit.MILLISECONDS);
}
@Override
public boolean despawnEntity(GeyserSession session) {
positionUpdater.cancel(true);
if (entityType == EntityType.THROWN_ENDERPEARL) {
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(LevelEventType.PARTICLE_TELEPORT);
particlePacket.setPosition(position);
session.sendUpstreamPacket(particlePacket);
}
return super.despawnEntity(session);
}
@Override
public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
position = lastPosition;
super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
lastPosition = position;
}
@Override
public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
lastPosition = position;
}
}

View File

@ -25,12 +25,39 @@
package org.geysermc.connector.entity;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.item.TippedArrowPotion;
/**
* Internally this is known as TippedArrowEntity but is used with tipped arrows and normal arrows
*/
public class TippedArrowEntity extends AbstractArrowEntity {
public TippedArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
}
@Override
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
// Arrow potion effect color
if (entityMetadata.getId() == 9) {
int potionColor = (int) entityMetadata.getValue();
// -1 means no color
if (potionColor == -1) {
metadata.remove(EntityData.CUSTOM_DISPLAY);
} else {
TippedArrowPotion potion = TippedArrowPotion.getByJavaColor(potionColor);
if (potion != null && potion.getJavaColor() != -1) {
metadata.put(EntityData.CUSTOM_DISPLAY, (byte) potion.getBedrockId());
} else {
metadata.remove(EntityData.CUSTOM_DISPLAY);
}
}
}
super.updateBedrockMetadata(entityMetadata, session);
}
}

View File

@ -34,6 +34,8 @@ import org.geysermc.connector.network.session.GeyserSession;
public class WolfEntity extends TameableEntity {
private byte collarColor;
public WolfEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
}
@ -57,12 +59,13 @@ public class WolfEntity extends TameableEntity {
// Wolf collar color
// Relies on EntityData.OWNER_EID being set in TameableEntity.java
if (entityMetadata.getId() == 19 && !metadata.getFlags().getFlag(EntityFlag.ANGRY)) {
metadata.put(EntityData.COLOR, (byte) (int) entityMetadata.getValue());
metadata.put(EntityData.COLOR, collarColor = (byte) (int) entityMetadata.getValue());
}
// Wolf anger (1.16+)
if (entityMetadata.getId() == 20) {
metadata.getFlags().setFlag(EntityFlag.ANGRY, (int) entityMetadata.getValue() != 0);
metadata.put(EntityData.COLOR, (int) entityMetadata.getValue() != 0 ? (byte) 0 : collarColor);
}
super.updateBedrockMetadata(entityMetadata, session);

View File

@ -32,33 +32,60 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.AddEntityPacket;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import lombok.Data;
import org.geysermc.connector.entity.living.InsentientEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class EnderDragonEntity extends InsentientEntity {
/**
* The Ender Dragon has multiple hit boxes, which
* are each its own invisible entity
*/
private EnderDragonPartEntity head;
private EnderDragonPartEntity neck;
private EnderDragonPartEntity body;
private EnderDragonPartEntity leftWing;
private EnderDragonPartEntity rightWing;
private EnderDragonPartEntity[] tail;
private EnderDragonPartEntity[] allParts;
/**
* A circular buffer that stores a history of
* y and yaw values.
*/
private final Segment[] segmentHistory = new Segment[19];
private int latestSegment = -1;
private boolean hovering;
private ScheduledFuture<?> partPositionUpdater;
public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
}
@Override
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
// Phase
if (entityMetadata.getId() == 15) {
metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
switch ((int) entityMetadata.getValue()) {
int value = (int) entityMetadata.getValue();
if (value == 5) {
// Performing breath attack
case 5:
EntityEventPacket entityEventPacket = new EntityEventPacket();
entityEventPacket.setType(EntityEventType.DRAGON_FLAMING);
entityEventPacket.setRuntimeEntityId(geyserId);
entityEventPacket.setData(0);
session.sendUpstreamPacket(entityEventPacket);
case 6:
case 7:
metadata.getFlags().setFlag(EntityFlag.SITTING, true);
break;
EntityEventPacket entityEventPacket = new EntityEventPacket();
entityEventPacket.setType(EntityEventType.DRAGON_FLAMING);
entityEventPacket.setRuntimeEntityId(geyserId);
entityEventPacket.setData(0);
session.sendUpstreamPacket(entityEventPacket);
}
metadata.getFlags().setFlag(EntityFlag.SITTING, value == 5 || value == 6 || value == 7);
hovering = value == 10;
}
super.updateBedrockMetadata(entityMetadata, session);
}
@ -81,6 +108,118 @@ public class EnderDragonEntity extends InsentientEntity {
valid = true;
session.sendUpstreamPacket(addEntityPacket);
head = new EnderDragonPartEntity(entityId + 1, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 1, 1);
neck = new EnderDragonPartEntity(entityId + 2, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 3, 3);
body = new EnderDragonPartEntity(entityId + 3, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 5, 3);
leftWing = new EnderDragonPartEntity(entityId + 4, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2);
rightWing = new EnderDragonPartEntity(entityId + 5, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2);
tail = new EnderDragonPartEntity[3];
for (int i = 0; i < 3; i++) {
tail[i] = new EnderDragonPartEntity(entityId + 6 + i, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 2, 2);
}
allParts = new EnderDragonPartEntity[]{head, neck, body, leftWing, rightWing, tail[0], tail[1], tail[2]};
for (EnderDragonPartEntity part : allParts) {
session.getEntityCache().spawnEntity(part);
}
for (int i = 0; i < segmentHistory.length; i++) {
segmentHistory[i] = new Segment();
segmentHistory[i].yaw = rotation.getZ();
segmentHistory[i].y = position.getY();
}
partPositionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> {
pushSegment();
updateBoundingBoxes(session);
}, 0, 50, TimeUnit.MILLISECONDS);
session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")");
}
@Override
public boolean despawnEntity(GeyserSession session) {
partPositionUpdater.cancel(true);
for (EnderDragonPartEntity part : allParts) {
part.despawnEntity(session);
}
return super.despawnEntity(session);
}
/**
* Updates the positions of the Ender Dragon's multiple bounding boxes
*
* @param session GeyserSession.
*/
private void updateBoundingBoxes(GeyserSession session) {
Vector3f facingDir = Vector3f.createDirectionDeg(0, rotation.getZ());
Segment baseSegment = getSegment(5);
// Used to angle the head, neck, and tail when the dragon flies up/down
float pitch = (float) Math.toRadians(10 * (baseSegment.getY() - getSegment(10).getY()));
float pitchXZ = (float) Math.cos(pitch);
float pitchY = (float) Math.sin(pitch);
// Lowers the head when the dragon sits/hovers
float headDuck;
if (hovering || metadata.getFlags().getFlag(EntityFlag.SITTING)) {
headDuck = -1f;
} else {
headDuck = baseSegment.y - getSegment(0).y;
}
head.setPosition(facingDir.up(pitchY).mul(pitchXZ, 1, -pitchXZ).mul(6.5f).up(headDuck));
neck.setPosition(facingDir.up(pitchY).mul(pitchXZ, 1, -pitchXZ).mul(5.5f).up(headDuck));
body.setPosition(facingDir.mul(0.5f, 0f, -0.5f));
Vector3f wingPos = Vector3f.createDirectionDeg(0, 90f - rotation.getZ()).mul(4.5f).up(2f);
rightWing.setPosition(wingPos);
leftWing.setPosition(wingPos.mul(-1, 1, -1)); // Mirror horizontally
Vector3f tailBase = facingDir.mul(1.5f);
for (int i = 0; i < tail.length; i++) {
float distance = (i + 1) * 2f;
// Curls the tail when the dragon turns
Segment targetSegment = getSegment(12 + 2 * i);
float angle = rotation.getZ() + targetSegment.yaw - baseSegment.yaw;
float tailYOffset = targetSegment.y - baseSegment.y - (distance + 1.5f) * pitchY + 1.5f;
tail[i].setPosition(Vector3f.createDirectionDeg(0, angle).mul(distance).add(tailBase).mul(-pitchXZ, 1, pitchXZ).up(tailYOffset));
}
// Send updated positions
for (EnderDragonPartEntity part : allParts) {
part.moveAbsolute(session, part.getPosition().add(position), Vector3f.ZERO, false, false);
}
}
/**
* Store the current yaw and y into the circular buffer
*/
private void pushSegment() {
latestSegment = (latestSegment + 1) % segmentHistory.length;
segmentHistory[latestSegment].yaw = rotation.getZ();
segmentHistory[latestSegment].y = position.getY();
}
/**
* Gets the previous yaw and y
* Used to curl the tail and pitch the head and tail up/down
*
* @param index Number of ticks in the past
* @return Segment with the yaw and y
*/
private Segment getSegment(int index) {
index = (latestSegment - index) % segmentHistory.length;
if (index < 0) {
index += segmentHistory.length;
}
return segmentHistory[index];
}
@Data
private static class Segment {
private float yaw;
private float y;
}
}

View File

@ -23,31 +23,20 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network.translators.world.chunk;
package org.geysermc.connector.entity.living.monster;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.type.EntityType;
@Getter
@Setter
@AllArgsConstructor
@EqualsAndHashCode
public class ChunkPosition {
public class EnderDragonPartEntity extends Entity {
public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, float width, float height) {
super(entityId, geyserId, entityType, position, motion, rotation);
private int x;
private int z;
public Position getBlock(int x, int y, int z) {
return new Position((this.x << 4) + x, y, (this.z << 4) + z);
}
public Position getChunkBlock(int x, int y, int z) {
int chunkX = x & 15;
int chunkY = y & 15;
int chunkZ = z & 15;
return new Position(chunkX, chunkY, chunkZ);
metadata.put(EntityData.BOUNDING_BOX_WIDTH, width);
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height);
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.entity.living.monster.raid;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
public class PillagerEntity extends AbstractIllagerEntity {
public PillagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
}
@Override
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
if (entityMetadata.getId() == 16) {
// Java Edition always has the Pillager entity as positioning the crossbow
metadata.getFlags().setFlag(EntityFlag.USING_ITEM, true);
metadata.getFlags().setFlag(EntityFlag.CHARGED, true);
}
super.updateBedrockMetadata(entityMetadata, session);
}
}

View File

@ -34,6 +34,7 @@ import org.geysermc.connector.entity.living.animal.tameable.*;
import org.geysermc.connector.entity.living.merchant.*;
import org.geysermc.connector.entity.living.monster.*;
import org.geysermc.connector.entity.living.monster.raid.AbstractIllagerEntity;
import org.geysermc.connector.entity.living.monster.raid.PillagerEntity;
import org.geysermc.connector.entity.living.monster.raid.RaidParticipantEntity;
import org.geysermc.connector.entity.living.monster.raid.SpellcasterIllagerEntity;
@ -85,12 +86,12 @@ public enum EntityType {
ELDER_GUARDIAN(ElderGuardianEntity.class, 50, 1.9975f),
NPC(PlayerEntity.class, 51, 1.8f, 0.6f, 0.6f, 1.62f),
WITHER(WitherEntity.class, 52, 3.5f, 0.9f),
ENDER_DRAGON(EnderDragonEntity.class, 53, 4f, 13f),
ENDER_DRAGON(EnderDragonEntity.class, 53, 0f, 0f),
SHULKER(ShulkerEntity.class, 54, 1f, 1f),
ENDERMITE(MonsterEntity.class, 55, 0.3f, 0.4f),
AGENT(Entity.class, 56, 0f),
VINDICATOR(AbstractIllagerEntity.class, 57, 1.8f, 0.6f, 0.6f, 1.62f),
PILLAGER(AbstractIllagerEntity.class, 114, 1.8f, 0.6f, 0.6f, 1.62f),
PILLAGER(PillagerEntity.class, 114, 1.8f, 0.6f, 0.6f, 1.62f),
WANDERING_TRADER(AbstractMerchantEntity.class, 118, 1.8f, 0.6f, 0.6f, 1.62f),
PHANTOM(FlyingEntity.class, 58, 0.5f, 0.9f, 0.9f, 0.6f),
RAVAGER(RaidParticipantEntity.class, 59, 1.9f, 1.2f),
@ -165,7 +166,12 @@ public enum EntityType {
/**
* Not an entity in Bedrock, so we replace it with a Pillager
*/
ILLUSIONER(AbstractIllagerEntity.class, 114, 1.8f, 0.6f, 0.6f, 1.62f, "minecraft:pillager");
ILLUSIONER(AbstractIllagerEntity.class, 114, 1.8f, 0.6f, 0.6f, 1.62f, "minecraft:pillager"),
/**
* Not an entity in Bedrock, but used for the Ender Dragon's multiple hitboxes
*/
ENDER_DRAGON_PART(EnderDragonPartEntity.class, 32, 0, 0, 0, 0, "minecraft:armor_stand");
private static final EntityType[] VALUES = values();

View File

@ -116,7 +116,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
case HAVE_ALL_PACKS:
ResourcePackStackPacket stackPacket = new ResourcePackStackPacket();
stackPacket.setExperimental(false);
stackPacket.setExperimentsPreviouslyToggled(false);
stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not
stackPacket.setGameVersion(session.getClientData().getGameVersion());
@ -139,7 +139,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
@Override
public boolean handle(ModalFormResponsePacket packet) {
session.getFormCache().handleResponse(packet);
return true;
return true; //todo change the Statistics Form to match the new style
}
private boolean couldLoginUserByName(String bedrockUsername) {
@ -161,7 +161,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
@Override
public boolean handle(SetLocalPlayerAsInitializedPacket packet) {
LanguageUtils.loadGeyserLocale(session.getClientData().getLanguageCode());
LanguageUtils.loadGeyserLocale(session.getLocale());
if (!session.isLoggedIn() && !session.isLoggingIn() && session.getConnector().getAuthType() == AuthType.ONLINE) {
// TODO it is safer to key authentication on something that won't change (UUID, not username)
@ -176,7 +176,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
@Override
public boolean handle(MovePlayerPacket packet) {
if (session.isLoggingIn()) {
session.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.auth.login.wait", session.getClientData().getLanguageCode()));
session.sendMessage(LanguageUtils.getPlayerLocaleString("geyser.auth.login.wait", session.getLocale()));
}
return translateAndDefault(packet);

View File

@ -28,9 +28,11 @@ package org.geysermc.connector.network.session;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException;
import com.github.steveice10.mc.auth.exception.request.RequestException;
import com.github.steveice10.mc.protocol.MinecraftConstants;
import com.github.steveice10.mc.protocol.MinecraftProtocol;
import com.github.steveice10.mc.protocol.data.SubProtocol;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.statistic.Statistic;
import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade;
import com.github.steveice10.mc.protocol.data.message.MessageSerializer;
import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket;
@ -54,6 +56,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.geysermc.common.form.Form;
import org.geysermc.connector.GeyserConnector;
@ -79,6 +82,7 @@ import org.geysermc.floodgate.util.BedrockData;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
@Getter
@ -113,8 +117,6 @@ public class GeyserSession implements CommandSender {
*/
private final Object2LongMap<Vector3i> itemFrameCache = new Object2LongOpenHashMap<>();
private DataCache<Packet> javaPacketCache;
@Setter
private Vector2i lastChunkPosition = null;
private int renderDistance;
@ -172,6 +174,12 @@ public class GeyserSession implements CommandSender {
@Setter
private long lastWindowCloseTime = 0;
/**
* Saves the timestamp of the last keep alive packet
*/
@Setter
private long lastKeepAliveTimestamp = 0;
@Setter
private VillagerTrade[] villagerTrades;
@Setter
@ -184,9 +192,10 @@ public class GeyserSession implements CommandSender {
/**
* The current attack speed of the player. Used for sending proper cooldown timings.
* Setting a default fixes cooldowns not showing up on a fresh world.
*/
@Setter
private double attackSpeed;
private double attackSpeed = 4.0d;
/**
* The time of the last hit. Used to gauge how long the cooldown is taking.
* This is a session variable in order to prevent more scheduled threads than necessary.
@ -201,6 +210,13 @@ public class GeyserSession implements CommandSender {
@Setter
private long lastInteractionTime;
/**
* Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to
* interact with a block.
*/
@Setter
private ScheduledFuture<?> bucketScheduledFuture;
private boolean reducedDebugInfo = false;
/**
@ -254,6 +270,22 @@ public class GeyserSession implements CommandSender {
@Setter
private String lastSignMessage;
/**
* Stores a map of all statistics sent from the server.
* The server only sends new statistics back to us, so in order to show all statistics we need to cache existing ones.
*/
private final Map<Statistic, Integer> statistics = new HashMap<>();
/**
* Whether we're expecting statistics to be sent back to us.
*/
@Setter
private boolean waitingForStatistics = false;
@Setter
private List<UUID> selectedEmotes = new ArrayList<>();
private final Set<UUID> emotes = new HashSet<>();
private MinecraftProtocol protocol;
public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) {
@ -269,13 +301,13 @@ public class GeyserSession implements CommandSender {
this.playerEntity = new PlayerEntity(new GameProfile(UUID.randomUUID(), "unknown"), 1, 1, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO);
this.inventory = new PlayerInventory();
this.javaPacketCache = new DataCache<>();
this.spawned = false;
this.loggedIn = false;
this.inventoryCache.getInventories().put(0, inventory);
connector.getPlayers().forEach(player -> this.emotes.addAll(player.getEmotes()));
bedrockServerSession.addDisconnectHandler(disconnectReason -> {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", bedrockServerSession.getAddress().getAddress(), disconnectReason));
@ -359,6 +391,8 @@ public class GeyserSession implements CommandSender {
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE;
downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory());
// Let Geyser handle sending the keep alive
downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false);
downstream.getSession().addListener(new SessionAdapter() {
@Override
public void packetSending(PacketSendingEvent event) {
@ -421,7 +455,7 @@ public class GeyserSession implements CommandSender {
// as it has to be extracted from a JAR
if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) {
// This should probably be left hardcoded as it will only show for en_us clients
sendMessage("Downloading your locale (en_us) this may take some time");
sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time");
}
// Download and load the language for the player
@ -541,6 +575,11 @@ public class GeyserSession implements CommandSender {
return false;
}
@Override
public String getLocale() {
return clientData.getLanguageCode();
}
public void setRenderDistance(int renderDistance) {
renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle
if (renderDistance > 32) renderDistance = 32; // <3 u ViaVersion but I don't like crashing clients x)
@ -717,6 +756,10 @@ public class GeyserSession implements CommandSender {
// Required to make command blocks destroyable
adventureSettingsPacket.setPlayerPermission(opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER);
// Update the noClip and worldImmutable values based on the current gamemode
noClip = gameMode == GameMode.SPECTATOR;
worldImmutable = gameMode == GameMode.ADVENTURE || gameMode == GameMode.SPECTATOR;
Set<AdventureSetting> flags = new HashSet<>();
if (canFly) {
flags.add(AdventureSetting.MAY_FLY);
@ -739,4 +782,31 @@ public class GeyserSession implements CommandSender {
adventureSettingsPacket.getSettings().addAll(flags);
sendUpstreamPacket(adventureSettingsPacket);
}
/**
* Used for updating statistic values since we only get changes from the server
*
* @param statistics Updated statistics values
*/
public void updateStatistics(@NonNull Map<Statistic, Integer> statistics) {
this.statistics.putAll(statistics);
}
public void refreshEmotes(List<UUID> emotes) {
this.selectedEmotes = emotes;
this.emotes.addAll(emotes);
for (GeyserSession player : connector.getPlayers()) {
List<UUID> pieces = new ArrayList<>();
for (UUID piece : emotes) {
if (!player.getEmotes().contains(piece)) {
pieces.add(piece);
}
player.getEmotes().add(piece);
}
EmoteListPacket emoteList = new EmoteListPacket();
emoteList.setRuntimeEntityId(player.getPlayerEntity().getGeyserId());
emoteList.getPieceIds().addAll(pieces);
player.sendUpstreamPacket(emoteList);
}
}
}

View File

@ -58,7 +58,7 @@ public class BossBar {
BossEventPacket bossEventPacket = new BossEventPacket();
bossEventPacket.setBossUniqueEntityId(entityId);
bossEventPacket.setAction(BossEventPacket.Action.CREATE);
bossEventPacket.setTitle(MessageUtils.getTranslatedBedrockMessage(title, session.getClientData().getLanguageCode()));
bossEventPacket.setTitle(MessageUtils.getTranslatedBedrockMessage(title, session.getLocale()));
bossEventPacket.setHealthPercentage(health);
bossEventPacket.setColor(color); //ignored by client
bossEventPacket.setOverlay(overlay);
@ -72,7 +72,7 @@ public class BossBar {
BossEventPacket bossEventPacket = new BossEventPacket();
bossEventPacket.setBossUniqueEntityId(entityId);
bossEventPacket.setAction(BossEventPacket.Action.UPDATE_NAME);
bossEventPacket.setTitle(MessageUtils.getTranslatedBedrockMessage(title, session.getClientData().getLanguageCode()));
bossEventPacket.setTitle(MessageUtils.getTranslatedBedrockMessage(title, session.getLocale()));
session.sendUpstreamPacket(bossEventPacket);
}

View File

@ -27,22 +27,18 @@ package org.geysermc.connector.network.session.cache;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.chunk.Column;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import lombok.Getter;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.geysermc.connector.bootstrap.GeyserBootstrap;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.chunk.ChunkPosition;
import java.util.HashMap;
import java.util.Map;
import org.geysermc.connector.utils.MathUtils;
public class ChunkCache {
private final boolean cache;
@Getter
private final Map<ChunkPosition, Column> chunks = new HashMap<>();
private final Long2ObjectMap<Column> chunks = new Long2ObjectOpenHashMap<>();
public ChunkCache(GeyserSession session) {
if (session.getConnector().getWorldManager().getClass() == GeyserBootstrap.DEFAULT_CHUNK_MANAGER.getClass()) {
@ -52,61 +48,74 @@ public class ChunkCache {
}
}
public void addToCache(Column chunk) {
public Column addToCache(Column chunk) {
if (!cache) {
return;
return chunk;
}
ChunkPosition position = new ChunkPosition(chunk.getX(), chunk.getZ());
if (chunk.getBiomeData() == null && chunks.containsKey(position)) {
Column newColumn = chunk;
chunk = chunks.get(position);
for (int i = 0; i < newColumn.getChunks().length; i++) {
if (newColumn.getChunks()[i] != null) {
chunk.getChunks()[i] = newColumn.getChunks()[i];
long chunkPosition = MathUtils.chunkPositionToLong(chunk.getX(), chunk.getZ());
Column existingChunk;
if (chunk.getBiomeData() == null // Only consider merging columns if the new chunk isn't a full chunk
&& (existingChunk = chunks.getOrDefault(chunkPosition, null)) != null) { // Column is already present in cache, we can merge with existing
boolean changed = false;
for (int i = 0; i < chunk.getChunks().length; i++) { // The chunks member is final, so chunk.getChunks() will probably be inlined and then completely optimized away
if (chunk.getChunks()[i] != null) {
existingChunk.getChunks()[i] = chunk.getChunks()[i];
changed = true;
}
}
}
chunks.put(position, chunk);
}
public void updateBlock(Position position, int block) {
if (!cache) {
return;
}
ChunkPosition chunkPosition = new ChunkPosition(position.getX() >> 4, position.getZ() >> 4);
if (!chunks.containsKey(chunkPosition))
return;
Column column = chunks.get(chunkPosition);
Chunk chunk = column.getChunks()[position.getY() >> 4];
Position blockPosition = chunkPosition.getChunkBlock(position.getX(), position.getY(), position.getZ());
if (chunk != null) {
chunk.set(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), block);
return changed ? existingChunk : null;
} else {
chunks.put(chunkPosition, chunk);
return chunk;
}
}
public int getBlockAt(Position position) {
public Column getChunk(int chunkX, int chunkZ) {
long chunkPosition = MathUtils.chunkPositionToLong(chunkX, chunkZ);
return chunks.getOrDefault(chunkPosition, null);
}
public void updateBlock(int x, int y, int z, int block) {
if (!cache) {
return;
}
Column column = this.getChunk(x >> 4, z >> 4);
if (column == null) {
return;
}
Chunk chunk = column.getChunks()[y >> 4];
if (chunk != null) {
chunk.set(x & 0xF, y & 0xF, z & 0xF, block);
}
}
public int getBlockAt(int x, int y, int z) {
if (!cache) {
return BlockTranslator.AIR;
}
ChunkPosition chunkPosition = new ChunkPosition(position.getX() >> 4, position.getZ() >> 4);
if (!chunks.containsKey(chunkPosition))
return BlockTranslator.AIR;
Column column = chunks.get(chunkPosition);
Chunk chunk = column.getChunks()[position.getY() >> 4];
Position blockPosition = chunkPosition.getChunkBlock(position.getX(), position.getY(), position.getZ());
Column column = this.getChunk(x >> 4, z >> 4);
if (column == null) {
return BlockTranslator.AIR;
}
Chunk chunk = column.getChunks()[y >> 4];
if (chunk != null) {
return chunk.get(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ());
return chunk.get(x & 0xF, y & 0xF, z & 0xF);
}
return BlockTranslator.AIR;
}
public void removeChunk(ChunkPosition position) {
public void removeChunk(int chunkX, int chunkZ) {
if (!cache) {
return;
}
chunks.remove(position);
long chunkPosition = MathUtils.chunkPositionToLong(chunkX, chunkZ);
chunks.remove(chunkPosition);
}
}

View File

@ -25,7 +25,6 @@
package org.geysermc.connector.network.translators;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerKeepAlivePacket;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPlayerListDataPacket;
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUpdateLightPacket;
import com.github.steveice10.packetlib.packet.Packet;
@ -49,7 +48,7 @@ public class PacketTranslatorRegistry<T> {
private static final ObjectArrayList<Class<?>> IGNORED_PACKETS = new ObjectArrayList<>();
static {
Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators") : new Reflections("org.geysermc.connector.network.translators");
Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators") : new Reflections("org.geysermc.connector.network.translators");
for (Class<?> clazz : ref.getTypesAnnotatedWith(Translator.class)) {
Class<?> packet = clazz.getAnnotation(Translator.class).packet();
@ -75,7 +74,6 @@ public class PacketTranslatorRegistry<T> {
}
}
IGNORED_PACKETS.add(ServerKeepAlivePacket.class); // Handled by MCProtocolLib
IGNORED_PACKETS.add(ServerUpdateLightPacket.class); // Light is handled on Bedrock for us
IGNORED_PACKETS.add(ServerPlayerListDataPacket.class); // Cant be implemented in bedrock
}

View File

@ -23,15 +23,18 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network.session.cache;
package org.geysermc.connector.network.translators.bedrock;
import lombok.Getter;
import com.nukkitx.protocol.bedrock.packet.EmoteListPacket;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import java.util.HashMap;
import java.util.Map;
@Translator(packet = EmoteListPacket.class)
public class BedrockEmoteListTranslator extends PacketTranslator<EmoteListPacket> {
public class DataCache<T> {
@Getter
private Map<String, T> cachedValues = new HashMap<String, T>();
@Override
public void translate(EmoteListPacket packet, GeyserSession session) {
session.refreshEmotes(packet.getPieceIds());
}
}

View File

@ -49,6 +49,7 @@ import org.geysermc.connector.entity.CommandBlockMinecartEntity;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
@ -58,8 +59,11 @@ import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.sound.EntitySoundInteractionHandler;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.BlockUtils;
import org.geysermc.connector.utils.InventoryUtils;
import java.util.concurrent.TimeUnit;
@Translator(packet = InventoryTransactionPacket.class)
public class BedrockInventoryTransactionTranslator extends PacketTranslator<InventoryTransactionPacket> {
@ -119,18 +123,19 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.sendDownstreamPacket(itemPacket);
}
// Check actions, otherwise buckets may be activated when block inventories are accessed
// But don't check actions if the item damage is 0
else if (packet.getItemInHand() != null && packet.getItemInHand().getId() == ItemRegistry.BUCKET.getBedrockId() &&
(packet.getItemInHand().getDamage() == 0 || !packet.getActions().isEmpty())) {
ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
session.sendDownstreamPacket(itemPacket);
else if (packet.getItemInHand() != null && packet.getItemInHand().getId() == ItemRegistry.BUCKET.getBedrockId()) {
// Let the server decide if the bucket item should change, not the client, and revert the changes the client made
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.INVENTORY);
slotPacket.setSlot(packet.getHotbarSlot());
slotPacket.setItem(packet.getItemInHand());
session.sendUpstreamPacket(slotPacket);
// Delay the interaction in case the client doesn't intend to actually use the bucket
// See BedrockActionTranslator.java
session.setBucketScheduledFuture(session.getConnector().getGeneralThreadPool().schedule(() -> {
ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
session.sendDownstreamPacket(itemPacket);
}, 5, TimeUnit.MILLISECONDS));
}
if (packet.getActions().isEmpty()) {
@ -151,28 +156,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
}
Vector3i blockPos = packet.getBlockPosition();
// TODO: Find a better way to do this?
switch (packet.getBlockFace()) {
case 0:
blockPos = blockPos.sub(0, 1, 0);
break;
case 1:
blockPos = blockPos.add(0, 1, 0);
break;
case 2:
blockPos = blockPos.sub(0, 0, 1);
break;
case 3:
blockPos = blockPos.add(0, 0, 1);
break;
case 4:
blockPos = blockPos.sub(1, 0, 0);
break;
case 5:
blockPos = blockPos.add(1, 0, 0);
break;
}
Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace());
ItemEntry handItem = ItemRegistry.getItem(packet.getItemInHand());
if (handItem.isBlock()) {
session.setLastBlockPlacePosition(blockPos);
@ -187,10 +171,9 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
break;
}
// Handled in ITEM_USE
// Handled in ITEM_USE if the item is not milk
if (packet.getItemInHand() != null && packet.getItemInHand().getId() == ItemRegistry.BUCKET.getBedrockId() &&
// Normal bucket, water bucket, lava bucket
(packet.getItemInHand().getDamage() == 0 || packet.getItemInHand().getDamage() == 8 || packet.getItemInHand().getDamage() == 10)) {
packet.getItemInHand().getDamage() != 1) {
break;
}
@ -269,9 +252,17 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
break;
case 1: //Attack
ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
if (entity.getEntityType() == EntityType.ENDER_DRAGON) {
// Redirects the attack to its body entity, this only happens when
// attacking the underbelly of the ender dragon
ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId() + 3,
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
} else {
ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
}
break;
}
break;

View File

@ -40,7 +40,8 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
@Override
public void translate(MobEquipmentPacket packet, GeyserSession session) {
if (!session.isSpawned() || packet.getHotbarSlot() > 8 ||
packet.getContainerId() != ContainerId.INVENTORY) {
packet.getContainerId() != ContainerId.INVENTORY || session.getInventory().getHeldItemSlot() == packet.getHotbarSlot()) {
// For the last condition - Don't update the slot if the slot is the same - not Java Edition behavior and messes with plugins such as Grief Prevention
return;
}

View File

@ -0,0 +1,46 @@
/*
* 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.bedrock;
import com.github.steveice10.mc.protocol.packet.ingame.client.ClientKeepAlivePacket;
import com.nukkitx.protocol.bedrock.packet.NetworkStackLatencyPacket;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
/**
* Used to send the keep alive packet back to the server
*/
@Translator(packet = NetworkStackLatencyPacket.class)
public class BedrockNetworkStackLatencyTranslator extends PacketTranslator<NetworkStackLatencyPacket> {
@Override
public void translate(NetworkStackLatencyPacket packet, GeyserSession session) {
// The client sends a timestamp back but it's rounded and therefore unreliable when we need the exact number
ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(session.getLastKeepAliveTimestamp());
session.sendDownstreamPacket(keepAlivePacket);
}
}

View File

@ -28,7 +28,10 @@ package org.geysermc.connector.network.translators.bedrock;
import com.github.steveice10.mc.protocol.data.game.ClientRequest;
import com.github.steveice10.mc.protocol.packet.ingame.client.ClientRequestPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
import com.nukkitx.protocol.bedrock.packet.RespawnPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
@ -39,12 +42,30 @@ public class BedrockRespawnTranslator extends PacketTranslator<RespawnPacket> {
@Override
public void translate(RespawnPacket packet, GeyserSession session) {
if (packet.getState() == RespawnPacket.State.CLIENT_READY) {
if (!session.isSpawned()) { // Otherwise when immediate respawn is on the client never loads
RespawnPacket respawnPacket = new RespawnPacket();
respawnPacket.setRuntimeEntityId(0);
respawnPacket.setPosition(Vector3f.ZERO);
respawnPacket.setState(RespawnPacket.State.SERVER_READY);
session.sendUpstreamPacket(respawnPacket);
// Previously we only sent the respawn packet before the server finished loading
// The message included was 'Otherwise when immediate respawn is on the client never loads'
// But I assume the new if statement below fixes that problem
RespawnPacket respawnPacket = new RespawnPacket();
respawnPacket.setRuntimeEntityId(0);
respawnPacket.setPosition(Vector3f.ZERO);
respawnPacket.setState(RespawnPacket.State.SERVER_READY);
session.sendUpstreamPacket(respawnPacket);
if (session.isSpawned()) {
// Client might be stuck; resend spawn information
PlayerEntity entity = session.getPlayerEntity();
if (entity == null) return;
SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
entityDataPacket.getMetadata().putAll(entity.getMetadata());
session.sendUpstreamPacket(entityDataPacket);
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
movePlayerPacket.setPosition(entity.getPosition());
movePlayerPacket.setRotation(entity.getBedrockRotation());
movePlayerPacket.setMode(MovePlayerPacket.Mode.RESPAWN);
session.sendUpstreamPacket(movePlayerPacket);
}
ClientRequestPacket javaRespawnPacket = new ClientRequestPacket(ClientRequest.RESPAWN);

View File

@ -44,6 +44,7 @@ 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.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.BlockUtils;
import java.util.concurrent.TimeUnit;
@ -67,6 +68,8 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
eventPacket.setType(EntityEventType.RESPAWN);
eventPacket.setData(0);
session.sendUpstreamPacket(eventPacket);
// Resend attributes or else in rare cases the user can think they're not dead when they are, upon joining the server
entity.updateBedrockAttributes(session);
break;
case START_SWIMMING:
ClientPlayerStatePacket startSwimPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.START_SPRINTING);
@ -113,19 +116,24 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
session.sendDownstreamPacket(stopSleepingPacket);
break;
case BLOCK_INTERACT:
// Handled in BedrockInventoryTransactionTranslator
// Client means to interact with a block; cancel bucket interaction, if any
if (session.getBucketScheduledFuture() != null) {
session.getBucketScheduledFuture().cancel(true);
session.setBucketScheduledFuture(null);
}
// Otherwise handled in BedrockInventoryTransactionTranslator
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;
}
// Account for fire - the client likes to hit the block behind.
Vector3i fireBlockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getFace());
int blockUp = session.getConnector().getWorldManager().getBlockAt(session, fireBlockPos);
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(fireBlockPos.getX(),
fireBlockPos.getY(), fireBlockPos.getZ()), BlockFace.values()[packet.getFace()]);
session.sendDownstreamPacket(startBreakingPacket);
break;
}
}
ClientPlayerActionPacket startBreakingPacket = new ClientPlayerActionPacket(PlayerAction.START_DIGGING, new Position(packet.getBlockPosition().getX(),

View File

@ -32,10 +32,11 @@ import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.NonNull;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.LanguageUtils;
import java.io.InputStream;
import java.util.HashMap;
@ -50,8 +51,19 @@ public class EffectRegistry {
public static final Map<SoundEffect, Effect> SOUND_EFFECTS = new HashMap<>();
public static final Int2ObjectMap<SoundEvent> RECORDS = new Int2ObjectOpenHashMap<>();
private static Map<ParticleType, LevelEventType> particleTypeMap = new HashMap<>();
private static Map<ParticleType, String> particleStringMap = new HashMap<>();
/**
* Java particle type to Bedrock particle ID
* Used for area effect clouds.
*/
private static final Object2IntMap<ParticleType> PARTICLE_TO_ID = new Object2IntOpenHashMap<>();
/**
* Java particle type to Bedrock level event
*/
private static final Map<ParticleType, LevelEventType> PARTICLE_TO_LEVEL_EVENT = new HashMap<>();
/**
* Java particle type to Bedrock namespaced string ID
*/
private static final Map<ParticleType, String> PARTICLE_TO_STRING = new HashMap<>();
public static void init() {
// no-op
@ -68,22 +80,24 @@ public class EffectRegistry {
}
Iterator<Map.Entry<String, JsonNode>> particlesIterator = particleEntries.fields();
while (particlesIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = particlesIterator.next();
try {
particleTypeMap.put(ParticleType.valueOf(entry.getKey().toUpperCase()), LevelEventType.valueOf(entry.getValue().asText().toUpperCase()));
} catch (IllegalArgumentException e1) {
try {
particleStringMap.put(ParticleType.valueOf(entry.getKey().toUpperCase()), entry.getValue().asText());
GeyserConnector.getInstance().getLogger().debug("Force to map particle "
+ entry.getKey()
+ "=>"
+ entry.getValue().asText()
+ ", it will take effect.");
} catch (IllegalArgumentException e2){
GeyserConnector.getInstance().getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.particle.failed_map", entry.getKey(), entry.getValue().asText()));
try {
while (particlesIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = particlesIterator.next();
JsonNode bedrockId = entry.getValue().get("bedrockId");
JsonNode bedrockIdNumeric = entry.getValue().get("bedrockNumericId");
JsonNode eventType = entry.getValue().get("eventType");
if (bedrockIdNumeric != null) {
PARTICLE_TO_ID.put(ParticleType.valueOf(entry.getKey().toUpperCase()), bedrockIdNumeric.asInt());
}
if (bedrockId != null) {
PARTICLE_TO_STRING.put(ParticleType.valueOf(entry.getKey().toUpperCase()), bedrockId.asText());
}
if (eventType != null) {
PARTICLE_TO_LEVEL_EVENT.put(ParticleType.valueOf(entry.getKey().toUpperCase()), LevelEventType.valueOf(eventType.asText().toUpperCase()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
/* Load effects */
@ -149,11 +163,27 @@ public class EffectRegistry {
}
}
public static LevelEventType getParticleLevelEventType(@NonNull ParticleType type) {
return particleTypeMap.getOrDefault(type, null);
/**
* @param type the Java particle to search for
* @return the Bedrock integer ID of the particle, or -1 if it does not exist
*/
public static int getParticleId(@NonNull ParticleType type) {
return PARTICLE_TO_ID.getOrDefault(type, -1);
}
public static String getParticleString(@NonNull ParticleType type){
return particleStringMap.getOrDefault(type, null);
/**
* @param type the Java particle to search for
* @return the level event equivalent Bedrock particle
*/
public static LevelEventType getParticleLevelEventType(@NonNull ParticleType type) {
return PARTICLE_TO_LEVEL_EVENT.getOrDefault(type, null);
}
/**
* @param type the Java particle to search for
* @return the namespaced ID equivalent for Bedrock
*/
public static String getParticleString(@NonNull ParticleType type) {
return PARTICLE_TO_STRING.getOrDefault(type, null);
}
}

View File

@ -199,7 +199,7 @@ public class EnchantmentInventoryTranslator extends BlockInventoryTranslator {
private String toRomanNumeral(GeyserSession session, int level) {
return LocaleUtils.getLocaleString("enchantment.level." + level,
session.getClientData().getLanguageCode());
session.getLocale());
}
/**
@ -261,7 +261,7 @@ public class EnchantmentInventoryTranslator extends BlockInventoryTranslator {
public String toString(GeyserSession session) {
return LocaleUtils.getLocaleString("enchantment.minecraft." + this.toString().toLowerCase(),
session.getClientData().getLanguageCode());
session.getLocale());
}
}
}

View File

@ -35,9 +35,8 @@ import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
import lombok.AllArgsConstructor;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
import org.geysermc.connector.utils.LocaleUtils;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
@AllArgsConstructor
public class BlockInventoryHolder extends InventoryHolder {
@ -60,7 +59,7 @@ public class BlockInventoryHolder extends InventoryHolder {
.putInt("x", position.getX())
.putInt("y", position.getY())
.putInt("z", position.getZ())
.putString("CustomName", LocaleUtils.getLocaleString(inventory.getTitle(), session.getClientData().getLanguageCode())).build();
.putString("CustomName", inventory.getTitle()).build();
BlockEntityDataPacket dataPacket = new BlockEntityDataPacket();
dataPacket.setData(tag);
dataPacket.setBlockPosition(position);

View File

@ -63,6 +63,10 @@ public class ItemRegistry {
* Bucket item entry, used in BedrockInventoryTransactionTranslator.java
*/
public static ItemEntry BUCKET;
/**
* Egg item entry, used in JavaEntityStatusTranslator.java
*/
public static ItemEntry EGG;
/**
* Gold item entry, used in PiglinEntity.java
*/
@ -141,6 +145,9 @@ public class ItemRegistry {
case "minecraft:oak_boat":
BOAT = ITEM_ENTRIES.get(itemIndex);
break;
case "minecraft:egg":
EGG = ITEM_ENTRIES.get(itemIndex);
break;
case "minecraft:gold_ingot":
GOLD = ITEM_ENTRIES.get(itemIndex);
break;
@ -197,7 +204,9 @@ public class ItemRegistry {
*/
public static ItemEntry getItem(ItemData data) {
for (ItemEntry itemEntry : ITEM_ENTRIES.values()) {
if (itemEntry.getBedrockId() == data.getId() && (itemEntry.getBedrockData() == data.getDamage() || itemEntry.getJavaIdentifier().endsWith("potion"))) {
if (itemEntry.getBedrockId() == data.getId() && (itemEntry.getBedrockData() == data.getDamage() ||
// Make exceptions for potions and tipped arrows, whose damage values can vary
(itemEntry.getJavaIdentifier().endsWith("potion") || itemEntry.getJavaIdentifier().equals("minecraft:arrow")))) {
return itemEntry;
}
}

View File

@ -64,7 +64,7 @@ public abstract class ItemTranslator {
static {
/* Load item translators */
Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.item") : new Reflections("org.geysermc.connector.network.translators.item");
Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators.item") : new Reflections("org.geysermc.connector.network.translators.item");
Map<NbtItemStackTranslator, Integer> loadedNbtItemTranslators = new HashMap<>();
for (Class<?> clazz : ref.getTypesAnnotatedWith(ItemRemapper.class)) {
@ -400,7 +400,7 @@ public abstract class ItemTranslator {
// Check if its a message to translate
if (MessageUtils.isMessage(name)) {
// Get the translated name
name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(name), session.getClientData().getLanguageCode());
name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(name), session.getLocale());
// Add the new name tag
display.put(new StringTag("Name", name));

View File

@ -48,7 +48,7 @@ public enum Potion {
STRONG_SWIFTNESS(16),
LONG_SWIFTNESS(15),
SLOWNESS(17),
STRONG_SLOWNESS(18), //does not exist
STRONG_SLOWNESS(42),
LONG_SLOWNESS(18),
WATER_BREATHING(19),
LONG_WATER_BREATHING(20),

View File

@ -34,8 +34,7 @@ import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.LanguageUtils;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
import java.util.*;
/**
* Manages any recipe-related storing
@ -46,22 +45,45 @@ public class RecipeRegistry {
* A list of all possible leather armor dyeing recipes.
* Created manually.
*/
public static List<CraftingData> LEATHER_DYEING_RECIPES = new ObjectArrayList<>();
public static final List<CraftingData> LEATHER_DYEING_RECIPES = new ObjectArrayList<>();
/**
* A list of all possible firework rocket recipes, including the base rocket.
* Obtained from a ProxyPass dump of protocol v407
*/
public static List<CraftingData> FIREWORK_ROCKET_RECIPES = new ObjectArrayList<>(21);
public static final List<CraftingData> FIREWORK_ROCKET_RECIPES = new ObjectArrayList<>();
/**
* A list of all possible firework star recipes.
* Obtained from a ProxyPass dump of protocol v407
*/
public static List<CraftingData> FIREWORK_STAR_RECIPES = new ObjectArrayList<>(40);
public static final List<CraftingData> FIREWORK_STAR_RECIPES = new ObjectArrayList<>();
/**
* A list of all possible shulker box dyeing options.
* Obtained from a ProxyPass dump of protocol v407
*/
public static List<CraftingData> SHULKER_BOX_DYEING_RECIPES = new ObjectArrayList<>();
public static final List<CraftingData> SHULKER_BOX_DYEING_RECIPES = new ObjectArrayList<>();
/**
* A list of all possible suspicious stew recipes.
* Obtained from a ProxyPass dump of protocol v407
*/
public static final List<CraftingData> SUSPICIOUS_STEW_RECIPES = new ObjectArrayList<>();
/**
* A list of all possible tipped arrow recipes.
* Obtained from a ProxyPass dump of protocol v407
*/
public static final List<CraftingData> TIPPED_ARROW_RECIPES = new ObjectArrayList<>();
// TODO: These are the other "multi" UUIDs that supposedly enable various recipes. Find out what each enables.
// 442d85ed-8272-4543-a6f1-418f90ded05d 8b36268c-1829-483c-a0f1-993b7156a8f2 602234e4-cac1-4353-8bb7-b1ebff70024b 98c84b38-1085-46bd-b1ce-dd38c159e6cc
// d81aaeaf-e172-4440-9225-868df030d27b b5c5d105-75a2-4076-af2b-923ea2bf4bf0 00000000-0000-0000-0000-000000000002 85939755-ba10-4d9d-a4cc-efb7a8e943c4
// d392b075-4ba1-40ae-8789-af868d56f6ce aecd2294-4b94-434b-8667-4499bb2c9327
/**
* Recipe data that, when sent to the client, enables book cloning
*/
public static final CraftingData BOOK_CLONING_RECIPE_DATA = CraftingData.fromMulti(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"));
/**
* Recipe data that, when sent to the client, enables tool repairing in a crafting table
*/
public static final CraftingData TOOL_REPAIRING_RECIPE_DATA = CraftingData.fromMulti(UUID.fromString("00000000-0000-0000-0000-000000000001"));
static {
// Get all recipes that are not directly sent from a Java server
@ -88,6 +110,12 @@ public class RecipeRegistry {
for (JsonNode entry : items.get("shulker_boxes")) {
SHULKER_BOX_DYEING_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
for (JsonNode entry : items.get("suspicious_stew")) {
SUSPICIOUS_STEW_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
for (JsonNode entry : items.get("tipped_arrows")) {
TIPPED_ARROW_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
}
/**
@ -97,11 +125,41 @@ public class RecipeRegistry {
*/
private static CraftingData getCraftingDataFromJsonNode(JsonNode node) {
ItemData output = ItemRegistry.getBedrockItemFromJson(node.get("output").get(0));
UUID uuid = UUID.randomUUID();
if (node.get("type").asInt() == 1) {
// Shaped recipe
List<String> shape = new ArrayList<>();
// Get the shape of the recipe
for (JsonNode chars : node.get("shape")) {
shape.add(chars.asText());
}
// In recipes.json each recipe is mapped by a letter
Map<String, ItemData> letterToRecipe = new HashMap<>();
Iterator<Map.Entry<String, JsonNode>> iterator = node.get("input").fields();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
letterToRecipe.put(entry.getKey(), ItemRegistry.getBedrockItemFromJson(entry.getValue()));
}
ItemData[] inputs = new ItemData[shape.size() * shape.get(0).length()];
int i = 0;
// Create a linear array of items from the "cube" of the shape
for (int j = 0; i < shape.size() * shape.get(0).length(); j++) {
for (char c : shape.get(j).toCharArray()) {
ItemData data = letterToRecipe.getOrDefault(String.valueOf(c), ItemData.AIR);
inputs[i] = data;
i++;
}
}
return CraftingData.fromShaped(uuid.toString(), shape.get(0).length(), shape.size(),
inputs, new ItemData[]{output}, uuid, "crafting_table", 0);
}
List<ItemData> inputs = new ObjectArrayList<>();
for (JsonNode entry : node.get("input")) {
inputs.add(ItemRegistry.getBedrockItemFromJson(entry));
}
UUID uuid = UUID.randomUUID();
if (node.get("type").asInt() == 5) {
// Shulker box
return CraftingData.fromShulkerBox(uuid.toString(),

View File

@ -0,0 +1,151 @@
/*
* 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.item;
import lombok.Getter;
import java.util.Locale;
/**
* Potion identifiers and their respective Bedrock IDs used with arrows.
* https://minecraft.gamepedia.com/Arrow#Item_Data
*/
@Getter
public enum TippedArrowPotion {
MUNDANE(2, ArrowParticleColors.NONE), // 3 is extended?
THICK(4, ArrowParticleColors.NONE),
AWKWARD(5, ArrowParticleColors.NONE),
NIGHT_VISION(6, ArrowParticleColors.NIGHT_VISION),
LONG_NIGHT_VISION(7, ArrowParticleColors.NIGHT_VISION),
INVISIBILITY(8, ArrowParticleColors.INVISIBILITY),
LONG_INVISIBILITY(9, ArrowParticleColors.INVISIBILITY),
LEAPING(10, ArrowParticleColors.LEAPING),
LONG_LEAPING(11, ArrowParticleColors.LEAPING),
STRONG_LEAPING(12, ArrowParticleColors.LEAPING),
FIRE_RESISTANCE(13, ArrowParticleColors.FIRE_RESISTANCE),
LONG_FIRE_RESISTANCE(14, ArrowParticleColors.FIRE_RESISTANCE),
SWIFTNESS(15, ArrowParticleColors.SWIFTNESS),
LONG_SWIFTNESS(16, ArrowParticleColors.SWIFTNESS),
STRONG_SWIFTNESS(17, ArrowParticleColors.SWIFTNESS),
SLOWNESS(18, ArrowParticleColors.SLOWNESS),
LONG_SLOWNESS(19, ArrowParticleColors.SLOWNESS),
STRONG_SLOWNESS(43, ArrowParticleColors.SLOWNESS),
WATER_BREATHING(20, ArrowParticleColors.WATER_BREATHING),
LONG_WATER_BREATHING(21, ArrowParticleColors.WATER_BREATHING),
HEALING(22, ArrowParticleColors.HEALING),
STRONG_HEALING(23, ArrowParticleColors.HEALING),
HARMING(24, ArrowParticleColors.HARMING),
STRONG_HARMING(25, ArrowParticleColors.HARMING),
POISON(26, ArrowParticleColors.POISON),
LONG_POISON(27, ArrowParticleColors.POISON),
STRONG_POISON(28, ArrowParticleColors.POISON),
REGENERATION(29, ArrowParticleColors.REGENERATION),
LONG_REGENERATION(30, ArrowParticleColors.REGENERATION),
STRONG_REGENERATION(31, ArrowParticleColors.REGENERATION),
STRENGTH(32, ArrowParticleColors.STRENGTH),
LONG_STRENGTH(33, ArrowParticleColors.STRENGTH),
STRONG_STRENGTH(34, ArrowParticleColors.STRENGTH),
WEAKNESS(35, ArrowParticleColors.WEAKNESS),
LONG_WEAKNESS(36, ArrowParticleColors.WEAKNESS),
LUCK(2, ArrowParticleColors.NONE), // does not exist in Bedrock
TURTLE_MASTER(38, ArrowParticleColors.TURTLE_MASTER),
LONG_TURTLE_MASTER(39, ArrowParticleColors.TURTLE_MASTER),
STRONG_TURTLE_MASTER(40, ArrowParticleColors.TURTLE_MASTER),
SLOW_FALLING(41, ArrowParticleColors.SLOW_FALLING),
LONG_SLOW_FALLING(42, ArrowParticleColors.SLOW_FALLING);
private final String javaIdentifier;
private final short bedrockId;
/**
* The Java color associated with this ID.
* Used for looking up Java arrow color entity metadata as Bedrock potion IDs, which is what is used for entities in Bedrock
*/
private final int javaColor;
TippedArrowPotion(int bedrockId, ArrowParticleColors arrowParticleColor) {
this.javaIdentifier = "minecraft:" + this.name().toLowerCase(Locale.ENGLISH);
this.bedrockId = (short) bedrockId;
this.javaColor = arrowParticleColor.getColor();
}
public static TippedArrowPotion getByJavaIdentifier(String javaIdentifier) {
for (TippedArrowPotion potion : TippedArrowPotion.values()) {
if (potion.javaIdentifier.equals(javaIdentifier)) {
return potion;
}
}
return null;
}
public static TippedArrowPotion getByBedrockId(short bedrockId) {
for (TippedArrowPotion potion : TippedArrowPotion.values()) {
if (potion.bedrockId == bedrockId) {
return potion;
}
}
return null;
}
/**
* @param color the potion color to look up
* @return the tipped arrow potion that most closely resembles that color.
*/
public static TippedArrowPotion getByJavaColor(int color) {
for (TippedArrowPotion potion : TippedArrowPotion.values()) {
if (potion.javaColor == color) {
return potion;
}
}
return null;
}
private enum ArrowParticleColors {
NONE(-1),
NIGHT_VISION(2039713),
INVISIBILITY(8356754),
LEAPING(2293580),
FIRE_RESISTANCE(14981690),
SWIFTNESS(8171462),
SLOWNESS(5926017),
TURTLE_MASTER(7691106),
WATER_BREATHING(3035801),
HEALING(16262179),
HARMING(4393481),
POISON(5149489),
REGENERATION(13458603),
STRENGTH(9643043),
WEAKNESS(4738376),
LUCK(3381504),
SLOW_FALLING(16773073);
@Getter
private final int color;
ArrowParticleColors(int color) {
this.color = color;
}
}
}

View File

@ -50,7 +50,7 @@ import java.util.stream.Collectors;
@ItemRemapper
public class BannerTranslator extends ItemTranslator {
private List<ItemEntry> appliedItems;
private final List<ItemEntry> appliedItems;
public BannerTranslator() {
appliedItems = ItemRegistry.ITEM_ENTRIES.values().stream().filter(entry -> entry.getJavaIdentifier().endsWith("banner")).collect(Collectors.toList());

View File

@ -40,7 +40,7 @@ import java.util.stream.Collectors;
@ItemRemapper
public class CompassTranslator extends ItemTranslator {
private List<ItemEntry> appliedItems;
private final List<ItemEntry> appliedItems;
public CompassTranslator() {
appliedItems = ItemRegistry.ITEM_ENTRIES.values().stream().filter(entry -> entry.getJavaIdentifier().endsWith("compass")).collect(Collectors.toList());

View File

@ -42,7 +42,7 @@ import java.util.stream.Collectors;
@ItemRemapper
public class PotionTranslator extends ItemTranslator {
private List<ItemEntry> appliedItems;
private final List<ItemEntry> appliedItems;
public PotionTranslator() {
appliedItems = ItemRegistry.ITEM_ENTRIES.values().stream().filter(entry -> entry.getJavaIdentifier().endsWith("potion")).collect(Collectors.toList());
@ -57,7 +57,7 @@ public class PotionTranslator extends ItemTranslator {
if (potion != null) {
return ItemData.of(itemEntry.getBedrockId(), potion.getBedrockId(), itemStack.getAmount(), translateNbtToBedrock(itemStack.getNbt()));
}
GeyserConnector.getInstance().getLogger().debug("Unknown java potion: " + potionTag.getValue());
GeyserConnector.getInstance().getLogger().debug("Unknown Java potion: " + potionTag.getValue());
}
return super.translateToBedrock(itemStack, itemEntry);
}

View File

@ -0,0 +1,87 @@
/*
* 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.item.translators;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.translators.ItemRemapper;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
import org.geysermc.connector.network.translators.item.TippedArrowPotion;
import java.util.List;
import java.util.stream.Collectors;
@ItemRemapper
public class TippedArrowTranslator extends ItemTranslator {
private final List<ItemEntry> appliedItems;
private static final int TIPPED_ARROW_JAVA_ID = ItemRegistry.getItemEntry("minecraft:tipped_arrow").getJavaId();
public TippedArrowTranslator() {
appliedItems = ItemRegistry.ITEM_ENTRIES.values().stream().filter(entry ->
entry.getJavaIdentifier().contains("arrow") && !entry.getJavaIdentifier().contains("spectral")).collect(Collectors.toList());
}
@Override
public ItemData translateToBedrock(ItemStack itemStack, ItemEntry itemEntry) {
if (!itemEntry.getJavaIdentifier().equals("minecraft:tipped_arrow") || itemStack.getNbt() == null) {
// We're only concerned about minecraft:arrow when translating Bedrock -> Java
return super.translateToBedrock(itemStack, itemEntry);
}
Tag potionTag = itemStack.getNbt().get("Potion");
if (potionTag instanceof StringTag) {
TippedArrowPotion tippedArrowPotion = TippedArrowPotion.getByJavaIdentifier(((StringTag) potionTag).getValue());
if (tippedArrowPotion != null) {
return ItemData.of(itemEntry.getBedrockId(), tippedArrowPotion.getBedrockId(), itemStack.getAmount(), translateNbtToBedrock(itemStack.getNbt()));
}
GeyserConnector.getInstance().getLogger().debug("Unknown Java potion (tipped arrow): " + potionTag.getValue());
}
return super.translateToBedrock(itemStack, itemEntry);
}
@Override
public ItemStack translateToJava(ItemData itemData, ItemEntry itemEntry) {
TippedArrowPotion tippedArrowPotion = TippedArrowPotion.getByBedrockId(itemData.getDamage());
ItemStack itemStack = super.translateToJava(itemData, itemEntry);
if (tippedArrowPotion != null) {
itemStack = new ItemStack(TIPPED_ARROW_JAVA_ID, itemStack.getAmount(), itemStack.getNbt());
StringTag potionTag = new StringTag("Potion", tippedArrowPotion.getJavaIdentifier());
itemStack.getNbt().put(potionTag);
}
return itemStack;
}
@Override
public List<ItemEntry> getAppliedItems() {
return appliedItems;
}
}

View File

@ -101,7 +101,7 @@ public class BasicItemTranslator extends NbtItemStackTranslator {
if (message.startsWith("§r")) {
message = message.replaceFirst("§r", "");
}
Component component = TextComponent.of(message);
Component component = Component.text(message);
return GsonComponentSerializer.gson().serialize(component);
}

View File

@ -25,14 +25,15 @@
package org.geysermc.connector.network.translators.item.translators.nbt;
import com.github.steveice10.opennbt.tag.builtin.ByteTag;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.ItemRemapper;
import org.geysermc.connector.network.translators.item.NbtItemStackTranslator;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
import org.geysermc.connector.network.translators.item.NbtItemStackTranslator;
@ItemRemapper
public class CrossbowTranslator extends NbtItemStackTranslator {
@ -44,12 +45,17 @@ public class CrossbowTranslator extends NbtItemStackTranslator {
if (!chargedProjectiles.getValue().isEmpty()) {
CompoundTag projectile = (CompoundTag) chargedProjectiles.getValue().get(0);
CompoundTag newProjectile = new CompoundTag("chargedItem");
newProjectile.put(new ByteTag("Count", (byte) projectile.get("Count").getValue()));
newProjectile.put(new StringTag("Name", (String) projectile.get("id").getValue()));
ItemEntry entry = ItemRegistry.getItemEntry((String) projectile.get("id").getValue());
if (entry == null) return;
CompoundTag tag = projectile.get("tag");
ItemStack itemStack = new ItemStack(itemEntry.getJavaId(), (byte) projectile.get("Count").getValue(), tag);
ItemData itemData = ItemTranslator.translateToBedrock(session, itemStack);
// Not sure what this is for
newProjectile.put(new ByteTag("Damage", (byte) 0));
CompoundTag newProjectile = new CompoundTag("chargedItem");
newProjectile.put(new ByteTag("Count", (byte) itemData.getCount()));
newProjectile.put(new StringTag("Name", ItemRegistry.getBedrockIdentifer(entry)));
newProjectile.put(new ShortTag("Damage", itemData.getDamage()));
itemTag.put(newProjectile);
}

View File

@ -59,7 +59,7 @@ public class JavaChatTranslator extends PacketTranslator<ServerChatPacket> {
break;
}
String locale = session.getClientData().getLanguageCode();
String locale = session.getLocale();
if (packet.getMessage() instanceof TranslationMessage) {
textPacket.setType(TextPacket.Type.TRANSLATION);

View File

@ -80,8 +80,19 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclare
}
break;
}
// These recipes are enabled by sending a special recipe
case CRAFTING_SPECIAL_BOOKCLONING: {
craftingDataPacket.getCraftingData().add(RecipeRegistry.BOOK_CLONING_RECIPE_DATA);
break;
}
case CRAFTING_SPECIAL_REPAIRITEM: {
craftingDataPacket.getCraftingData().add(RecipeRegistry.TOOL_REPAIRING_RECIPE_DATA);
break;
}
// Java doesn't actually tell us the recipes so we need to calculate this ahead of time.
case CRAFTING_SPECIAL_FIREWORK_ROCKET: {
// Java doesn't actually tell us the recipes so we need to calculate this ahead of time.
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.FIREWORK_ROCKET_RECIPES);
break;
}
@ -93,6 +104,14 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclare
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.SHULKER_BOX_DYEING_RECIPES);
break;
}
case CRAFTING_SPECIAL_SUSPICIOUSSTEW: {
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.SUSPICIOUS_STEW_RECIPES);
break;
}
case CRAFTING_SPECIAL_TIPPEDARROW: {
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.TIPPED_ARROW_RECIPES);
break;
}
case CRAFTING_SPECIAL_ARMORDYE: {
// This one's even worse since it's not actually on Bedrock, but it still works!
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.LEATHER_DYEING_RECIPES);

View File

@ -36,6 +36,6 @@ public class JavaDisconnectPacket extends PacketTranslator<ServerDisconnectPacke
@Override
public void translate(ServerDisconnectPacket packet, GeyserSession session) {
session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getClientData().getLanguageCode(), true));
session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getLocale(), true));
}
}

View File

@ -89,7 +89,7 @@ public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacke
session.setRenderDistance(packet.getViewDistance());
// We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
String locale = session.getClientData().getLanguageCode();
String locale = session.getLocale();
List<SkinPart> skinParts = Arrays.asList(SkinPart.values());
ClientSettingsPacket clientSettingsPacket = new ClientSettingsPacket(locale, (byte) session.getRenderDistance(), ChatVisibility.FULL, true, skinParts, HandPreference.RIGHT_HAND);
session.sendDownstreamPacket(clientSettingsPacket);

View File

@ -0,0 +1,48 @@
/*
* 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.java;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerKeepAlivePacket;
import com.nukkitx.protocol.bedrock.packet.NetworkStackLatencyPacket;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
/**
* Used to forward the keep alive packet to the client in order to get back a reliable ping.
*/
@Translator(packet = ServerKeepAlivePacket.class)
public class JavaKeepAliveTranslator extends PacketTranslator<ServerKeepAlivePacket> {
@Override
public void translate(ServerKeepAlivePacket packet, GeyserSession session) {
session.setLastKeepAliveTimestamp(packet.getPingId());
NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket();
latencyPacket.setFromServer(true);
latencyPacket.setTimestamp(packet.getPingId());
session.sendUpstreamPacket(latencyPacket);
}
}

View File

@ -37,6 +37,6 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator<LoginDisconn
@Override
public void translate(LoginDisconnectPacket packet, GeyserSession session) {
// The client doesn't manually get disconnected so we have to do it ourselves
session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getClientData().getLanguageCode()));
session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getLocale()));
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.java;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerStatisticsPacket;
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.StatisticsUtils;
@Translator(packet = ServerStatisticsPacket.class)
public class JavaStatisticsTranslator extends PacketTranslator<ServerStatisticsPacket> {
@Override
public void translate(ServerStatisticsPacket packet, GeyserSession session) {
session.updateStatistics(packet.getStatistics());
if (session.isWaitingForStatistics()) {
session.setWaitingForStatistics(false);
session.sendForm(StatisticsUtils.buildMenuForm(session), StatisticsUtils.STATISTICS_MENU_FORM_ID);
}
}
}

View File

@ -39,7 +39,7 @@ public class JavaTitleTranslator extends PacketTranslator<ServerTitlePacket> {
@Override
public void translate(ServerTitlePacket packet, GeyserSession session) {
SetTitlePacket titlePacket = new SetTitlePacket();
String locale = session.getClientData().getLanguageCode();
String locale = session.getLocale();
switch (packet.getAction()) {
case TITLE:

View File

@ -82,7 +82,6 @@ public class JavaEntitySetPassengersTranslator extends PacketTranslator<ServerEn
}
passenger.updateBedrockMetadata(session);
this.updateOffset(passenger, entity.getEntityType(), session, rider, true, (passengers.size() > 1));
rider = false;
}
@ -90,6 +89,9 @@ public class JavaEntitySetPassengersTranslator extends PacketTranslator<ServerEn
for (long passengerId : entity.getPassengers()) {
Entity passenger = session.getEntityCache().getEntityByJavaId(passengerId);
if (passengerId == session.getPlayerEntity().getEntityId()) {
passenger = session.getPlayerEntity();
}
if (passenger == null) {
continue;
}
@ -99,66 +101,160 @@ public class JavaEntitySetPassengersTranslator extends PacketTranslator<ServerEn
session.sendUpstreamPacket(linkPacket);
passengers.remove(passenger.getEntityId());
this.updateOffset(passenger, entity.getEntityType(), session, false, false, (passengers.size() > 1));
this.updateOffset(passenger, entity, session, false, false, (packet.getPassengerIds().length > 1));
} else {
this.updateOffset(passenger, entity, session, (packet.getPassengerIds()[0] == passengerId), true, (packet.getPassengerIds().length > 1));
}
// Force an update to the passenger metadata
passenger.updateBedrockMetadata(session);
}
if (entity.getEntityType() == EntityType.HORSE) {
entity.getMetadata().put(EntityData.RIDER_SEAT_POSITION, Vector3f.from(0.0f, 2.3200102f, -0.2f));
entity.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 181.0f);
entity.updateBedrockMetadata(session);
switch (entity.getEntityType()) {
case HORSE:
case SKELETON_HORSE:
case DONKEY:
case MULE:
case RAVAGER:
entity.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 181.0f);
entity.updateBedrockMetadata(session);
break;
}
}
private void updateOffset(Entity passenger, EntityType mountType, GeyserSession session, boolean rider, boolean riding, boolean moreThanOneEntity) {
// Without the Y offset, Bedrock players will find themselves in the floor when mounting
float yOffset = 0;
private float getMountedHeightOffset(Entity mount) {
final EntityType mountType = mount.getEntityType();
float mountedHeightOffset = mountType.getHeight() * 0.75f;
switch (mountType) {
case BOAT:
yOffset = passenger.getEntityType() == EntityType.PLAYER ? 1.02001f : -0.2f;
break;
case MINECART:
yOffset = passenger.getEntityType() == EntityType.PLAYER ? 1.02001f : 0f;
case CHICKEN:
case SPIDER:
mountedHeightOffset = mountType.getHeight() * 0.5f;
break;
case DONKEY:
yOffset = 2.1f;
break;
case HORSE:
case SKELETON_HORSE:
case ZOMBIE_HORSE:
case MULE:
yOffset = 2.3f;
mountedHeightOffset -= 0.25f;
break;
case LLAMA:
case TRADER_LLAMA:
yOffset = 2.5f;
mountedHeightOffset = mountType.getHeight() * 0.67f;
break;
case PIG:
yOffset = 1.85001f;
case MINECART:
case MINECART_HOPPER:
case MINECART_TNT:
case MINECART_CHEST:
case MINECART_FURNACE:
case MINECART_SPAWNER:
case MINECART_COMMAND_BLOCK:
mountedHeightOffset = 0;
break;
case ARMOR_STAND:
yOffset = 1.3f;
case BOAT:
mountedHeightOffset = -0.1f;
break;
case HOGLIN:
case ZOGLIN:
boolean isBaby = mount.getMetadata().getFlags().getFlag(EntityFlag.BABY);
mountedHeightOffset = mountType.getHeight() - (isBaby ? 0.2f : 0.15f);
break;
case PIGLIN:
mountedHeightOffset = mountType.getHeight() * 0.92f;
break;
case RAVAGER:
mountedHeightOffset = 2.1f;
break;
case SKELETON_HORSE:
mountedHeightOffset -= 0.1875f;
break;
case STRIDER:
yOffset = passenger.getEntityType() == EntityType.PLAYER ? 2.8200102f : 1.6f;
mountedHeightOffset = mountType.getHeight() - 0.19f;
break;
}
Vector3f offset = Vector3f.from(0f, yOffset, 0f);
if (mountType == EntityType.STRIDER) {
offset = offset.add(0f, 0f, -0.2f);
}
// Without the X offset, more than one entity on a boat is stacked on top of each other
if (rider && moreThanOneEntity) {
offset = offset.add(Vector3f.from(0.2, 0, 0));
} else if (moreThanOneEntity) {
offset = offset.add(Vector3f.from(-0.6, 0, 0));
return mountedHeightOffset;
}
private float getHeightOffset(Entity passenger) {
boolean isBaby;
switch (passenger.getEntityType()) {
case SKELETON:
case STRAY:
case WITHER_SKELETON:
return -0.6f;
case ARMOR_STAND:
// Armor stand isn't a marker
if (passenger.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT) != 0.0f) {
return 0.1f;
} else {
return 0.0f;
}
case ENDERMITE:
case SILVERFISH:
return 0.1f;
case PIGLIN:
case PIGLIN_BRUTE:
case ZOMBIFIED_PIGLIN:
isBaby = passenger.getMetadata().getFlags().getFlag(EntityFlag.BABY);
return isBaby ? -0.05f : -0.45f;
case ZOMBIE:
isBaby = passenger.getMetadata().getFlags().getFlag(EntityFlag.BABY);
return isBaby ? 0.0f : -0.45f;
case EVOKER:
case ILLUSIONER:
case PILLAGER:
case RAVAGER:
case VINDICATOR:
case WITCH:
return -0.45f;
case PLAYER:
return -0.35f;
}
return 0f;
}
private void updateOffset(Entity passenger, Entity mount, GeyserSession session, boolean rider, boolean riding, boolean moreThanOneEntity) {
passenger.getMetadata().getFlags().setFlag(EntityFlag.RIDING, riding);
if (riding) {
// Without the Y offset, Bedrock players will find themselves in the floor when mounting
float mountedHeightOffset = getMountedHeightOffset(mount);
float heightOffset = getHeightOffset(passenger);
float xOffset = 0;
float yOffset = mountedHeightOffset + heightOffset;
float zOffset = 0;
switch (mount.getEntityType()) {
case BOAT:
// Without the X offset, more than one entity on a boat is stacked on top of each other
if (rider && moreThanOneEntity) {
xOffset = 0.2f;
} else if (moreThanOneEntity) {
xOffset = -0.6f;
}
break;
case CHICKEN:
zOffset = -0.1f;
break;
case LLAMA:
zOffset = -0.3f;
break;
}
/*
* Bedrock Differences
* Zoglin & Hoglin seem to be taller in Bedrock edition
* Horses are tinier
* Players, Minecarts, and Boats have different origins
*/
if (passenger.getEntityType() == EntityType.PLAYER) {
yOffset += EntityType.PLAYER.getOffset();
}
switch (mount.getEntityType()) {
case MINECART:
case MINECART_HOPPER:
case MINECART_TNT:
case MINECART_CHEST:
case MINECART_FURNACE:
case MINECART_SPAWNER:
case MINECART_COMMAND_BLOCK:
case BOAT:
yOffset -= mount.getEntityType().getHeight() * 0.5f;
}
Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset);
passenger.getMetadata().put(EntityData.RIDER_SEAT_POSITION, offset);
}
passenger.updateBedrockMetadata(session);

View File

@ -25,17 +25,26 @@
package org.geysermc.connector.network.translators.java.entity;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.world.particle.ItemParticleData;
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityStatusPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.PlayerEntity;
import org.geysermc.connector.entity.type.EntityType;
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.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
@Translator(packet = ServerEntityStatusPacket.class)
public class JavaEntityStatusTranslator extends PacketTranslator<ServerEntityStatusPacket> {
@ -88,6 +97,22 @@ public class JavaEntityStatusTranslator extends PacketTranslator<ServerEntitySta
break;
case LIVING_DEATH:
entityEventPacket.setType(EntityEventType.DEATH);
if (entity.getEntityType() == EntityType.THROWN_EGG) {
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(LevelEventType.PARTICLE_ITEM_BREAK);
particlePacket.setData(ItemRegistry.EGG.getBedrockId() << 16);
particlePacket.setPosition(entity.getPosition());
for (int i = 0; i < 6; i++) {
session.sendUpstreamPacket(particlePacket);
}
} else if (entity.getEntityType() == EntityType.SNOWBALL) {
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(LevelEventType.PARTICLE_SNOWBALL_POOF);
particlePacket.setPosition(entity.getPosition());
for (int i = 0; i < 8; i++) {
session.sendUpstreamPacket(particlePacket);
}
}
break;
case WOLF_SHAKE_WATER:
entityEventPacket.setType(EntityEventType.SHAKE_WETNESS);
@ -96,8 +121,20 @@ public class JavaEntityStatusTranslator extends PacketTranslator<ServerEntitySta
entityEventPacket.setType(EntityEventType.USE_ITEM);
break;
case FISHING_HOOK_PULL_PLAYER:
entityEventPacket.setType(EntityEventType.FISH_HOOK_TEASE); //TODO: CHECK
break;
// Player is pulled from a fishing rod
// The physics of this are clientside on Java
long pulledById = entity.getMetadata().getLong(EntityData.TARGET_EID);
if (session.getPlayerEntity().getGeyserId() == pulledById) {
Entity hookOwner = session.getEntityCache().getEntityByGeyserId(entity.getMetadata().getLong(EntityData.OWNER_EID));
if (hookOwner != null) {
// https://minecraft.gamepedia.com/Fishing_Rod#Hooking_mobs_and_other_entities
SetEntityMotionPacket motionPacket = new SetEntityMotionPacket();
motionPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
motionPacket.setMotion(hookOwner.getPosition().sub(session.getPlayerEntity().getPosition()).mul(0.1f));
session.sendUpstreamPacket(motionPacket);
}
}
return;
case TAMEABLE_TAMING_FAILED:
entityEventPacket.setType(EntityEventType.TAME_FAILED);
break;

View File

@ -57,9 +57,8 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator<ServerPlayer
if (self) {
// Entity is ourself
playerEntity = session.getPlayerEntity();
SkinUtils.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape -> {
GeyserConnector.getInstance().getLogger().debug("Loading Local Bedrock Java Skin Data");
});
SkinUtils.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
GeyserConnector.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data"));
} else {
playerEntity = session.getEntityCache().getPlayerEntity(entry.getProfile().getId());
}

View File

@ -62,8 +62,8 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
.setName(MessageUtils.getBedrockMessage(packet.getDisplayName()))
.setColor(packet.getColor())
.setNameTagVisibility(packet.getNameTagVisibility())
.setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getClientData().getLanguageCode()))
.setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode()));
.setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getLocale()))
.setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getLocale()));
break;
case UPDATE:
if (team == null) {
@ -77,8 +77,8 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
team.setName(MessageUtils.getBedrockMessage(packet.getDisplayName()))
.setColor(packet.getColor())
.setNameTagVisibility(packet.getNameTagVisibility())
.setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getClientData().getLanguageCode()))
.setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode()))
.setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getLocale()))
.setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getLocale()))
.setUpdateType(UpdateType.UPDATE);
break;
case ADD_PLAYER:

View File

@ -25,18 +25,17 @@
package org.geysermc.connector.network.translators.java.window;
import com.github.steveice10.mc.protocol.data.message.MessageSerializer;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket;
import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerOpenWindowPacket;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.nukkitx.protocol.bedrock.packet.ContainerClosePacket;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.inventory.Inventory;
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.network.translators.inventory.InventoryTranslator;
import org.geysermc.connector.utils.InventoryUtils;
import org.geysermc.connector.utils.LocaleUtils;
import org.geysermc.connector.utils.MessageUtils;
@Translator(packet = ServerOpenWindowPacket.class)
public class JavaOpenWindowTranslator extends PacketTranslator<ServerOpenWindowPacket> {
@ -58,18 +57,10 @@ public class JavaOpenWindowTranslator extends PacketTranslator<ServerOpenWindowP
return;
}
String name = packet.getName();
try {
JsonParser parser = new JsonParser();
JsonObject jsonObject = parser.parse(packet.getName()).getAsJsonObject();
if (jsonObject.has("text")) {
name = jsonObject.get("text").getAsString();
} else if (jsonObject.has("translate")) {
name = jsonObject.get("translate").getAsString();
}
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().debug("JavaOpenWindowTranslator: " + e.toString());
}
String name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(packet.getName()),
session.getLocale());
name = LocaleUtils.getLocaleString(name, session.getLocale());
Inventory newInventory = new Inventory(name, packet.getWindowId(), packet.getType(), newTranslator.size + 36);
session.getInventoryCache().cacheInventory(newInventory);

View File

@ -25,6 +25,7 @@
package org.geysermc.connector.network.translators.java.world;
import com.github.steveice10.mc.protocol.data.game.chunk.Column;
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerChunkDataPacket;
import com.nukkitx.nbt.NBTOutputStream;
import com.nukkitx.nbt.NbtMap;
@ -32,8 +33,8 @@ import com.nukkitx.nbt.NbtUtils;
import com.nukkitx.network.VarInts;
import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.BiomeTranslator;
@ -66,57 +67,69 @@ public class JavaChunkDataTranslator extends PacketTranslator<ServerChunkDataPac
return;
}
// Non-full chunks don't have all the chunk data, and Bedrock won't accept that
final boolean isNonFullChunk = (packet.getColumn().getBiomeData() == null);
// Merge received column with cache on network thread
Column mergedColumn = session.getChunkCache().addToCache(packet.getColumn());
if (mergedColumn == null) { // There were no changes?!?
return;
}
boolean isNonFullChunk = packet.getColumn().getBiomeData() == null;
GeyserConnector.getInstance().getGeneralThreadPool().execute(() -> {
try {
ChunkUtils.ChunkData chunkData = ChunkUtils.translateToBedrock(session, packet.getColumn(), isNonFullChunk);
ByteBuf byteBuf = Unpooled.buffer(32);
ChunkSection[] sections = chunkData.sections;
ChunkUtils.ChunkData chunkData = ChunkUtils.translateToBedrock(session, mergedColumn, isNonFullChunk);
ChunkSection[] sections = chunkData.getSections();
// Find highest section
int sectionCount = sections.length - 1;
while (sectionCount >= 0 && sections[sectionCount].isEmpty()) {
while (sectionCount >= 0 && sections[sectionCount] == null) {
sectionCount--;
}
sectionCount++;
// Estimate chunk size
int size = 0;
for (int i = 0; i < sectionCount; i++) {
ChunkSection section = chunkData.sections[i];
section.writeToNetwork(byteBuf);
ChunkSection section = sections[i];
size += (section != null ? section : ChunkUtils.EMPTY_SECTION).estimateNetworkSize();
}
size += 256; // Biomes
size += 1; // Border blocks
size += 1; // Extra data length (always 0)
size += chunkData.getBlockEntities().length * 64; // Conservative estimate of 64 bytes per tile entity
byte[] bedrockBiome;
if (packet.getColumn().getBiomeData() == null) {
bedrockBiome = BiomeTranslator.toBedrockBiome(session.getConnector().getWorldManager().getBiomeDataAt(session, packet.getColumn().getX(), packet.getColumn().getZ()));
} else {
bedrockBiome = BiomeTranslator.toBedrockBiome(packet.getColumn().getBiomeData());
// Allocate output buffer
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(size);
byte[] payload;
try {
for (int i = 0; i < sectionCount; i++) {
ChunkSection section = sections[i];
(section != null ? section : ChunkUtils.EMPTY_SECTION).writeToNetwork(byteBuf);
}
byteBuf.writeBytes(BiomeTranslator.toBedrockBiome(mergedColumn.getBiomeData())); // Biomes - 256 bytes
byteBuf.writeByte(0); // Border blocks - Edu edition only
VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now
// Encode tile entities into buffer
NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf));
for (NbtMap blockEntity : chunkData.getBlockEntities()) {
nbtStream.writeTag(blockEntity);
}
// Copy data into byte[], because the protocol lib really likes things that are s l o w
byteBuf.readBytes(payload = new byte[byteBuf.readableBytes()]);
} finally {
byteBuf.release(); // Release buffer to allow buffer pooling to be useful
}
byteBuf.writeBytes(bedrockBiome); // Biomes - 256 bytes
byteBuf.writeByte(0); // Border blocks - Edu edition only
VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now
ByteBufOutputStream stream = new ByteBufOutputStream(Unpooled.buffer());
NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(stream);
for (NbtMap blockEntity : chunkData.getBlockEntities()) {
nbtStream.writeTag(blockEntity);
}
byteBuf.writeBytes(stream.buffer());
byte[] payload = new byte[byteBuf.writerIndex()];
byteBuf.readBytes(payload);
LevelChunkPacket levelChunkPacket = new LevelChunkPacket();
levelChunkPacket.setSubChunksLength(sectionCount);
levelChunkPacket.setCachingEnabled(false);
levelChunkPacket.setChunkX(packet.getColumn().getX());
levelChunkPacket.setChunkZ(packet.getColumn().getZ());
levelChunkPacket.setChunkX(mergedColumn.getX());
levelChunkPacket.setChunkZ(mergedColumn.getZ());
levelChunkPacket.setData(payload);
session.sendUpstreamPacket(levelChunkPacket);
session.getChunkCache().addToCache(packet.getColumn());
} catch (Exception ex) {
ex.printStackTrace();
}

View File

@ -103,8 +103,6 @@ public class JavaNotifyClientTranslator extends PacketTranslator<ServerNotifyCli
case CHANGE_GAMEMODE:
GameMode gameMode = (GameMode) packet.getValue();
session.setNoClip(gameMode == GameMode.SPECTATOR);
session.setWorldImmutable(gameMode == GameMode.ADVENTURE || gameMode == GameMode.SPECTATOR);
session.sendAdventureSettings();
SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket();
@ -145,7 +143,15 @@ public class JavaNotifyClientTranslator extends PacketTranslator<ServerNotifyCli
case INVALID_BED:
// Not sent as a proper message? Odd.
session.sendMessage(LocaleUtils.getLocaleString("block.minecraft.spawn.not_valid",
session.getClientData().getLanguageCode()));
session.getLocale()));
break;
case ARROW_HIT_PLAYER:
PlaySoundPacket arrowSoundPacket = new PlaySoundPacket();
arrowSoundPacket.setSound("random.orb");
arrowSoundPacket.setPitch(0.5f);
arrowSoundPacket.setVolume(0.5f);
arrowSoundPacket.setPosition(entity.getPosition());
session.sendUpstreamPacket(arrowSoundPacket);
break;
default:
break;

View File

@ -78,7 +78,7 @@ public class JavaPlayEffectTranslator extends PacketTranslator<ServerPlayEffectP
textPacket.setMessage("record.nowPlaying");
List<String> params = new ArrayList<>();
String recordString = "%item." + soundEvent.name().toLowerCase(Locale.ROOT) + ".desc";
params.add(LocaleUtils.getLocaleString(recordString, session.getClientData().getLanguageCode()));
params.add(LocaleUtils.getLocaleString(recordString, session.getLocale()));
textPacket.setParameters(params);
session.sendUpstreamPacket(textPacket);
}

View File

@ -36,6 +36,8 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.UpdateTradePacket;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
@ -60,14 +62,20 @@ public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPac
UpdateTradePacket updateTradePacket = new UpdateTradePacket();
updateTradePacket.setTradeTier(packet.getVillagerLevel() - 1);
updateTradePacket.setContainerId((short) packet.getWindowId()); //TODO: CHECK THIS AND THE ONE BELOW
updateTradePacket.setContainerId((short) packet.getWindowId());
updateTradePacket.setContainerType(ContainerType.TRADE);
Inventory openInv = session.getInventoryCache().getOpenInventory();
String displayName;
Entity realVillager = session.getEntityCache().getEntityByGeyserId(session.getLastInteractedVillagerEid());
if (realVillager != null && realVillager.getMetadata().containsKey(EntityData.NAMETAG) && realVillager.getMetadata().getString(EntityData.NAMETAG) != null) {
displayName = realVillager.getMetadata().getString(EntityData.NAMETAG);
if (openInv != null && openInv.getId() == packet.getWindowId()) {
displayName = openInv.getTitle();
} else {
displayName = packet.isRegularVillager() ? "Villager" : "Wandering Trader";
Entity realVillager = session.getEntityCache().getEntityByGeyserId(session.getLastInteractedVillagerEid());
if (realVillager != null && realVillager.getMetadata().containsKey(EntityData.NAMETAG) && realVillager.getMetadata().getString(EntityData.NAMETAG) != null) {
displayName = realVillager.getMetadata().getString(EntityData.NAMETAG);
} else {
displayName = realVillager != null &&
realVillager.getEntityType() == EntityType.WANDERING_TRADER ? "Wandering Trader" : "Villager";
}
}
updateTradePacket.setDisplayName(displayName);
updateTradePacket.setSize(0);
@ -87,7 +95,7 @@ public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPac
recipe.putInt("buyCountB", trade.getSecondInput() != null ? trade.getSecondInput().getAmount() : 0);
recipe.putInt("buyCountA", trade.getFirstInput().getAmount());
recipe.putInt("demand", trade.getDemand());
recipe.putInt("tier", packet.getVillagerLevel() - 1);
recipe.putInt("tier", packet.getVillagerLevel() > 0 ? packet.getVillagerLevel() - 1 : 0); // -1 crashes client
recipe.put("buyA", getItemTag(session, trade.getFirstInput(), trade.getSpecialPrice()));
if (trade.getSecondInput() != null) {
recipe.put("buyB", getItemTag(session, trade.getSecondInput(), 0));

View File

@ -28,7 +28,6 @@ package org.geysermc.connector.network.translators.java.world;
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.network.translators.world.chunk.ChunkPosition;
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUnloadChunkPacket;
@ -37,6 +36,6 @@ public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChun
@Override
public void translate(ServerUnloadChunkPacket packet, GeyserSession session) {
session.getChunkCache().removeChunk(new ChunkPosition(packet.getX(), packet.getZ()));
session.getChunkCache().removeChunk(packet.getX(), packet.getZ());
}
}

View File

@ -40,7 +40,7 @@ public class SoundHandlerRegistry {
static final Map<SoundHandler, SoundInteractionHandler<?>> INTERACTION_HANDLERS = new HashMap<>();
static {
Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.sound") : new Reflections("org.geysermc.connector.network.translators.sound");
Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators.sound") : new Reflections("org.geysermc.connector.network.translators.sound");
for (Class<?> clazz : ref.getTypesAnnotatedWith(SoundHandler.class)) {
try {
SoundInteractionHandler<?> interactionHandler = (SoundInteractionHandler<?>) clazz.newInstance();

View File

@ -38,6 +38,7 @@ public class BucketSoundInteractionHandler implements BlockSoundInteractionHandl
@Override
public void handleInteraction(GeyserSession session, Vector3f position, String identifier) {
if (session.getBucketScheduledFuture() == null) return; // No bucket was really interacted with
String handItemIdentifier = ItemRegistry.getItem(session.getInventory().getItemInHand()).getJavaIdentifier();
LevelSoundEventPacket soundEventPacket = new LevelSoundEventPacket();
soundEventPacket.setPosition(position);
@ -68,5 +69,6 @@ public class BucketSoundInteractionHandler implements BlockSoundInteractionHandl
soundEventPacket.setSound(soundEvent);
session.sendUpstreamPacket(soundEventPacket);
}
session.setBucketScheduledFuture(null);
}
}

View File

@ -37,6 +37,7 @@ public class DoorSoundInteractionHandler implements BlockSoundInteractionHandler
@Override
public void handleInteraction(GeyserSession session, Vector3f position, String identifier) {
if (identifier.contains("iron")) return;
LevelEventPacket levelEventPacket = new LevelEventPacket();
levelEventPacket.setType(LevelEventType.SOUND_DOOR_OPEN);
levelEventPacket.setPosition(position);

View File

@ -25,14 +25,15 @@
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.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.chunk.Column;
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.network.translators.world.chunk.ChunkPosition;
import org.geysermc.connector.network.session.cache.ChunkCache;
import org.geysermc.connector.utils.GameRule;
public class GeyserWorldManager extends WorldManager {
@ -41,14 +42,50 @@ public class GeyserWorldManager extends WorldManager {
@Override
public int getBlockAt(GeyserSession session, int x, int y, int z) {
return session.getChunkCache().getBlockAt(new Position(x, y, z));
ChunkCache chunkCache = session.getChunkCache();
if (chunkCache != null) { // Chunk cache can be null if the session is closed asynchronously
return chunkCache.getBlockAt(x, y, z);
}
return 0;
}
@Override
public void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk chunk) {
ChunkCache chunkCache = session.getChunkCache();
Column cachedColumn;
Chunk cachedChunk;
if (chunkCache == null || (cachedColumn = chunkCache.getChunk(x, z)) == null || (cachedChunk = cachedColumn.getChunks()[y]) == null) {
return;
}
// Copy state IDs from cached chunk to output chunk
for (int blockY = 0; blockY < 16; blockY++) { // Cache-friendly iteration order
for (int blockZ = 0; blockZ < 16; blockZ++) {
for (int blockX = 0; blockX < 16; blockX++) {
chunk.set(blockX, blockY, blockZ, cachedChunk.get(blockX, blockY, blockZ));
}
}
}
}
@Override
public boolean hasMoreBlockDataThanChunkCache() {
// This implementation can only fetch data from the session chunk cache
return false;
}
@Override
public int[] getBiomeDataAt(GeyserSession session, int x, int z) {
if (!session.getConnector().getConfig().isCacheChunks())
return new int[1024];
return session.getChunkCache().getChunks().get(new ChunkPosition(x, z)).getBiomeData();
if (session.getConnector().getConfig().isCacheChunks()) {
ChunkCache chunkCache = session.getChunkCache();
if (chunkCache != null) { // Chunk cache can be null if the session is closed asynchronously
Column column = chunkCache.getChunk(x, z);
if (column != null) { // Column can be null if the server sent a partial chunk update before the first ground-up-continuous one
return column.getBiomeData();
}
}
}
return new int[1024];
}
@Override

View File

@ -25,6 +25,7 @@
package org.geysermc.connector.network.translators.world;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
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;
@ -74,6 +75,27 @@ public abstract class WorldManager {
*/
public abstract int getBlockAt(GeyserSession session, int x, int y, int z);
/**
* Gets all block states in the specified chunk section.
*
* @param session the session
* @param x the chunk's X coordinate
* @param y the chunk's Y coordinate
* @param z the chunk's Z coordinate
* @param section the chunk section to store the block data in
*/
public abstract void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk section);
/**
* Checks whether or not this world manager has access to more block data than the chunk cache.
* <p>
* Some world managers (e.g. Spigot) can provide access to block data outside of the chunk cache, and even with chunk caching disabled. This
* method provides a means to check if this manager has this capability.
*
* @return whether or not this world manager has access to more block data than the chunk cache
*/
public abstract boolean hasMoreBlockDataThanChunkCache();
/**
* Gets the biome data for the specified chunk.
*

View File

@ -67,6 +67,11 @@ public class BlockTranslator {
public static final Int2BooleanMap JAVA_RUNTIME_ID_TO_CAN_HARVEST_WITH_HAND = new Int2BooleanOpenHashMap();
public static final Int2ObjectMap<String> JAVA_RUNTIME_ID_TO_TOOL_TYPE = new Int2ObjectOpenHashMap<>();
/**
* Java numeric ID to java unique identifier, used for block names in the statistics screen
*/
public static final Int2ObjectMap<String> JAVA_ID_TO_JAVA_IDENTIFIER_MAP = new Int2ObjectOpenHashMap<>();
/**
* Runtime command block ID, used for fixing command block minecart appearances
*/
@ -113,7 +118,7 @@ public class BlockTranslator {
addedStatesMap.defaultReturnValue(-1);
List<NbtMap> paletteList = new ArrayList<>();
Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity");
Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity");
ref.getTypesAnnotatedWith(BlockEntity.class);
int waterRuntimeId = -1;
@ -124,6 +129,7 @@ public class BlockTranslator {
int furnaceRuntimeId = -1;
int furnaceLitRuntimeId = -1;
int spawnerRuntimeId = -1;
int uniqueJavaId = -1;
Iterator<Map.Entry<String, JsonNode>> blocksIterator = blocks.fields();
while (blocksIterator.hasNext()) {
javaRuntimeId++;
@ -166,6 +172,11 @@ public class BlockTranslator {
String cleanJavaIdentifier = entry.getKey().split("\\[")[0];
if (!JAVA_ID_TO_JAVA_IDENTIFIER_MAP.containsValue(cleanJavaIdentifier)) {
uniqueJavaId++;
JAVA_ID_TO_JAVA_IDENTIFIER_MAP.put(uniqueJavaId, cleanJavaIdentifier);
}
if (!cleanJavaIdentifier.equals(bedrockIdentifier)) {
JAVA_TO_BEDROCK_IDENTIFIERS.put(cleanJavaIdentifier, bedrockIdentifier);
}

View File

@ -68,7 +68,7 @@ public abstract class BlockEntityTranslator {
}
static {
Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity");
Reflections ref = GeyserConnector.getInstance().useXmlReflections() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity");
for (Class<?> clazz : ref.getTypesAnnotatedWith(BlockEntity.class)) {
GeyserConnector.getInstance().getLogger().debug("Found annotated block entity: " + clazz.getCanonicalName());

View File

@ -29,14 +29,16 @@ import com.nukkitx.network.VarInts;
import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
import java.util.function.IntConsumer;
@Getter
public class BlockStorage {
private static final int SIZE = 4096;
public static final int SIZE = 4096;
private final IntList palette;
private BitArray bitArray;
@ -46,12 +48,12 @@ public class BlockStorage {
}
public BlockStorage(BitArrayVersion version) {
this.bitArray = version.createPalette(SIZE);
this.bitArray = version.createArray(SIZE);
this.palette = new IntArrayList(16);
this.palette.add(0); // Air is at the start of every palette.
}
private BlockStorage(BitArray bitArray, IntArrayList palette) {
public BlockStorage(BitArray bitArray, IntList palette) {
this.palette = palette;
this.bitArray = bitArray;
}
@ -64,16 +66,16 @@ public class BlockStorage {
return BitArrayVersion.get(header >> 1, true);
}
public synchronized int getFullBlock(int index) {
public int getFullBlock(int index) {
return this.palette.getInt(this.bitArray.get(index));
}
public synchronized void setFullBlock(int index, int runtimeId) {
public void setFullBlock(int index, int runtimeId) {
int idx = this.idFor(runtimeId);
this.bitArray.set(index, idx);
}
public synchronized void writeToNetwork(ByteBuf buffer) {
public void writeToNetwork(ByteBuf buffer) {
buffer.writeByte(getPaletteHeader(bitArray.getVersion(), true));
for (int word : bitArray.getWords()) {
@ -84,8 +86,18 @@ public class BlockStorage {
palette.forEach((IntConsumer) id -> VarInts.writeInt(buffer, id));
}
public int estimateNetworkSize() {
int size = 1; // Palette header
size += this.bitArray.getWords().length * 4;
// We assume that none of the VarInts will be larger than 3 bytes
size += 3; // Palette size
size += this.palette.size() * 3;
return size;
}
private void onResize(BitArrayVersion version) {
BitArray newBitArray = version.createPalette(SIZE);
BitArray newBitArray = version.createArray(SIZE);
for (int i = 0; i < SIZE; i++) {
newBitArray.set(i, this.bitArray.get(i));

View File

@ -27,42 +27,19 @@ package org.geysermc.connector.network.translators.world.chunk;
import com.nukkitx.network.util.Preconditions;
import io.netty.buffer.ByteBuf;
import lombok.Synchronized;
public class ChunkSection {
private static final int CHUNK_SECTION_VERSION = 8;
public static final int SIZE = 4096;
private final BlockStorage[] storage;
private final NibbleArray blockLight;
private final NibbleArray skyLight;
public ChunkSection() {
this(new BlockStorage[]{new BlockStorage(), new BlockStorage()}, new NibbleArray(SIZE),
new NibbleArray(SIZE));
this(new BlockStorage[]{new BlockStorage(), new BlockStorage()});
}
public ChunkSection(BlockStorage[] blockStorage) {
this(blockStorage, new NibbleArray(SIZE), new NibbleArray(SIZE));
}
public ChunkSection(BlockStorage[] storage, byte[] blockLight, byte[] skyLight) {
Preconditions.checkNotNull(storage, "storage");
Preconditions.checkArgument(storage.length > 1, "Block storage length must be at least 2");
for (BlockStorage blockStorage : storage) {
Preconditions.checkNotNull(blockStorage, "storage");
}
public ChunkSection(BlockStorage[] storage) {
this.storage = storage;
this.blockLight = new NibbleArray(blockLight);
this.skyLight = new NibbleArray(skyLight);
}
private ChunkSection(BlockStorage[] storage, NibbleArray blockLight, NibbleArray skyLight) {
this.storage = storage;
this.blockLight = blockLight;
this.skyLight = skyLight;
}
public int getFullBlock(int x, int y, int z, int layer) {
@ -77,30 +54,6 @@ public class ChunkSection {
this.storage[layer].setFullBlock(blockPosition(x, y, z), fullBlock);
}
@Synchronized("skyLight")
public byte getSkyLight(int x, int y, int z) {
checkBounds(x, y, z);
return this.skyLight.get(blockPosition(x, y, z));
}
@Synchronized("skyLight")
public void setSkyLight(int x, int y, int z, byte val) {
checkBounds(x, y, z);
this.skyLight.set(blockPosition(x, y, z), val);
}
@Synchronized("blockLight")
public byte getBlockLight(int x, int y, int z) {
checkBounds(x, y, z);
return this.blockLight.get(blockPosition(x, y, z));
}
@Synchronized("blockLight")
public void setBlockLight(int x, int y, int z, byte val) {
checkBounds(x, y, z);
this.blockLight.set(blockPosition(x, y, z), val);
}
public void writeToNetwork(ByteBuf buffer) {
buffer.writeByte(CHUNK_SECTION_VERSION);
buffer.writeByte(this.storage.length);
@ -109,12 +62,12 @@ public class ChunkSection {
}
}
public NibbleArray getSkyLightArray() {
return skyLight;
}
public NibbleArray getBlockLightArray() {
return blockLight;
public int estimateNetworkSize() {
int size = 2; // Version + storage count
for (BlockStorage blockStorage : this.storage) {
size += blockStorage.estimateNetworkSize();
}
return size;
}
public BlockStorage[] getBlockStorageArray() {
@ -135,7 +88,7 @@ public class ChunkSection {
for (int i = 0; i < storage.length; i++) {
storage[i] = this.storage[i].copy();
}
return new ChunkSection(storage, skyLight.copy(), blockLight.copy());
return new ChunkSection(storage);
}
public static int blockPosition(int x, int y, int z) {

View File

@ -37,6 +37,8 @@ public enum BitArrayVersion {
V2(2, 16, V3),
V1(1, 32, V2);
private static final BitArrayVersion[] VALUES = values();
final byte bits;
final byte entriesPerWord;
final int maxEntryValue;
@ -58,8 +60,14 @@ public enum BitArrayVersion {
throw new IllegalArgumentException("Invalid palette version: " + version);
}
public BitArray createPalette(int size) {
return this.createPalette(size, new int[MathUtils.ceil((float) size / entriesPerWord)]);
public static BitArrayVersion forBitsCeil(int bits) {
for (int i = VALUES.length - 1; i >= 0; i--) {
BitArrayVersion version = VALUES[i];
if (version.bits >= bits) {
return version;
}
}
return null;
}
public byte getId() {
@ -78,7 +86,11 @@ public enum BitArrayVersion {
return next;
}
public BitArray createPalette(int size, int[] words) {
public BitArray createArray(int size) {
return this.createArray(size, new int[MathUtils.ceil((float) size / entriesPerWord)]);
}
public BitArray createArray(int size, int[] words) {
if (this == V3 || this == V5 || this == V6) {
// Padded palettes aren't able to use bitwise operations due to their padding.
return new PaddedBitArray(this, size, words);

View File

@ -76,7 +76,8 @@ public class Objective {
if (!scores.containsKey(id)) {
Score score1 = new Score(this, id)
.setScore(score)
.setTeam(scoreboard.getTeamFor(id));
.setTeam(scoreboard.getTeamFor(id))
.setUpdateType(UpdateType.ADD);
scores.put(id, score1);
}
}
@ -96,15 +97,6 @@ public class Objective {
return 0;
}
public Score getScore(int line) {
for (Score score : scores.values()) {
if (score.getScore() == line) {
return score;
}
}
return null;
}
public void removeScore(String id) {
if (scores.containsKey(id)) {
scores.get(id).setUpdateType(UpdateType.REMOVE);

View File

@ -49,7 +49,6 @@ public class Score {
this.id = objective.getScoreboard().getNextId().getAndIncrement();
this.objective = objective;
this.name = name;
update();
}
public String getDisplayName() {

View File

@ -122,7 +122,7 @@ public class Scoreboard {
for (Objective objective : objectives.values()) {
if (!objective.isActive()) {
logger.debug("Ignoring non-active Scoreboard Objective '"+ objective.getObjectiveName() +'\'');
logger.debug("Ignoring non-active Scoreboard Objective '" + objective.getObjectiveName() + '\'');
continue;
}
@ -152,10 +152,6 @@ public class Scoreboard {
boolean globalAdd = objective.getUpdateType() == ADD;
boolean globalRemove = objective.getUpdateType() == REMOVE;
// Track if any scores changed
// Used to delete and resend scoreboard objectives; otherwise they won't update on Bedrock
boolean scoreChanged = false;
for (Score score : objective.getScores().values()) {
Team team = score.getTeam();
@ -171,16 +167,21 @@ public class Scoreboard {
teamChanged |= team.getUpdateType() == UPDATE;
add |= team.getUpdateType() == ADD || team.getUpdateType() == UPDATE;
remove |= team.getUpdateType() == REMOVE;
remove |= team.getUpdateType() != NOTHING;
}
add |= score.getUpdateType() == ADD || score.getUpdateType() == UPDATE;
remove |= score.getUpdateType() == REMOVE;
if (score.getUpdateType() == REMOVE) {
remove |= score.getUpdateType() == REMOVE || score.getUpdateType() == UPDATE;
if (score.getUpdateType() == REMOVE || globalRemove) {
add = false;
}
if (score.getUpdateType() == UPDATE || teamChanged) {
if (score.getUpdateType() == ADD) {
remove = false;
}
if (score.getUpdateType() == ADD || score.getUpdateType() == UPDATE || teamChanged) {
score.update();
}
@ -191,12 +192,6 @@ public class Scoreboard {
removeScores.add(score.getCachedInfo());
}
if (add || remove) {
scoreChanged = true;
}
// score is pending to be updated, so we use the current score as the old score
score.setOldScore(score.getScore());
// score is pending to be removed, so we can remove it from the objective
if (score.getUpdateType() == REMOVE) {
objective.removeScore0(score.getName());
@ -205,17 +200,17 @@ public class Scoreboard {
score.setUpdateType(NOTHING);
}
if (globalRemove || globalUpdate || scoreChanged) {
if (globalRemove || globalUpdate) {
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
if (objective.getUpdateType() == REMOVE) {
if (globalRemove) {
objectives.remove(objective.getObjectiveName()); // now we can deregister
objective.removed();
}
}
if (globalAdd || globalUpdate || scoreChanged) {
if ((globalAdd || globalUpdate) && !globalRemove) {
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
displayObjectivePacket.setDisplayName(objective.getDisplayName());

View File

@ -28,6 +28,7 @@ package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.data.game.entity.Effect;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.item.ItemEntry;
@ -138,4 +139,28 @@ public class BlockUtils {
return calculateBreakTime(blockHardness, toolTier, canHarvestWithHand, correctTool, toolType, isWoolBlock, isCobweb, toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, outOfWaterButNotOnGround, insideWaterNotOnGround);
}
/**
* Given a position, return the position if a block were located on the specified block face.
* @param blockPos the block position
* @param face the face of the block - see {@link com.github.steveice10.mc.protocol.data.game.world.block.BlockFace}
* @return the block position with the block face accounted for
*/
public static Vector3i getBlockPosition(Vector3i blockPos, int face) {
switch (face) {
case 0:
return blockPos.sub(0, 1, 0);
case 1:
return blockPos.add(0, 1, 0);
case 2:
return blockPos.sub(0, 0, 1);
case 3:
return blockPos.add(0, 0, 1);
case 4:
return blockPos.sub(1, 0, 0);
case 5:
return blockPos.add(1, 0, 0);
}
return blockPos;
}
}

View File

@ -25,8 +25,10 @@
package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.chunk.Column;
import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
@ -36,27 +38,39 @@ import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.nbt.NBTOutputStream;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtUtils;
import com.nukkitx.protocol.bedrock.packet.*;
import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
import com.nukkitx.protocol.bedrock.packet.NetworkChunkPublisherUpdatePacket;
import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
import lombok.Data;
import lombok.experimental.UtilityClass;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import org.geysermc.connector.network.translators.world.block.entity.*;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.chunk.ChunkPosition;
import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
import org.geysermc.connector.network.translators.world.block.entity.RequiresBlockState;
import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.AIR;
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.BEDROCK_WATER_ID;
import static org.geysermc.connector.network.translators.world.block.BlockTranslator.*;
@UtilityClass
public class ChunkUtils {
/**
@ -67,6 +81,9 @@ public class ChunkUtils {
private static final NbtMap EMPTY_TAG = NbtMap.builder().build();
public static final byte[] EMPTY_LEVEL_CHUNK_DATA;
public static final BlockStorage EMPTY_STORAGE = new BlockStorage();
public static final ChunkSection EMPTY_SECTION = new ChunkSection(new BlockStorage[]{ EMPTY_STORAGE });
static {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size
@ -76,73 +93,144 @@ public class ChunkUtils {
}
EMPTY_LEVEL_CHUNK_DATA = outputStream.toByteArray();
}catch (IOException e) {
} catch (IOException e) {
throw new AssertionError("Unable to generate empty level chunk data");
}
}
public static ChunkData translateToBedrock(GeyserSession session, Column column, boolean isNonFullChunk) {
ChunkData chunkData = new ChunkData();
Chunk[] chunks = column.getChunks();
chunkData.sections = new ChunkSection[chunks.length];
private static int indexYZXtoXZY(int yzx) {
return (yzx >> 8) | (yzx & 0x0F0) | ((yzx & 0x00F) << 8);
}
CompoundTag[] blockEntities = column.getTileEntities();
// Temporarily stores positions of BlockState values per chunk load
Object2IntMap<Position> blockEntityPositions = new Object2IntOpenHashMap<>();
public static ChunkData translateToBedrock(GeyserSession session, Column column, boolean isNonFullChunk) {
Chunk[] javaSections = column.getChunks();
ChunkSection[] sections = new ChunkSection[javaSections.length];
// Temporarily stores compound tags of Bedrock-only block entities
ObjectArrayList<NbtMap> bedrockOnlyBlockEntities = new ObjectArrayList<>();
List<NbtMap> bedrockOnlyBlockEntities = Collections.emptyList();
for (int chunkY = 0; chunkY < chunks.length; chunkY++) {
chunkData.sections[chunkY] = new ChunkSection();
Chunk chunk = chunks[chunkY];
BitSet waterloggedPaletteIds = new BitSet();
BitSet pistonOrFlowerPaletteIds = new BitSet();
// Chunk is null and caching chunks is off or this isn't a non-full chunk
if (chunk == null && (!session.getConnector().getConfig().isCacheChunks() || !isNonFullChunk))
boolean worldManagerHasMoreBlockDataThanCache = session.getConnector().getWorldManager().hasMoreBlockDataThanChunkCache();
// If the received packet was a full chunk update, null sections in the chunk are guaranteed to also be null in the world manager
boolean shouldCheckWorldManagerOnMissingSections = isNonFullChunk && worldManagerHasMoreBlockDataThanCache;
Chunk temporarySection = null;
for (int sectionY = 0; sectionY < javaSections.length; sectionY++) {
Chunk javaSection = javaSections[sectionY];
// Section is null, the cache will not contain anything of use
if (javaSection == null) {
// The column parameter contains all data currently available from the cache. If the chunk is null and the world manager
// reports the ability to access more data than the cache, attempt to fetch from the world manager instead.
if (shouldCheckWorldManagerOnMissingSections) {
// Ensure that temporary chunk is set
if (temporarySection == null) {
temporarySection = new Chunk();
}
// Read block data in section
session.getConnector().getWorldManager().getBlocksInSection(session, column.getX(), sectionY, column.getZ(), temporarySection);
if (temporarySection.isEmpty()) {
// The world manager only contains air for the given section
// We can leave temporarySection as-is to allow it to potentially be re-used for later sections
continue;
} else {
javaSection = temporarySection;
// Section contents have been modified, we can't re-use it
temporarySection = null;
}
} else {
continue;
}
}
// No need to encode an empty section...
if (javaSection.isEmpty()) {
continue;
}
// If chunk is empty then no need to process
if (chunk != null && chunk.isEmpty())
continue;
Palette javaPalette = javaSection.getPalette();
IntList bedrockPalette = new IntArrayList(javaPalette.size());
waterloggedPaletteIds.clear();
pistonOrFlowerPaletteIds.clear();
ChunkSection section = chunkData.sections[chunkY];
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
int blockState;
// If a non-full chunk, then grab the block that should be here to create a 'full' chunk
if (chunk == null) {
Position pos = new ChunkPosition(column.getX(), column.getZ()).getBlock(x, (chunkY << 4) + y, z);
blockState = session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ());
} else {
blockState = chunk.get(x, y, z);
}
int id = BlockTranslator.getBedrockBlockId(blockState);
// Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
for (int i = 0; i < javaPalette.size(); i++) {
int javaId = javaPalette.idToState(i);
bedrockPalette.add(BlockTranslator.getBedrockBlockId(javaId));
// Check to see if the name is in BlockTranslator.getBlockEntityString, and therefore must be handled differently
if (BlockTranslator.getBlockEntityString(blockState) != null) {
Position pos = new ChunkPosition(column.getX(), column.getZ()).getBlock(x, (chunkY << 4) + y, z);
blockEntityPositions.put(pos, blockState);
}
if (BlockTranslator.isWaterlogged(javaId)) {
waterloggedPaletteIds.set(i);
}
section.getBlockStorageArray()[0].setFullBlock(ChunkSection.blockPosition(x, y, z), id);
// Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId)) {
pistonOrFlowerPaletteIds.set(i);
}
}
// Check if block is piston or flower - only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(blockState) ||
BlockStateValues.getPistonValues().containsKey(blockState)) {
Position pos = new ChunkPosition(column.getX(), column.getZ()).getBlock(x, (chunkY << 4) + y, z);
bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(Vector3i.from(pos.getX(), pos.getY(), pos.getZ()), blockState));
}
BitStorage javaData = javaSection.getStorage();
if (BlockTranslator.isWaterlogged(blockState)) {
section.getBlockStorageArray()[1].setFullBlock(ChunkSection.blockPosition(x, y, z), BEDROCK_WATER_ID);
}
// Add Bedrock-exclusive block entities
// We only if the palette contained any blocks that are Bedrock-exclusive block entities to avoid iterating through the whole block data
// for no reason, as most sections will not contain any pistons or flower pots
if (!pistonOrFlowerPaletteIds.isEmpty()) {
bedrockOnlyBlockEntities = new ArrayList<>();
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
if (pistonOrFlowerPaletteIds.get(paletteId)) {
bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(
Vector3i.from((column.getX() << 4) + (yzx & 0xF), (sectionY << 4) + ((yzx >> 8) & 0xF), (column.getZ() << 4) + ((yzx >> 4) & 0xF)),
javaPalette.idToState(paletteId)
));
}
}
}
BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE);
BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
BlockStorage[] layers;
// Convert data array from YZX to XZY coordinate order
if (waterloggedPaletteIds.isEmpty()) {
// No blocks are waterlogged, simply convert coordinate order
// This could probably be optimized further...
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx));
}
layers = new BlockStorage[]{ layer0 };
} else {
// The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
// layer 1 with palette ID 1 indicating water
int[] layer1Data = new int[BlockStorage.SIZE >> 5];
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
if (waterloggedPaletteIds.get(paletteId)) {
layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
}
}
// V1 palette
IntList layer1Palette = new IntArrayList(2);
layer1Palette.add(0); // Air
layer1Palette.add(BEDROCK_WATER_ID);
layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
}
sections[sectionY] = new ChunkSection(layers);
}
CompoundTag[] blockEntities = column.getTileEntities();
NbtMap[] bedrockBlockEntities = new NbtMap[blockEntities.length + bedrockOnlyBlockEntities.size()];
int i = 0;
while (i < blockEntities.length) {
@ -156,7 +244,7 @@ public class ChunkUtils {
for (Tag subTag : tag) {
if (subTag instanceof StringTag) {
StringTag stringTag = (StringTag) subTag;
if (stringTag.getValue().equals("")) {
if (stringTag.getValue().isEmpty()) {
tagName = stringTag.getName();
break;
}
@ -170,17 +258,25 @@ public class ChunkUtils {
String id = BlockEntityUtils.getBedrockBlockEntityId(tagName);
BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id);
Position pos = new Position((int) tag.get("x").getValue(), (int) tag.get("y").getValue(), (int) tag.get("z").getValue());
int blockState = blockEntityPositions.getOrDefault(pos, 0);
// Get Java blockstate ID from block entity position
int blockState = 0;
Chunk section = column.getChunks()[pos.getY() >> 4];
if (section != null) {
blockState = section.get(pos.getX() & 0xF, pos.getY() & 0xF, pos.getZ() & 0xF);
}
bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState);
i++;
}
// Append Bedrock-exclusive block entities to output array
for (NbtMap tag : bedrockOnlyBlockEntities) {
bedrockBlockEntities[i] = tag;
i++;
}
chunkData.blockEntities = bedrockBlockEntities;
return chunkData;
return new ChunkData(sections, bedrockBlockEntities);
}
public static void updateChunkPosition(GeyserSession session, Vector3i position) {
@ -250,7 +346,7 @@ public class ChunkUtils {
break; //No block will be a part of two classes
}
}
session.getChunkCache().updateBlock(new Position(position.getX(), position.getY(), position.getZ()), blockState);
session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
}
public static void sendEmptyChunks(GeyserSession session, Vector3i position, int radius, boolean forceUpdate) {
@ -278,10 +374,10 @@ public class ChunkUtils {
}
}
@Data
public static final class ChunkData {
public ChunkSection[] sections;
private final ChunkSection[] sections;
@Getter
private NbtMap[] blockEntities = new NbtMap[0];
private final NbtMap[] blockEntities;
}
}

View File

@ -59,6 +59,8 @@ public class LanguageUtils {
*/
public static void loadGeyserLocale(String locale) {
locale = formatLocale(locale);
// Don't load the locale if it's already loaded.
if (LOCALE_MAPPINGS.containsKey(locale)) return;
InputStream localeStream = GeyserConnector.class.getClassLoader().getResourceAsStream("languages/texts/" + locale + ".properties");
@ -131,7 +133,7 @@ public class LanguageUtils {
* @param locale The locale to format
* @return The formatted locale
*/
private static String formatLocale(String locale) {
public static String formatLocale(String locale) {
try {
String[] parts = locale.toLowerCase().split("_");
return parts[0] + "_" + parts[1].toUpperCase();

View File

@ -35,7 +35,6 @@ import org.geysermc.connector.GeyserConnector;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

View File

@ -182,7 +182,7 @@ public class LoginEncryptionUtils {
}
public static void buildAndShowLoginDetailsWindow(GeyserSession session) {
String userLanguage = session.getClientData().getLanguageCode();
String userLanguage = session.getLocale();
session.sendForm(
CustomForm.builder()
.translator(LanguageUtils::getPlayerLocaleString, userLanguage)

View File

@ -74,4 +74,15 @@ public class MathUtils {
}
return (Byte) value;
}
/**
* Packs a chunk's X and Z coordinates into a single {@code long}.
*
* @param x the X coordinate
* @param z the Z coordinate
* @return the packed coordinates
*/
public static long chunkPositionToLong(int x, int z) {
return ((x & 0xFFFFFFFFL) << 32L) | (z & 0xFFFFFFFFL);
}
}

Some files were not shown because too many files have changed in this diff Show More