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

# Conflicts:
#	connector/pom.xml
#	connector/src/main/java/org/geysermc/connector/GeyserConnector.java
#	connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
#	connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java
#	connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java
#	connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java
This commit is contained in:
Tim203 2021-02-25 02:28:48 +01:00
commit c16c66b860
No known key found for this signature in database
GPG Key ID: 064EE9F5BF7C3EE8
66 changed files with 1231 additions and 478 deletions

View File

@ -1,57 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!--- DELETING THIS TEMPLATE WILL GET YOUR ISSUE CLOSED! --->
<!--- Please follow this format COMPLETELY and make sure the bug you are reporting has not been reported yet. Reports should contain as much information or context as possible to help us find the problem. Simply creating an issue on a vague topic will not help us at all, and if you are unsure if something should belong here, please contact us on [Discord](http://discord.geysermc.org).-->
<!--- Issues pertaining to connection problems, or anything of that covered on the [Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues) do not belong here and only clutter this issue tracker. -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots / Videos**
If applicable, add screenshots to help explain your problem.
**Server Version and Plugins**
If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information.
If you're running a multi-server instance, or using Geyser Standalone:
- Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all.
- Please list all plugins on all servers involved.
If this bug occurs on a server you do not control, please fill this in to the best of your knowledge.
**Geyser Dump**
If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you use the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue.
**Minecraft: Bedrock Edition Version**
The version of your Minecraft: Bedrock Edition client you tested with, along with your device type (e.g. Windows 10, Switch...).
**Additional Context**
Add any other context about the problem here.

64
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Bug report
about: Create a report to help us improve
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report for Geyser! Fill out the following form to your best ability to help us fix the problem.
Only use this if you're absolutely sure that you found a bug and can reproduce it. For anything else, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or the [Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues).
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce this behaviour
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behaviour
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Screenshots / Videos
description: If applicable, add screenshots to help explain your problem.
- type: textarea
attributes:
label: Server Version and Plugins
description: |
If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information.
If you're running a multi-server instance or using Geyser Standalone:
* Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all.
* Please list all plugins on all servers involved.
If this bug occurs on a server you do not control, please full this in to the best of your knowledge.
- type: input
attributes:
label: Geyser Dump
description: If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you're using the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue.
- type: input
attributes:
label: Geyser Version
description: What version of Geyser are you running?
placeholder: "For example: 1.2.0-SNAPSHOT (git-master-2d9baf1)"
validations:
required: true
- type: input
attributes:
label: "Minecraft: Bedrock Edition Version"
description: "What version of Minecraft: Bedrock Edition are you using? Leave empty if the bug happens before you can connect with Minecraft: Bedrock Edition."
placeholder: "For example: 1.16.201"
- type: textarea
attributes:
label: Additional Context
description: Add any other context about the problem here

View File

@ -1,5 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: GeyserMC Discord
url: http://discord.geysermc.org/
- name: Common Issues
url: https://github.com/GeyserMC/Geyser/wiki/Common-Issues
about: Check the common issues to see if you are not alone with that issue and see how you can fix them.
- name: Frequently Asked Questions
url: https://github.com/GeyserMC/Geyser/wiki/FAQ
about: Look at the FAQ page for answers for frequently asked questions.
- name: Get help on the GeyserMC Discord server
url: https://discord.gg/geysermc
about: If your issue seems like it could possibly be an easy fix due to configuration, please hop on our Discord.

View File

@ -1,14 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**What feature do you want?**
Add a description
**Alternatives?**
List any alternatives you might have tried

View File

@ -0,0 +1,21 @@
name: Feature request
about: Suggest an idea for this project
labels: "Feature Request"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request for Geyser! Fill out the following form to your best ability to help us understand your feature request and greately improve the change of it getting added.
For anything else than a feature request, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or [the Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues).
- type: textarea
attributes:
label: What feature do you want to see added?
description: A clear and concise description of your feature request.
validations:
required: true
- type: textarea
attributes:
label: Are there any alternatives?
description: List any alternatives you might have tried
validations:
required: true

View File

@ -22,7 +22,7 @@
</repository>
<repository>
<id>sponge-repo</id>
<url>https://repo.spongepowered.org/maven</url>
<url>https://repo.spongepowered.org/repository/maven-public/</url>
</repository>
<repository>
<id>bungeecord-repo</id>

View File

@ -30,9 +30,9 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.geysermc.adapters</groupId>
<groupId>org.geysermc.geyser.adapters</groupId>
<artifactId>spigot-all</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>

View File

@ -28,7 +28,6 @@ package org.geysermc.platform.spigot;
import com.github.steveice10.mc.protocol.MinecraftConstants;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.geysermc.adapters.spigot.SpigotAdapters;
import org.geysermc.common.PlatformType;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.bootstrap.GeyserBootstrap;
@ -40,6 +39,7 @@ import org.geysermc.connector.ping.GeyserLegacyPingPassthrough;
import org.geysermc.connector.ping.IGeyserPingPassthrough;
import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor;
import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager;
import org.geysermc.platform.spigot.command.SpigotCommandSender;
@ -69,6 +69,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
private GeyserConnector connector;
/**
* The Minecraft server version, formatted as <code>1.#.#</code>
*/
private String minecraftVersion;
@Override
public void onEnable() {
// This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed
@ -118,6 +123,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserConfig.loadFloodgate(this);
// Turn "(MC: 1.16.4)" into 1.16.4.
this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0];
this.connector = GeyserConnector.start(PlatformType.SPIGOT, this);
if (geyserConfig.isLegacyPingPassthrough()) {
@ -239,6 +247,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
return new GeyserSpigotDumpInfo();
}
@Override
public String getMinecraftServerVersion() {
return this.minecraftVersion;
}
public boolean isCompatible(String version, String whichVersion) {
int[] currentVersion = parseVersion(version);
int[] otherVersion = parseVersion(whichVersion);
@ -277,10 +290,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
* @return the server version before ViaVersion finishes initializing
*/
public ProtocolVersion getServerProtocolVersion() {
String bukkitVersion = Bukkit.getServer().getVersion();
// Turn "(MC: 1.16.4)" into 1.16.4.
String version = bukkitVersion.split("\\(MC: ")[1].split("\\)")[0];
return ProtocolVersion.getClosest(version);
return ProtocolVersion.getClosest(this.minecraftVersion);
}
/**

View File

@ -27,10 +27,10 @@ package org.geysermc.platform.spigot.world.manager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.geysermc.adapters.spigot.SpigotAdapters;
import org.geysermc.adapters.spigot.SpigotWorldAdapter;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter;
import us.myles.ViaVersion.api.Via;
import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.storage.BlockStorage;

View File

@ -75,6 +75,10 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager {
if (player == null) {
return BlockTranslator.JAVA_AIR_ID;
}
if (!player.getWorld().isChunkLoaded(x >> 4, z >> 4)) {
// Prevent nasty async errors if a player is loading in
return BlockTranslator.JAVA_AIR_ID;
}
// Get block entity storage
BlockStorage storage = Via.getManager().getConnection(player.getUniqueId()).get(BlockStorage.class);
Block block = player.getWorld().getBlockAt(x, y, z);

View File

@ -27,10 +27,10 @@ package org.geysermc.platform.spigot.world.manager;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.geysermc.adapters.spigot.SpigotAdapters;
import org.geysermc.adapters.spigot.SpigotWorldAdapter;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter;
public class GeyserSpigotNativeWorldManager extends GeyserSpigotWorldManager {
protected final SpigotWorldAdapter adapter;

View File

@ -101,7 +101,7 @@ public class GeyserSpongePlugin implements GeyserBootstrap {
}
}
if (geyserConfig.getBedrock().isCloneRemotePort()){
if (geyserConfig.getBedrock().isCloneRemotePort()) {
geyserConfig.getBedrock().setPort(geyserConfig.getRemote().getPort());
}
@ -163,4 +163,9 @@ public class GeyserSpongePlugin implements GeyserBootstrap {
public BootstrapDumpInfo getDumpInfo() {
return new GeyserSpongeDumpInfo();
}
@Override
public String getMinecraftServerVersion() {
return Sponge.getPlatform().getMinecraftVersion().getName();
}
}

View File

@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import lombok.Getter;
import net.minecrell.terminalconsole.TerminalConsoleAppender;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Logger;
@ -167,11 +168,6 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
this.onEnable();
}
public void onEnable(boolean useGui) {
this.useGui = useGui;
this.onEnable();
}
@Override
public void onEnable() {
Logger logger = (Logger) LogManager.getRootLogger();
@ -213,6 +209,9 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
// Allow libraries like Protocol to have their debug information passthrough
logger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO);
connector = GeyserConnector.start(PlatformType.STANDALONE, this);
geyserCommandManager = new GeyserCommandManager(connector);

View File

@ -55,7 +55,7 @@ public class LoopbackUtil {
if (!result.contains("minecraftuwp")) {
Files.write(Paths.get(System.getenv("temp") + "/loopback_minecraft.bat"), loopbackCommand.getBytes(), new OpenOption[0]);
process = Runtime.getRuntime().exec(startScript);
Runtime.getRuntime().exec(startScript);
geyserLogger.info(ChatColor.AQUA + LanguageUtils.getLocaleStringLog("geyser.bootstrap.loopback.added"));
}

View File

@ -10,16 +10,21 @@
</parent>
<artifactId>connector</artifactId>
<properties>
<netty.version>4.1.59.Final</netty.version>
</properties>
<dependencies>
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>common</artifactId>
<version>1.2.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.9.8</version>
<version>2.10.2</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -27,23 +32,16 @@
<artifactId>Java-Websocket</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.CloudburstMC.Protocol</groupId>
<artifactId>bedrock-v422</artifactId>
<version>d41b84e86c</version>
<version>294e7e5</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>net.sf.trove4j</groupId>
<artifactId>trove</artifactId>
</exclusion>
<!-- Stay on the older version of Network while it's rewritten -->
<exclusion>
<groupId>com.nukkitx.network</groupId>
<artifactId>raknet</artifactId>
@ -51,10 +49,16 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.nukkitx.network</groupId>
<groupId>com.github.CloudburstMC.Network</groupId>
<artifactId>raknet</artifactId>
<version>1.6.20</version>
<version>a94d2dd</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.nukkitx.fastutil</groupId>
@ -157,15 +161,51 @@
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-resolver-dns</artifactId>
<version>4.1.43.Final</version>
<version>${netty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-resolver-dns-native-macos</artifactId>
<version>${netty.version}</version>
<scope>compile</scope>
<classifier>osx-x86_64</classifier>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-haproxy</artifactId>
<version>4.1.56.Final</version>
<version>${netty.version}</version>
<scope>compile</scope>
</dependency>
<!-- Network dependencies we are updating ourselves -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>${netty.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>${netty.version}</version>
<scope>compile</scope>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>${netty.version}</version>
<scope>compile</scope>
<classifier>linux-aarch_64</classifier>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-kqueue</artifactId>
<version>${netty.version}</version>
<scope>compile</scope>
<classifier>osx-x86_64</classifier>
</dependency>
<!-- End -->
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
@ -179,25 +219,25 @@
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>4.3.0</version>
<version>4.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>4.3.0</version>
<version>4.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>4.3.0</version>
<version>4.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-gson-legacy-impl</artifactId>
<version>4.3.0</version>
<version>4.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -29,6 +29,7 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nukkitx.network.raknet.RakNetConstants;
import com.nukkitx.network.util.EventLoops;
import com.nukkitx.protocol.bedrock.BedrockServer;
import lombok.Getter;
import lombok.Setter;
@ -57,10 +58,7 @@ import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
import org.geysermc.connector.skin.FloodgateSkinUploader;
import org.geysermc.connector.utils.DimensionUtils;
import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.utils.LocaleUtils;
import org.geysermc.connector.utils.ResourcePack;
import org.geysermc.connector.utils.*;
import org.geysermc.floodgate.crypto.AesCipher;
import org.geysermc.floodgate.crypto.AesKeyProducer;
import org.geysermc.floodgate.crypto.Base64Topping;
@ -86,7 +84,8 @@ public class GeyserConnector {
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
public static final String NAME = "Geyser";
public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs
@ -211,6 +210,7 @@ public class GeyserConnector {
}
}
CooldownUtils.setShowCooldown(config.isShowCooldown());
DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls();
@ -218,7 +218,13 @@ public class GeyserConnector {
RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu();
logger.debug("Setting MTU to " + config.getMtu());
bedrockServer = new BedrockServer(new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort()));
boolean enableProxyProtocol = config.getBedrock().isEnableProxyProtocol();
bedrockServer = new BedrockServer(
new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort()),
1,
EventLoops.commonGroup(),
enableProxyProtocol
);
bedrockServer.setHandler(new ConnectorServerEventHandler(this));
bedrockServer.bind().whenComplete((avoid, throwable) -> {
if (throwable == null) {
@ -265,6 +271,20 @@ public class GeyserConnector {
}
return valueMap;
}));
String minecraftVersion = bootstrap.getMinecraftServerVersion();
if (minecraftVersion != null) {
Map<String, Map<String, Integer>> versionMap = new HashMap<>();
Map<String, Integer> platformMap = new HashMap<>();
platformMap.put(platformType.getPlatformName(), 1);
versionMap.put(minecraftVersion, platformMap);
metrics.addCustomChart(new Metrics.DrilldownPie("minecraftServerVersion", () -> {
// By the end, we should return, for example:
// 1.16.5 => (Spigot, 1)
return versionMap;
}));
}
}
boolean isGui = false;

View File

@ -33,6 +33,7 @@ import org.geysermc.connector.command.CommandManager;
import org.geysermc.connector.network.translators.world.GeyserWorldManager;
import org.geysermc.connector.network.translators.world.WorldManager;
import javax.annotation.Nullable;
import java.nio.file.Path;
public interface GeyserBootstrap {
@ -99,4 +100,18 @@ public interface GeyserBootstrap {
* @return The info about the bootstrap
*/
BootstrapDumpInfo getDumpInfo();
/**
* Returns the Minecraft version currently being used on the server. This should be only be implemented on platforms
* that have direct server access - platforms such as proxies always have to be on their latest version to support
* the newest Minecraft version, but older servers can use ViaVersion to enable newer versions to join.
* <br>
* If used, this should not be null before {@link org.geysermc.connector.GeyserConnector} initialization.
*
* @return the Minecraft version being used on the server, or <code>null</code> if not applicable
*/
@Nullable
default String getMinecraftServerVersion() {
return null;
}
}

View File

@ -52,7 +52,7 @@ public class IGeyserMain {
* @return The formatted message
*/
private String createMessage() {
String message = "";
StringBuilder message = new StringBuilder();
InputStream helpStream = IGeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/" + Locale.getDefault().toString() + ".txt");
@ -68,10 +68,10 @@ public class IGeyserMain {
line = line.replace("${plugin_type}", this.getPluginType());
line = line.replace("${plugin_folder}", this.getPluginFolder());
message += line + "\n";
message.append(line).append("\n");
}
return message;
return message.toString();
}
/**

View File

@ -27,9 +27,11 @@ package org.geysermc.connector.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.geysermc.connector.GeyserLogger;
import org.geysermc.connector.network.CIDRMatcher;
import org.geysermc.connector.utils.LanguageUtils;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public interface GeyserConfiguration {
@ -59,6 +61,8 @@ public interface GeyserConfiguration {
int getPingPassthroughInterval();
boolean isForwardPlayerPing();
int getMaxPlayers();
boolean isDebugMode();
@ -104,6 +108,15 @@ public interface GeyserConfiguration {
String getMotd2();
String getServerName();
boolean isEnableProxyProtocol();
List<String> getProxyProtocolWhitelistedIPs();
/**
* @return Unmodifiable list of {@link CIDRMatcher}s from {@link #getProxyProtocolWhitelistedIPs()}
*/
List<CIDRMatcher> getWhitelistedIPsMatchers();
}
interface IRemoteConfiguration {

View File

@ -25,16 +25,21 @@
package org.geysermc.connector.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.common.serializer.AsteriskSerializer;
import org.geysermc.connector.network.CIDRMatcher;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
@ -74,6 +79,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("ping-passthrough-interval")
private int pingPassthroughInterval = 3;
@JsonProperty("forward-player-ping")
private boolean forwardPlayerPing = false;
@JsonProperty("max-players")
private int maxPlayers = 100;
@ -119,6 +127,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
private MetricsInfo metrics = new MetricsInfo();
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BedrockConfiguration implements IBedrockConfiguration {
@AsteriskSerializer.Asterisk(sensitive = true)
private String address = "0.0.0.0";
@ -134,9 +143,33 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("server-name")
private String serverName = GeyserConnector.NAME;
@JsonProperty("enable-proxy-protocol")
private boolean enableProxyProtocol = false;
@JsonProperty("proxy-protocol-whitelisted-ips")
private List<String> proxyProtocolWhitelistedIPs = Collections.emptyList();
@JsonIgnore
private List<CIDRMatcher> whitelistedIPsMatchers = null;
@Override
public List<CIDRMatcher> getWhitelistedIPsMatchers() {
// Effective Java, Third Edition; Item 83: Use lazy initialization judiciously
List<CIDRMatcher> matchers = this.whitelistedIPsMatchers;
if (matchers == null) {
synchronized (this) {
this.whitelistedIPsMatchers = matchers = proxyProtocolWhitelistedIPs.stream()
.map(CIDRMatcher::new)
.collect(Collectors.toList());
}
}
return Collections.unmodifiableList(matchers);
}
}
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class RemoteConfiguration implements IRemoteConfiguration {
@Setter
@AsteriskSerializer.Asterisk(sensitive = true)
@ -170,6 +203,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
}
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class MetricsInfo implements IMetricsInfo {
private boolean enabled = true;

View File

@ -35,6 +35,8 @@ public class AbstractArrowEntity extends Entity {
public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
setMotion(motion);
}
@Override
@ -47,4 +49,20 @@ public class AbstractArrowEntity extends Entity {
super.updateBedrockMetadata(entityMetadata, session);
}
@Override
public void setRotation(Vector3f rotation) {
// Ignore the rotation sent by the Java server since the
// Java client calculates the rotation from the motion
}
@Override
public void setMotion(Vector3f motion) {
super.setMotion(motion);
double horizontalSpeed = Math.sqrt(motion.getX() * motion.getX() + motion.getZ() * motion.getZ());
float yaw = (float) Math.toDegrees(Math.atan2(motion.getX(), motion.getZ()));
float pitch = (float) Math.toDegrees(Math.atan2(motion.getY(), horizontalSpeed));
rotation = Vector3f.from(yaw, pitch, yaw);
}
}

View File

@ -43,7 +43,7 @@ public class BoatEntity extends Entity {
*/
private static final String BUOYANCY_DATA = "{\"apply_gravity\":true,\"base_buoyancy\":1.0,\"big_wave_probability\":0.02999999932944775," +
"\"big_wave_speed\":10.0,\"drag_down_on_buoyancy_removed\":0.0,\"liquid_blocks\":[\"minecraft:water\"," +
"\"minecraft:flowing_water\"],\"simulate_waves\":false}}";
"\"minecraft:flowing_water\"],\"simulate_waves\":false}";
private boolean isPaddlingLeft;
private float paddleTimeLeft;

View File

@ -26,43 +26,167 @@
package org.geysermc.connector.entity;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.object.ProjectileData;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import org.geysermc.connector.GeyserConnector;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.collision.BoundingBox;
import org.geysermc.connector.network.translators.collision.CollisionManager;
import org.geysermc.connector.network.translators.collision.CollisionTranslator;
import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
public class FishingHookEntity extends Entity {
public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, ProjectileData data) {
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class FishingHookEntity extends ThrowableEntity {
private boolean hooked = false;
private final BoundingBox boundingBox;
private boolean inWater = false;
public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, PlayerEntity owner) {
super(entityId, geyserId, entityType, position, motion, rotation);
for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) {
Entity entity = session.getEntityCache().getEntityByJavaId(data.getOwnerId());
if (entity == null && session.getPlayerEntity().getEntityId() == data.getOwnerId()) {
entity = session.getPlayerEntity();
}
this.boundingBox = new BoundingBox(0.125, 0.125, 0.125, 0.25, 0.25, 0.25);
if (entity != null) {
this.metadata.put(EntityData.OWNER_EID, entity.getGeyserId());
return;
}
}
// In Java, the splash sound depends on the entity's velocity, but in Bedrock the volume doesn't change.
// This splash can be confused with the sound from catching a fish. This silences the splash from Bedrock,
// so that it can be handled by moveAbsoluteImmediate.
this.metadata.putFloat(EntityData.BOUNDING_BOX_HEIGHT, 128);
this.metadata.put(EntityData.OWNER_EID, owner.getGeyserId());
}
@Override
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
if (entityMetadata.getId() == 7) {
Entity entity = session.getEntityCache().getEntityByJavaId((Integer) entityMetadata.getValue() - 1);
if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue() - 1) {
if (entityMetadata.getId() == 7) { // Hooked entity
int hookedEntityId = (int) entityMetadata.getValue() - 1;
Entity entity = session.getEntityCache().getEntityByJavaId(hookedEntityId);
if (entity == null && session.getPlayerEntity().getEntityId() == hookedEntityId) {
entity = session.getPlayerEntity();
}
if (entity != null) {
metadata.put(EntityData.TARGET_EID, entity.getGeyserId());
hooked = true;
} else {
hooked = false;
}
}
super.updateBedrockMetadata(entityMetadata, session);
}
@Override
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
boundingBox.setMiddleX(position.getX());
boundingBox.setMiddleY(position.getY() + boundingBox.getSizeY() / 2);
boundingBox.setMiddleZ(position.getZ());
CollisionManager collisionManager = session.getCollisionManager();
List<Vector3i> collidableBlocks = collisionManager.getCollidableBlocks(boundingBox);
boolean touchingWater = false;
boolean collided = false;
for (Vector3i blockPos : collidableBlocks) {
if (0 <= blockPos.getY() && blockPos.getY() <= 255) {
int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ());
if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) {
// TODO Push bounding box out of collision to improve movement
collided = true;
}
int waterLevel = BlockStateValues.getWaterLevel(blockID);
if (BlockTranslator.isWaterlogged(blockID)) {
waterLevel = 0;
}
if (waterLevel >= 0) {
double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0;
// Falling water is a full block
if (waterLevel >= 8) {
waterMaxY = blockPos.getY() + 1;
}
if (position.getY() <= waterMaxY) {
touchingWater = true;
}
}
}
}
if (!inWater && touchingWater) {
sendSplashSound(session);
}
inWater = touchingWater;
if (!collided) {
super.moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported);
} else {
super.moveAbsoluteImmediate(session, this.position, rotation, true, true);
}
}
private void sendSplashSound(GeyserSession session) {
if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) {
float volume = (float) (0.2f * Math.sqrt(0.2 * (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) + motion.getY() * motion.getY()));
if (volume > 1) {
volume = 1;
}
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
playSoundPacket.setSound("random.splash");
playSoundPacket.setPosition(position);
playSoundPacket.setVolume(volume);
playSoundPacket.setPitch(1f + ThreadLocalRandom.current().nextFloat() * 0.3f);
session.sendUpstreamPacket(playSoundPacket);
}
}
@Override
public void tick(GeyserSession session) {
if (hooked || !isInAir(session) && !isInWater(session) || isOnGround()) {
motion = Vector3f.ZERO;
return;
}
float gravity = getGravity(session);
motion = motion.down(gravity);
moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
float drag = getDrag(session);
motion = motion.mul(drag);
}
@Override
protected float getGravity(GeyserSession session) {
if (!isInWater(session) && !onGround) {
return 0.03f;
}
return 0;
}
/**
* @param session the session of the Bedrock client.
* @return true if this entity is currently in air.
*/
protected boolean isInAir(GeyserSession session) {
if (session.getConnector().getConfig().isCacheChunks()) {
if (0 <= position.getFloorY() && position.getFloorY() <= 255) {
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
return block == BlockTranslator.JAVA_AIR_ID;
}
}
return false;
}
@Override
protected float getDrag(GeyserSession session) {
return 0.92f;
}
}

View File

@ -32,19 +32,44 @@ import org.geysermc.connector.network.session.GeyserSession;
public class ItemedFireballEntity extends ThrowableEntity {
private final Vector3f acceleration;
/**
* The number of ticks to advance movement before sending to Bedrock
*/
protected int futureTicks = 3;
public ItemedFireballEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, Vector3f.ZERO, rotation);
acceleration = motion;
float magnitude = motion.length();
if (magnitude != 0) {
acceleration = motion.div(magnitude).mul(0.1f);
} else {
acceleration = Vector3f.ZERO;
}
}
private Vector3f tickMovement(GeyserSession session, Vector3f position) {
position = position.add(motion);
float drag = getDrag(session);
motion = motion.add(acceleration).mul(drag);
return position;
}
@Override
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
// Advance the position by a few ticks before sending it to Bedrock
Vector3f lastMotion = motion;
Vector3f newPosition = position;
for (int i = 0; i < futureTicks; i++) {
newPosition = tickMovement(session, newPosition);
}
super.moveAbsoluteImmediate(session, newPosition, rotation, isOnGround, teleported);
this.position = position;
this.motion = lastMotion;
}
@Override
public void tick(GeyserSession session) {
position = position.add(motion);
// TODO: While this reduces latency in position updating (needed for better fireball reflecting),
// TODO: movement is incredibly stiff.
// TODO: Only use this laggy movement for fireballs that be reflected
moveAbsoluteImmediate(session, position, rotation, false, true);
float drag = getDrag(session);
motion = motion.add(acceleration).mul(drag);
moveAbsoluteImmediate(session, tickMovement(session, position), rotation, false, false);
}
}

View File

@ -29,20 +29,21 @@ 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 com.nukkitx.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
/**
* Used as a class for any object-like entity that moves as a projectile
*/
public class ThrowableEntity extends Entity implements Tickable {
private Vector3f lastPosition;
protected Vector3f lastJavaPosition;
public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
this.lastPosition = position;
this.lastJavaPosition = position;
}
/**
@ -52,22 +53,65 @@ public class ThrowableEntity extends Entity implements Tickable {
*/
@Override
public void tick(GeyserSession session) {
super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround);
moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
float drag = getDrag(session);
float gravity = getGravity();
float gravity = getGravity(session);
motion = motion.mul(drag).down(gravity);
}
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
moveEntityDeltaPacket.setRuntimeEntityId(geyserId);
if (isOnGround) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND);
}
setOnGround(isOnGround);
if (teleported) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING);
}
if (this.position.getX() != position.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
moveEntityDeltaPacket.setX(position.getX());
}
if (this.position.getY() != position.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
moveEntityDeltaPacket.setY(position.getY());
}
if (this.position.getZ() != position.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(position.getZ());
}
setPosition(position);
if (this.rotation.getX() != rotation.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW);
moveEntityDeltaPacket.setYaw(rotation.getX());
}
if (this.rotation.getY() != rotation.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
moveEntityDeltaPacket.setPitch(rotation.getY());
}
if (this.rotation.getZ() != rotation.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW);
moveEntityDeltaPacket.setHeadYaw(rotation.getZ());
}
setRotation(rotation);
if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
session.sendUpstreamPacket(moveEntityDeltaPacket);
}
}
/**
* Get the gravity of this entity type. Used for applying gravity while the entity is in motion.
*
* @param session the session of the Bedrock client.
* @return the amount of gravity to apply to this entity while in motion.
*/
protected float getGravity() {
protected float getGravity(GeyserSession session) {
if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) {
switch (entityType) {
case THROWN_POTION:
@ -76,11 +120,14 @@ public class ThrowableEntity extends Entity implements Tickable {
case THROWN_EXP_BOTTLE:
return 0.07f;
case FIREBALL:
case SHULKER_BULLET:
return 0;
case SNOWBALL:
case THROWN_EGG:
case THROWN_ENDERPEARL:
return 0.03f;
case LLAMA_SPIT:
return 0.06f;
}
}
return 0;
@ -101,11 +148,14 @@ public class ThrowableEntity extends Entity implements Tickable {
case SNOWBALL:
case THROWN_EGG:
case THROWN_ENDERPEARL:
case LLAMA_SPIT:
return 0.99f;
case FIREBALL:
case SMALL_FIREBALL:
case DRAGON_FIREBALL:
return 0.95f;
case SHULKER_BULLET:
return 1;
}
}
return 1;
@ -117,8 +167,10 @@ public class ThrowableEntity extends Entity implements Tickable {
*/
protected boolean isInWater(GeyserSession session) {
if (session.getConnector().getConfig().isCacheChunks()) {
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
return block == BlockTranslator.BEDROCK_WATER_ID;
if (0 <= position.getFloorY() && position.getFloorY() <= 255) {
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
return BlockStateValues.getWaterLevel(block) != -1;
}
}
return false;
}
@ -136,14 +188,13 @@ public class ThrowableEntity extends Entity implements Tickable {
@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;
moveAbsoluteImmediate(session, lastJavaPosition.add(relX, relY, relZ), rotation, isOnGround, false);
lastJavaPosition = position;
}
@Override
public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
lastPosition = position;
moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported);
lastJavaPosition = position;
}
}

View File

@ -35,6 +35,8 @@ public class WitherSkullEntity extends ItemedFireballEntity {
public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
this.futureTicks = 1;
}
@Override

View File

@ -44,7 +44,7 @@ public class RabbitEntity extends AnimalEntity {
if (entityMetadata.getId() == 15) {
metadata.put(EntityData.SCALE, .55f);
boolean isBaby = (boolean) entityMetadata.getValue();
if(isBaby) {
if (isBaby) {
metadata.put(EntityData.SCALE, .35f);
metadata.getFlags().setFlag(EntityFlag.BABY, true);
}

View File

@ -125,7 +125,7 @@ public class VillagerEntity extends AbstractMerchantEntity {
Pattern r = Pattern.compile("facing=([a-z]+)");
Matcher m = r.matcher(bedRotationZ);
if (m.find()) {
switch (m.group(0)){
switch (m.group(0)) {
case "facing=south":
//bed is facing south
z = 180;

View File

@ -46,7 +46,7 @@ public class WitherEntity extends MonsterEntity {
if (entityMetadata.getId() >= 15 && entityMetadata.getId() <= 17) {
Entity entity = session.getEntityCache().getEntityByJavaId((int) entityMetadata.getValue());
if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue()) {
if (entity == null && session.getPlayerEntity().getEntityId() == (int) entityMetadata.getValue()) {
entity = session.getPlayerEntity();
}
@ -62,7 +62,7 @@ public class WitherEntity extends MonsterEntity {
} else if (entityMetadata.getId() == 17) {
metadata.put(EntityData.WITHER_TARGET_3, targetID);
} else if (entityMetadata.getId() == 18) {
metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, (int) entityMetadata.getValue());
metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, entityMetadata.getValue());
// Show the shield for the first few seconds of spawning (like Java)
if ((int) entityMetadata.getValue() >= 165) {

View File

@ -30,8 +30,11 @@ import org.geysermc.connector.entity.*;
import org.geysermc.connector.entity.living.*;
import org.geysermc.connector.entity.living.animal.*;
import org.geysermc.connector.entity.living.animal.horse.*;
import org.geysermc.connector.entity.living.animal.tameable.*;
import org.geysermc.connector.entity.living.merchant.*;
import org.geysermc.connector.entity.living.animal.tameable.CatEntity;
import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity;
import org.geysermc.connector.entity.living.animal.tameable.WolfEntity;
import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity;
import org.geysermc.connector.entity.living.merchant.VillagerEntity;
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;
@ -39,6 +42,9 @@ import org.geysermc.connector.entity.living.monster.raid.RaidParticipantEntity;
import org.geysermc.connector.entity.living.monster.raid.SpellcasterIllagerEntity;
import org.geysermc.connector.entity.player.PlayerEntity;
import java.util.ArrayList;
import java.util.List;
@Getter
public enum EntityType {
@ -112,7 +118,7 @@ public enum EntityType {
TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"),
TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f),
CAT(CatEntity.class, 75, 0.35f, 0.3f),
SHULKER_BULLET(Entity.class, 76, 0.3125f),
SHULKER_BULLET(ThrowableEntity.class, 76, 0.3125f),
FISHING_BOBBER(FishingHookEntity.class, 77, 0f, 0f, 0f, 0f, "minecraft:fishing_hook"),
CHALKBOARD(Entity.class, 78, 0f),
DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f),
@ -139,7 +145,7 @@ public enum EntityType {
MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"),
MINECART_COMMAND_BLOCK(CommandBlockMinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"),
LINGERING_POTION(ThrowableEntity.class, 101, 0f),
LLAMA_SPIT(Entity.class, 102, 0.25f),
LLAMA_SPIT(ThrowableEntity.class, 102, 0.25f),
EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"),
EVOKER(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"),
VEX(VexEntity.class, 105, 0.8f, 0.4f),
@ -174,17 +180,33 @@ public enum EntityType {
*/
ENDER_DRAGON_PART(EnderDragonPartEntity.class, 32, 0, 0, 0, 0, "minecraft:armor_stand");
/**
* A list of all Java identifiers for use with command suggestions
*/
public static final String[] ALL_JAVA_IDENTIFIERS;
private static final EntityType[] VALUES = values();
private Class<? extends Entity> entityClass;
static {
List<String> allJavaIdentifiers = new ArrayList<>();
for (EntityType type : values()) {
if (type == AGENT || type == BALLOON || type == CHALKBOARD || type == NPC || type == TRIPOD_CAMERA || type == ENDER_DRAGON_PART) {
continue;
}
allJavaIdentifiers.add("minecraft:" + type.name().toLowerCase());
}
ALL_JAVA_IDENTIFIERS = allJavaIdentifiers.toArray(new String[0]);
}
private final Class<? extends Entity> entityClass;
private final int type;
private final float height;
private final float width;
private final float length;
private final float offset;
private String identifier;
private final String identifier;
EntityType(Class<? extends Entity> entityClass, int type, float height) {
//noinspection SuspiciousNameCombination
this(entityClass, type, height, height);
}
@ -198,8 +220,6 @@ public enum EntityType {
EntityType(Class<? extends Entity> entityClass, int type, float height, float width, float length, float offset) {
this(entityClass, type, height, width, length, offset, null);
this.identifier = "minecraft:" + name().toLowerCase();
}
EntityType(Class<? extends Entity> entityClass, int type, float height, float width, float length, float offset, String identifier) {
@ -209,7 +229,7 @@ public enum EntityType {
this.width = width;
this.length = length;
this.offset = offset + 0.00001f;
this.identifier = identifier;
this.identifier = identifier == null ? "minecraft:" + name().toLowerCase() : identifier;
}
public static EntityType getFromIdentifier(String identifier) {

View File

@ -36,6 +36,7 @@ import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -75,7 +76,7 @@ public class Metrics {
private final static ObjectMapper mapper = new ObjectMapper();
private GeyserConnector connector;
private final GeyserConnector connector;
/**
* Class constructor.
@ -156,6 +157,7 @@ public class Metrics {
String osName = System.getProperty("os.name");
String osArch = System.getProperty("os.arch");
String osVersion = System.getProperty("os.version");
String javaVersion = System.getProperty("java.version");
int coreCount = Runtime.getRuntime().availableProcessors();
ObjectNode data = mapper.createObjectNode();
@ -163,6 +165,7 @@ public class Metrics {
data.put("serverUUID", serverUUID);
data.put("playerAmount", playerAmount);
data.put("javaVersion", javaVersion);
data.put("osName", osName);
data.put("osArch", osArch);
data.put("osVersion", osVersion);
@ -241,7 +244,7 @@ public class Metrics {
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
gzip.write(str.getBytes("UTF-8"));
gzip.write(str.getBytes(StandardCharsets.UTF_8));
gzip.close();
return outputStream.toByteArray();
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network;
import java.net.InetAddress;
import java.net.UnknownHostException;
/*
* Taken & modified from TCPShield, licensed under MIT. See https://github.com/TCPShield/RealIP/blob/master/LICENSE
*
* https://github.com/TCPShield/RealIP/blob/32d422a9523cb6e25b571072851f3306bb8bbc4f/src/main/java/net/tcpshield/tcpshield/validation/cidr/CIDRMatcher.java
*/
public class CIDRMatcher {
private final int maskBits;
private final int maskBytes;
private final boolean simpleCIDR;
private final InetAddress cidrAddress;
public CIDRMatcher(String ipAddress) {
String[] split = ipAddress.split("/", 2);
String parsedIPAddress;
if (split.length == 2) {
parsedIPAddress = split[0];
this.maskBits = Integer.parseInt(split[1]);
this.simpleCIDR = maskBits == 32;
} else {
parsedIPAddress = ipAddress;
this.maskBits = -1;
this.simpleCIDR = true;
}
this.maskBytes = simpleCIDR ? -1 : maskBits / 8;
try {
cidrAddress = InetAddress.getByName(parsedIPAddress);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
public boolean matches(InetAddress inetAddress) {
// check if IP is IPv4 or IPv6
if (cidrAddress.getClass() != inetAddress.getClass()) {
return false;
}
// check for equality if it's a simple CIDR
if (simpleCIDR) {
return inetAddress.equals(cidrAddress);
}
byte[] inetAddressBytes = inetAddress.getAddress();
byte[] requiredAddressBytes = cidrAddress.getAddress();
byte finalByte = (byte) (0xFF00 >> (maskBits & 0x07));
for (int i = 0; i < maskBytes; i++) {
if (inetAddressBytes[i] != requiredAddressBytes[i]) {
return false;
}
}
if (finalByte != 0) {
return (inetAddressBytes[maskBytes] & finalByte) == (requiredAddressBytes[maskBytes] & finalByte);
}
return true;
}
}

View File

@ -40,8 +40,18 @@ import org.geysermc.connector.utils.LanguageUtils;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class ConnectorServerEventHandler implements BedrockServerEventHandler {
/*
The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client
*/
private static final int MINECRAFT_VERSION_BYTES_LENGTH = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length;
private static final int BRAND_BYTES_LENGTH = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8).length;
/**
* The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length.
*/
private static final int MAGIC_RAKNET_LENGTH = 338;
private final GeyserConnector connector;
@ -51,6 +61,21 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
@Override
public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) {
List<String> allowedProxyIPs = connector.getConfig().getBedrock().getProxyProtocolWhitelistedIPs();
if (connector.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) {
boolean isWhitelistedIP = false;
for (CIDRMatcher matcher : connector.getConfig().getBedrock().getWhitelistedIPsMatchers()) {
if (matcher.matches(inetSocketAddress.getAddress())) {
isWhitelistedIP = true;
break;
}
}
if (!isWhitelistedIP) {
return false;
}
}
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.attempt_connect", inetSocketAddress));
return true;
}
@ -69,16 +94,16 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
BedrockPong pong = new BedrockPong();
pong.setEdition("MCPE");
pong.setGameType("Default");
pong.setGameType("Survival"); // Can only be Survival or Creative as of 1.16.210.59
pong.setNintendoLimited(false);
pong.setProtocolVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion());
pong.setVersion(null); // Server tries to connect either way and it looks better
pong.setVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); // Required to not be empty as of 1.16.210.59. Can only contain . and numbers.
pong.setIpv4Port(config.getBedrock().getPort());
if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
String mainMotd = motd[0]; // First line of the motd.
String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank.
String subMotd = (motd.length != 1) ? motd[1] : GeyserConnector.NAME; // Second line of the motd if present, otherwise default.
pong.setMotd(mainMotd.trim());
pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit.
@ -95,15 +120,28 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
pong.setMaximumPlayerCount(config.getMaxPlayers());
}
// Fallbacks to prevent errors and allow Bedrock to see the server
if (pong.getMotd() == null || pong.getMotd().trim().isEmpty()) {
pong.setMotd(GeyserConnector.NAME);
}
if (pong.getSubMotd() == null || pong.getSubMotd().trim().isEmpty()) {
// Sub-MOTD cannot be empty as of 1.16.210.59
pong.setSubMotd(GeyserConnector.NAME);
}
// The ping will not appear if the MOTD + sub-MOTD is of a certain length.
// We don't know why, though
byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8);
if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) {
// Remove the sub-MOTD first since that only appears locally
pong.setSubMotd("");
if (motdArray.length > 338) {
int subMotdLength = pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length;
if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) {
// Shorten the sub-MOTD first since that only appears locally
if (subMotdLength > BRAND_BYTES_LENGTH) {
pong.setSubMotd(GeyserConnector.NAME);
subMotdLength = BRAND_BYTES_LENGTH;
}
if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) {
// If the top MOTD is still too long, we chop it down
byte[] newMotdArray = new byte[339];
byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength];
System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length);
pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8));
}

View File

@ -64,7 +64,7 @@ public class QueryPacketHandler {
* @param buffer The Query data
*/
public QueryPacketHandler(GeyserConnector connector, InetSocketAddress sender, ByteBuf buffer) {
if(!isQueryPacket(buffer))
if (!isQueryPacket(buffer))
return;
this.connector = connector;
@ -225,7 +225,7 @@ public class QueryPacketHandler {
query.write(new byte[] { 0x00, 0x00 });
// Fill player names
if(pingInfo != null) {
if (pingInfo != null) {
for (String username : pingInfo.getPlayerList()) {
query.write(username.getBytes());
query.write((byte) 0x00);

View File

@ -93,6 +93,8 @@ import org.geysermc.cumulus.util.FormBuilder;
import org.geysermc.floodgate.crypto.FloodgateCipher;
import org.geysermc.floodgate.util.BedrockData;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -168,6 +170,9 @@ public class GeyserSession implements CommandSender {
@Setter
private boolean sprinting;
/**
* Not updated if cache chunks is enabled.
*/
@Setter
private boolean jumping;
@ -370,7 +375,8 @@ public class GeyserSession implements CommandSender {
tmpPlayers.forEach(player -> this.emotes.addAll(player.getEmotes()));
bedrockServerSession.addDisconnectHandler(disconnectReason -> {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", bedrockServerSession.getAddress().getAddress(), disconnectReason));
InetAddress address = bedrockServerSession.getRealAddress().getAddress();
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", address, disconnectReason));
disconnect(disconnectReason.name());
connector.removePlayer(this);
@ -544,8 +550,10 @@ public class GeyserSession implements CommandSender {
downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
}
// Let Geyser handle sending the keep alive
downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false);
if (connector.getConfig().isForwardPlayerPing()) {
// 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) {
@ -565,7 +573,7 @@ public class GeyserSession implements CommandSender {
clientData.getLanguageCode(),
clientData.getUiProfile().ordinal(),
clientData.getCurrentInputMode().ordinal(),
upstream.getSession().getAddress().getAddress().getHostAddress(),
upstream.getAddress().getAddress().getHostAddress(),
skinUploader.getId(),
skinUploader.getVerifyCode()
).toString());
@ -826,7 +834,14 @@ public class GeyserSession implements CommandSender {
startGamePacket.setMultiplayerCorrelationId("");
startGamePacket.setItemEntries(ItemRegistry.ITEMS);
startGamePacket.setVanillaVersion("*");
startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT);
startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); // can be removed once 1.16.200 support is dropped
SyncedPlayerMovementSettings settings = new SyncedPlayerMovementSettings();
settings.setMovementMode(AuthoritativeMovementMode.CLIENT);
settings.setRewindHistorySize(0);
settings.setServerAuthoritativeBlockBreaking(false);
startGamePacket.setPlayerMovementSettings(settings);
upstream.sendPacket(startGamePacket);
}

View File

@ -61,6 +61,6 @@ public class UpstreamSession {
}
public InetSocketAddress getAddress() {
return session.getAddress();
return session.getRealAddress();
}
}

View File

@ -112,6 +112,8 @@ public final class BedrockClientData {
private String skinColor;
@JsonProperty(value = "ThirdPartyNameOnly")
private boolean thirdPartyNameOnly;
@JsonProperty(value = "PlayFabId")
private String playFabId;
public void setJsonData(JsonNode data) {
if (this.jsonData == null && data != null) {

View File

@ -38,7 +38,7 @@ import java.io.InputStream;
*/
public class EntityIdentifierRegistry {
public static NbtMap ENTITY_IDENTIFIERS;
public static final NbtMap ENTITY_IDENTIFIERS;
private EntityIdentifierRegistry() {
}

View File

@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUpdate
import com.github.steveice10.packetlib.packet.Packet;
import com.nukkitx.protocol.bedrock.BedrockPacket;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.geysermc.common.PlatformType;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.FileUtils;
@ -89,12 +90,15 @@ public class PacketTranslatorRegistry<T> {
public <P extends T> boolean translate(Class<? extends P> clazz, P packet, GeyserSession session) {
if (!session.getUpstream().isClosed() && !session.isClosed()) {
try {
if (translators.containsKey(clazz)) {
((PacketTranslator<P>) translators.get(clazz)).translate(packet, session);
PacketTranslator<P> translator = (PacketTranslator<P>) translators.get(clazz);
if (translator != null) {
translator.translate(packet, session);
return true;
} else {
if (!IGNORED_PACKETS.contains(clazz))
if ((GeyserConnector.getInstance().getPlatformType() != PlatformType.STANDALONE || !(packet instanceof BedrockPacket)) && !IGNORED_PACKETS.contains(clazz)) {
// Other debug logs already take care of Bedrock packets for us if on standalone
GeyserConnector.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet));
}
}
} catch (Throwable ex) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.packet.failed", packet.getClass().getSimpleName()), ex);

View File

@ -40,7 +40,7 @@ import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* Used to send the keep alive packet back to the server
* Used to send the forwarded keep alive packet back to the server
*/
@Translator(packet = NetworkStackLatencyPacket.class)
public class BedrockNetworkStackLatencyTranslator extends PacketTranslator<NetworkStackLatencyPacket> {
@ -60,8 +60,10 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator<Netwo
// negative timestamps are used as hack to fix the url image loading bug
if (packet.getTimestamp() > 0) {
ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId);
session.sendDownstreamPacket(keepAlivePacket);
if (session.getConnector().getConfig().isForwardPlayerPing()) {
ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId);
session.sendDownstreamPacket(keepAlivePacket);
}
return;
}

View File

@ -37,6 +37,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.PlayerActionType;
import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
@ -64,7 +65,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
return;
// Send book update before any player action
if (packet.getAction() != PlayerActionPacket.Action.RESPAWN) {
if (packet.getAction() != PlayerActionType.RESPAWN) {
session.getBookEditCache().checkForSend();
}
@ -205,10 +206,11 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
session.getEntityCache().updateBossBars();
break;
case JUMP:
session.setJumping(true);
session.getConnector().getGeneralThreadPool().schedule(() -> {
session.setJumping(false);
}, 1, TimeUnit.SECONDS);
if (!session.getConnector().getConfig().isCacheChunks()) {
// Save the jumping status for determining teleport status
session.setJumping(true);
session.getConnector().getGeneralThreadPool().schedule(() -> session.setJumping(false), 1, TimeUnit.SECONDS);
}
break;
}
}

View File

@ -177,24 +177,24 @@ public class CollisionManager {
session.sendUpstreamPacket(movePlayerPacket);
}
public List<Vector3i> getPlayerCollidableBlocks() {
public List<Vector3i> getCollidableBlocks(BoundingBox box) {
List<Vector3i> blocks = new ArrayList<>();
Vector3d position = Vector3d.from(playerBoundingBox.getMiddleX(),
playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2),
playerBoundingBox.getMiddleZ());
Vector3d position = Vector3d.from(box.getMiddleX(),
box.getMiddleY() - (box.getSizeY() / 2),
box.getMiddleZ());
// Loop through all blocks that could collide with the player
int minCollisionX = (int) Math.floor(position.getX() - ((playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE));
int maxCollisionX = (int) Math.floor(position.getX() + (playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE);
// Loop through all blocks that could collide
int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE));
int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE);
// Y extends 0.5 blocks down because of fence hitboxes
int minCollisionY = (int) Math.floor(position.getY() - 0.5);
int maxCollisionY = (int) Math.floor(position.getY() + playerBoundingBox.getSizeY());
int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY());
int minCollisionZ = (int) Math.floor(position.getZ() - ((playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE));
int maxCollisionZ = (int) Math.floor(position.getZ() + (playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE);
int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE));
int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE);
for (int y = minCollisionY; y < maxCollisionY + 1; y++) {
for (int x = minCollisionX; x < maxCollisionX + 1; x++) {
@ -207,6 +207,10 @@ public class CollisionManager {
return blocks;
}
public List<Vector3i> getPlayerCollidableBlocks() {
return getCollidableBlocks(playerBoundingBox);
}
/**
* Returns false if the movement is invalid, and in this case it shouldn't be sent to the server and should be
* cancelled

View File

@ -69,6 +69,18 @@ public enum Enchantment {
QUICK_CHARGE,
SOUL_SPEED;
/**
* A list of all enchantment Java identifiers for use with command suggestions.
*/
public static final String[] ALL_JAVA_IDENTIFIERS;
static {
ALL_JAVA_IDENTIFIERS = new String[values().length];
for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {
ALL_JAVA_IDENTIFIERS[i] = values()[i].javaIdentifier;
}
}
private final String javaIdentifier;
Enchantment() {

View File

@ -63,6 +63,11 @@ public class ItemRegistry {
public static final List<StartGamePacket.ItemEntry> ITEMS = new ArrayList<>();
public static final Int2ObjectMap<ItemEntry> ITEM_ENTRIES = new Int2ObjectOpenHashMap<>();
/**
* A list of all Java item names.
*/
public static final String[] ITEM_NAMES;
/**
* Bamboo item entry, used in PandaEntity.java
*/
@ -116,6 +121,8 @@ public class ItemRegistry {
// Used to get the Bedrock namespaced ID (in instances where there are small differences)
Int2ObjectMap<String> bedrockIdToIdentifier = new Int2ObjectOpenHashMap<>();
List<String> itemNames = new ArrayList<>();
List<JsonNode> itemEntries;
try {
itemEntries = GeyserConnector.JSON_MAPPER.readValue(stream, itemEntriesType);
@ -207,6 +214,8 @@ public class ItemRegistry {
BUCKETS.add(entry.getValue().get("bedrock_id").intValue());
}
itemNames.add(entry.getKey());
itemIndex++;
}
@ -235,6 +244,8 @@ public class ItemRegistry {
creativeItems.add(ItemData.fromNet(netId++, item.getId(), item.getDamage(), item.getCount(), item.getTag()));
}
CREATIVE_ITEMS = creativeItems.toArray(new ItemData[0]);
ITEM_NAMES = itemNames.toArray(new String[0]);
}
/**

View File

@ -303,9 +303,8 @@ public abstract class ItemTranslator {
CompoundTag javaTag = new CompoundTag(name);
Map<String, Tag> javaValue = javaTag.getValue();
if (tag != null && !tag.isEmpty()) {
for (String str : tag.keySet()) {
Object bedrockTag = tag.get(str);
Tag translatedTag = translateToJavaNBT(str, bedrockTag);
for (Map.Entry<String, Object> entry : tag.entrySet()) {
Tag translatedTag = translateToJavaNBT(entry.getKey(), entry.getValue());
if (translatedTag == null)
continue;

View File

@ -33,21 +33,69 @@ import com.nukkitx.protocol.bedrock.data.command.CommandEnumData;
import com.nukkitx.protocol.bedrock.data.command.CommandParamData;
import com.nukkitx.protocol.bedrock.data.command.CommandParamType;
import com.nukkitx.protocol.bedrock.packet.AvailableCommandsPacket;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
import lombok.Getter;
import lombok.ToString;
import net.kyori.adventure.text.format.NamedTextColor;
import org.geysermc.connector.GeyserConnector;
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.Enchantment;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
@Translator(packet = ServerDeclareCommandsPacket.class)
public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclareCommandsPacket> {
private static final String[] ENUM_BOOLEAN = {"true", "false"};
private static final String[] VALID_COLORS;
private static final String[] VALID_SCOREBOARD_SLOTS;
private static final Hash.Strategy<CommandParamData[][]> PARAM_STRATEGY = new Hash.Strategy<CommandParamData[][]>() {
@Override
public int hashCode(CommandParamData[][] o) {
return Arrays.deepHashCode(o);
}
@Override
public boolean equals(CommandParamData[][] a, CommandParamData[][] b) {
if (a == b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
CommandParamData[] a1 = a[i];
CommandParamData[] b1 = b[i];
if (a1.length != b1.length) return false;
for (int j = 0; j < a1.length; j++) {
if (!a1[j].equals(b1[j])) return false;
}
}
return true;
}
};
static {
List<String> validColors = new ArrayList<>(NamedTextColor.NAMES.keys());
validColors.add("reset");
VALID_COLORS = validColors.toArray(new String[0]);
List<String> teamOptions = new ArrayList<>(Arrays.asList("list", "sidebar", "belowName"));
for (String color : NamedTextColor.NAMES.keys()) {
teamOptions.add("sidebar.team." + color);
}
VALID_SCOREBOARD_SLOTS = teamOptions.toArray(new String[0]);
}
@Override
public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) {
// Don't send command suggestions if they are disabled
@ -60,48 +108,50 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
return;
}
CommandNode[] nodes = packet.getNodes();
List<CommandData> commandData = new ArrayList<>();
Int2ObjectMap<String> commands = new Int2ObjectOpenHashMap<>();
IntSet commandNodes = new IntOpenHashSet();
Set<String> knownAliases = new HashSet<>();
Map<CommandParamData[][], Set<String>> commands = new Object2ObjectOpenCustomHashMap<>(PARAM_STRATEGY);
Int2ObjectMap<List<CommandNode>> commandArgs = new Int2ObjectOpenHashMap<>();
// Get the first node, it should be a root node
CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()];
CommandNode rootNode = nodes[packet.getFirstNodeIndex()];
// Loop through the root nodes to get all commands
for (int nodeIndex : rootNode.getChildIndices()) {
CommandNode node = packet.getNodes()[nodeIndex];
CommandNode node = nodes[nodeIndex];
// Make sure we don't have duplicated commands (happens if there is more than 1 root node)
if (commands.containsKey(nodeIndex)) { continue; }
if (commands.containsValue(node.getName())) { continue; }
if (!commandNodes.add(nodeIndex) || !knownAliases.add(node.getName().toLowerCase())) continue;
// Get and update the commandArgs list with the found arguments
if (node.getChildIndices().length >= 1) {
for (int childIndex : node.getChildIndices()) {
commandArgs.putIfAbsent(nodeIndex, new ArrayList<>());
commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]);
commandArgs.computeIfAbsent(nodeIndex, ArrayList::new).add(nodes[childIndex]);
}
}
// Insert the command name into the list
commands.put(nodeIndex, node.getName());
// Get and parse all params
CommandParamData[][] params = getParams(nodes[nodeIndex], nodes);
// Insert the alias name into the command list
commands.computeIfAbsent(params, index -> new HashSet<>()).add(node.getName().toLowerCase());
}
// The command flags, not sure what these do apart from break things
List<CommandData.Flag> flags = Collections.emptyList();
// Loop through all the found commands
for (int commandID : commands.keySet()) {
String commandName = commands.get(commandID);
for (Map.Entry<CommandParamData[][], Set<String>> entry : commands.entrySet()) {
String commandName = entry.getValue().iterator().next(); // We know this has a value
// Create a basic alias
CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", new String[] { commandName.toLowerCase() }, false);
// Get and parse all params
CommandParamData[][] params = getParams(packet.getNodes()[commandID], packet.getNodes());
CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", entry.getValue().toArray(new String[0]), false);
// Build the completed command and add it to the final list
CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, params);
CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, entry.getKey());
commandData.add(data);
}
@ -109,7 +159,7 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket();
availableCommandsPacket.getCommands().addAll(commandData);
GeyserConnector.getInstance().getLogger().debug("Sending command packet of " + commandData.size() + " commands");
session.getConnector().getLogger().debug("Sending command packet of " + commandData.size() + " commands");
// Finally, send the commands to the client
session.sendUpstreamPacket(availableCommandsPacket);
@ -119,11 +169,10 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
* Build the command parameter array for the given command
*
* @param commandNode The command to build the parameters for
* @param allNodes Every command node
*
* @param allNodes Every command node
* @return An array of parameter option arrays
*/
private CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] allNodes) {
private static CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] allNodes) {
// Check if the command is an alias and redirect it
if (commandNode.getRedirectIndex() != -1) {
GeyserConnector.getInstance().getLogger().debug("Redirecting command " + commandNode.getName() + " to " + allNodes[commandNode.getRedirectIndex()].getName());
@ -136,16 +185,8 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
rootParam.buildChildren(allNodes);
List<CommandParamData[]> treeData = rootParam.getTree();
CommandParamData[][] params = new CommandParamData[treeData.size()][];
// Fill the nested params array
int i = 0;
for (CommandParamData[] tree : treeData) {
params[i] = tree;
i++;
}
return params;
return treeData.toArray(new CommandParamData[0][]);
}
return new CommandParamData[0][0];
@ -155,14 +196,17 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
* Convert Java edition command types to Bedrock edition
*
* @param parser Command type to convert
*
* @return Bedrock parameter data type
*/
private CommandParamType mapCommandType(CommandParser parser) {
if (parser == null) { return CommandParamType.STRING; }
private static Object mapCommandType(CommandParser parser) {
if (parser == null) {
return CommandParamType.STRING;
}
switch (parser) {
case FLOAT:
case ROTATION:
case DOUBLE:
return CommandParamType.FLOAT;
case INTEGER:
@ -189,50 +233,44 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
return CommandParamType.JSON;
case RESOURCE_LOCATION:
case FUNCTION:
return CommandParamType.FILE_PATH;
case INT_RANGE:
return CommandParamType.INT_RANGE;
case BOOL:
case DOUBLE:
case STRING:
case VEC2:
return ENUM_BOOLEAN;
case OPERATION: // ">=", "==", etc
return CommandParamType.OPERATOR;
case BLOCK_STATE:
case BLOCK_PREDICATE:
return BlockTranslator.getAllBlockIdentifiers();
case ITEM_STACK:
case ITEM_PREDICATE:
case COLOR:
case COMPONENT:
case OBJECTIVE:
case OBJECTIVE_CRITERIA:
case OPERATION: // Possibly OPERATOR
case PARTICLE:
case ROTATION:
case SCOREBOARD_SLOT:
case SCORE_HOLDER:
case SWIZZLE:
case TEAM:
case ITEM_SLOT:
case MOB_EFFECT:
case FUNCTION:
case ENTITY_ANCHOR:
case RANGE:
case FLOAT_RANGE:
return ItemRegistry.ITEM_NAMES;
case ITEM_ENCHANTMENT:
return Enchantment.ALL_JAVA_IDENTIFIERS; //TODO: inventory branch use Java enums
case ENTITY_SUMMON:
case DIMENSION:
case TIME:
return EntityType.ALL_JAVA_IDENTIFIERS;
case COLOR:
return VALID_COLORS;
case SCOREBOARD_SLOT:
return VALID_SCOREBOARD_SLOTS;
default:
return CommandParamType.STRING;
}
}
@Getter
private class ParamInfo {
private CommandNode paramNode;
private CommandParamData paramData;
private List<ParamInfo> children;
@ToString
private static class ParamInfo {
private final CommandNode paramNode;
private final CommandParamData paramData;
private final List<ParamInfo> children;
/**
* Create a new parameter info object
@ -252,33 +290,50 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
* @param allNodes Every command node
*/
public void buildChildren(CommandNode[] allNodes) {
int enumIndex = -1;
for (int paramID : paramNode.getChildIndices()) {
CommandNode paramNode = allNodes[paramID];
if (paramNode.getParser() == null) {
if (enumIndex == -1) {
enumIndex = children.size();
boolean foundCompatible = false;
for (int i = 0; i < children.size(); i++) {
ParamInfo enumParamInfo = children.get(i);
// Check to make sure all descending nodes of this command are compatible - otherwise, create a new overload
if (isCompatible(allNodes, enumParamInfo.getParamNode(), paramNode)) {
foundCompatible = true;
// Extend the current list of enum values
String[] enumOptions = Arrays.copyOf(enumParamInfo.getParamData().getEnumData().getValues(), enumParamInfo.getParamData().getEnumData().getValues().length + 1);
enumOptions[enumOptions.length - 1] = paramNode.getName();
// Create the new enum command
CommandEnumData enumData = new CommandEnumData(paramNode.getName(), new String[] { paramNode.getName() }, false);
children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, enumData, mapCommandType(paramNode.getParser()), null, Collections.emptyList())));
} else {
// Get the existing enum
ParamInfo enumParamInfo = children.get(enumIndex);
// Extend the current list of enum values
String[] enumOptions = Arrays.copyOf(enumParamInfo.getParamData().getEnumData().getValues(), enumParamInfo.getParamData().getEnumData().getValues().length + 1);
enumOptions[enumOptions.length - 1] = paramNode.getName();
// Re-create the command using the updated values
CommandEnumData enumData = new CommandEnumData(enumParamInfo.getParamData().getEnumData().getName(), enumOptions, false);
children.set(enumIndex, new ParamInfo(enumParamInfo.getParamNode(), new CommandParamData(enumParamInfo.getParamData().getName(), false, enumData, enumParamInfo.getParamData().getType(), null, Collections.emptyList())));
// Re-create the command using the updated values
CommandEnumData enumData = new CommandEnumData(enumParamInfo.getParamData().getEnumData().getName(), enumOptions, false);
children.set(i, new ParamInfo(enumParamInfo.getParamNode(), new CommandParamData(enumParamInfo.getParamData().getName(), this.paramNode.isExecutable(), enumData, null, null, Collections.emptyList())));
break;
}
}
}else{
if (!foundCompatible) {
// Create a new subcommand with this exact type
CommandEnumData enumData = new CommandEnumData(paramNode.getName(), new String[]{paramNode.getName()}, false);
// On setting optional:
// isExecutable is defined as a node "constitutes a valid command."
// Therefore, any children of the parameter must simply be optional.
children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), this.paramNode.isExecutable(), enumData, null, null, Collections.emptyList())));
}
} else {
// Put the non-enum param into the list
children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, null, mapCommandType(paramNode.getParser()), null, Collections.emptyList())));
Object mappedType = mapCommandType(paramNode.getParser());
CommandEnumData enumData = null;
CommandParamType type = null;
if (mappedType instanceof String[]) {
enumData = new CommandEnumData(paramNode.getParser().name().toLowerCase(), (String[]) mappedType, false);
} else {
type = (CommandParamType) mappedType;
}
// IF enumData != null:
// In game, this will show up like <paramNode.getName(): enumData.getName()>
// So if paramNode.getName() == "value" and enumData.getName() == "bool": <value: bool>
children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), this.paramNode.isExecutable(), enumData, type, null, Collections.emptyList())));
}
}
@ -288,6 +343,64 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
}
}
/**
* Comparing CommandNode type a and b, determine if they are in the same overload.
* <p>
* Take the <code>gamerule</code> command, and let's present three "subcommands" you can perform:
*
* <ul>
* <li><code>gamerule doDaylightCycle true</code></li>
* <li><code>gamerule announceAdvancements false</code></li>
* <li><code>gamerule randomTickSpeed 3</code></li>
* </ul>
*
* While all three of them are indeed part of the same command, the command setting randomTickSpeed parses an int,
* while the others use boolean. In Bedrock, this should be presented as a separate overload to indicate that this
* does something a little different.
* <p>
* Therefore, this function will return <code>true</code> if the first two are compared, as they use the same
* parsers. If the third is compared with either of the others, this function will return <code>false</code>.
* <p>
* Here's an example of how the above would be presented to Bedrock (as of 1.16.200). Notice how the top two <code>CommandParamData</code>
* classes of each array are identical in type, but the following class is different:
* <pre>
* overloads=[
* [
* CommandParamData(name=doDaylightCycle, optional=false, enumData=CommandEnumData(name=announceAdvancements, values=[announceAdvancements, doDaylightCycle], isSoft=false), type=STRING, postfix=null, options=[])
* CommandParamData(name=value, optional=false, enumData=CommandEnumData(name=value, values=[true, false], isSoft=false), type=null, postfix=null, options=[])
* ]
* [
* CommandParamData(name=randomTickSpeed, optional=false, enumData=CommandEnumData(name=randomTickSpeed, values=[randomTickSpeed], isSoft=false), type=STRING, postfix=null, options=[])
* CommandParamData(name=value, optional=false, enumData=null, type=INT, postfix=null, options=[])
* ]
* ]
* </pre>
*
* @return if these two can be merged into one overload.
*/
private boolean isCompatible(CommandNode[] allNodes, CommandNode a, CommandNode b) {
if (a == b) return true;
if (a.getParser() != b.getParser()) return false;
if (a.getChildIndices().length != b.getChildIndices().length) return false;
for (int i = 0; i < a.getChildIndices().length; i++) {
boolean hasSimilarity = false;
CommandNode a1 = allNodes[a.getChildIndices()[i]];
// Search "b" until we find a child that matches this one
for (int j = 0; j < b.getChildIndices().length; j++) {
if (isCompatible(allNodes, a1, allNodes[b.getChildIndices()[j]])) {
hasSimilarity = true;
break;
}
}
if (!hasSimilarity) {
return false;
}
}
return true;
}
/**
* Get the tree of every parameter node (recursive)
*
@ -301,13 +414,10 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator<ServerDeclar
List<CommandParamData[]> childTree = child.getTree();
// Un-pack the tree append the child node to it and push into the list
for (CommandParamData[] subchild : childTree) {
CommandParamData[] tmpTree = new ArrayList<CommandParamData>() {
{
add(child.getParamData());
addAll(Arrays.asList(subchild));
}
}.toArray(new CommandParamData[0]);
for (CommandParamData[] subChild : childTree) {
CommandParamData[] tmpTree = new CommandParamData[subChild.length + 1];
tmpTree[0] = child.getParamData();
System.arraycopy(subChild, 0, tmpTree, 1, subChild.length);
treeParamData.add(tmpTree);
}

View File

@ -39,6 +39,9 @@ public class JavaKeepAliveTranslator extends PacketTranslator<ServerKeepAlivePac
@Override
public void translate(ServerKeepAlivePacket packet, GeyserSession session) {
if (!session.getConnector().getConfig().isForwardPlayerPing()) {
return;
}
NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket();
latencyPacket.setFromServer(true);
latencyPacket.setTimestamp(packet.getPingId() * 1000);

View File

@ -71,6 +71,7 @@ public class JavaTitleTranslator extends PacketTranslator<ServerTitlePacket> {
titlePacket.setFadeInTime(packet.getFadeIn());
titlePacket.setFadeOutTime(packet.getFadeOut());
titlePacket.setStayTime(packet.getStay());
titlePacket.setText("");
break;
}

View File

@ -60,6 +60,9 @@ public class JavaEntityAnimationTranslator extends PacketTranslator<ServerEntity
case LEAVE_BED:
animatePacket.setAction(AnimatePacket.Action.WAKE_UP);
break;
default:
// Unknown Animation
return;
}
session.sendUpstreamPacket(animatePacket);

View File

@ -32,6 +32,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnEntityPacket;
import com.nukkitx.math.vector.Vector3f;
import org.geysermc.connector.entity.*;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
@ -69,8 +70,18 @@ public class JavaSpawnEntityTranslator extends PacketTranslator<ServerSpawnEntit
type, position, motion, rotation, (HangingDirection) packet.getData());
} else if (packet.getType() == EntityType.FISHING_BOBBER) {
// Fishing bobbers need the owner for the line
entity = new FishingHookEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
type, position, motion, rotation, (ProjectileData) packet.getData());
int ownerEntityId = ((ProjectileData) packet.getData()).getOwnerId();
Entity owner = session.getEntityCache().getEntityByJavaId(ownerEntityId);
if (owner == null && session.getPlayerEntity().getEntityId() == ownerEntityId) {
owner = session.getPlayerEntity();
}
// Java clients only spawn fishing hooks with a player as its owner
if (owner instanceof PlayerEntity) {
entity = new FishingHookEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
type, position, motion, rotation, (PlayerEntity) owner);
} else {
return;
}
} else if (packet.getType() == EntityType.BOAT) {
// Initial rotation is incorrect
entity = new BoatEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),

View File

@ -52,7 +52,7 @@ public class JavaPlaySoundTranslator extends PacketTranslator<ServerPlaySoundPac
SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound.replace("minecraft:", ""));
String playsound;
if(soundMapping == null || soundMapping.getPlaysound() == null) {
if (soundMapping == null || soundMapping.getPlaysound() == null) {
// no mapping
session.getConnector().getLogger()
.debug("[PlaySound] Defaulting to sound server gave us for " + packet.toString());

View File

@ -44,7 +44,7 @@ public class JavaUnloadChunkTranslator extends PacketTranslator<ServerUnloadChun
Iterator<Vector3i> iterator = session.getSkullCache().keySet().iterator();
while (iterator.hasNext()) {
Vector3i position = iterator.next();
if (Math.floor(position.getX() / 16) == packet.getX() && Math.floor(position.getZ() / 16) == packet.getZ()) {
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
session.getSkullCache().get(position).despawnEntity(session);
iterator.remove();
}

View File

@ -49,70 +49,72 @@ public class BlockStateValues {
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap();
private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap();
private static final Int2IntMap WATER_LEVEL = new Int2IntOpenHashMap();
/**
* Determines if the block state contains Bedrock block information
*
* @param entry The String to JsonNode map used in BlockTranslator
* @param javaId The Java Identifier of the block
* @param javaBlockState the Java Block State of the block
* @param blockData JsonNode of info about the block from blocks.json
*/
public static void storeBlockStateValues(Map.Entry<String, JsonNode> entry, int javaBlockState) {
JsonNode bannerColor = entry.getValue().get("banner_color");
public static void storeBlockStateValues(String javaId, int javaBlockState, JsonNode blockData) {
JsonNode bannerColor = blockData.get("banner_color");
if (bannerColor != null) {
BANNER_COLORS.put(javaBlockState, (byte) bannerColor.intValue());
return; // There will never be a banner color and a skull variant
}
JsonNode bedColor = entry.getValue().get("bed_color");
JsonNode bedColor = blockData.get("bed_color");
if (bedColor != null) {
BED_COLORS.put(javaBlockState, (byte) bedColor.intValue());
return;
}
if (entry.getKey().contains("command_block")) {
COMMAND_BLOCK_VALUES.put(javaBlockState, entry.getKey().contains("conditional=true") ? (byte) 1 : (byte) 0);
if (javaId.contains("command_block")) {
COMMAND_BLOCK_VALUES.put(javaBlockState, javaId.contains("conditional=true") ? (byte) 1 : (byte) 0);
return;
}
if (entry.getValue().get("double_chest_position") != null) {
boolean isX = (entry.getValue().get("x") != null);
boolean isDirectionPositive = ((entry.getValue().get("x") != null && entry.getValue().get("x").asBoolean()) ||
(entry.getValue().get("z") != null && entry.getValue().get("z").asBoolean()));
boolean isLeft = (entry.getValue().get("double_chest_position").asText().contains("left"));
if (blockData.get("double_chest_position") != null) {
boolean isX = (blockData.get("x") != null);
boolean isDirectionPositive = ((blockData.get("x") != null && blockData.get("x").asBoolean()) ||
(blockData.get("z") != null && blockData.get("z").asBoolean()));
boolean isLeft = (blockData.get("double_chest_position").asText().contains("left"));
DOUBLE_CHEST_VALUES.put(javaBlockState, new DoubleChestValue(isX, isDirectionPositive, isLeft));
return;
}
if (entry.getKey().contains("potted_") || entry.getKey().contains("flower_pot")) {
FLOWER_POT_VALUES.put(javaBlockState, entry.getKey().replace("potted_", ""));
if (javaId.contains("potted_") || javaId.contains("flower_pot")) {
FLOWER_POT_VALUES.put(javaBlockState, javaId.replace("potted_", ""));
return;
}
JsonNode notePitch = entry.getValue().get("note_pitch");
JsonNode notePitch = blockData.get("note_pitch");
if (notePitch != null) {
NOTEBLOCK_PITCHES.put(javaBlockState, entry.getValue().get("note_pitch").intValue());
NOTEBLOCK_PITCHES.put(javaBlockState, blockData.get("note_pitch").intValue());
return;
}
if (entry.getKey().contains("piston")) {
if (javaId.contains("piston")) {
// True if extended, false if not
PISTON_VALUES.put(javaBlockState, entry.getKey().contains("extended=true"));
IS_STICKY_PISTON.put(javaBlockState, entry.getKey().contains("sticky"));
PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true"));
IS_STICKY_PISTON.put(javaBlockState, javaId.contains("sticky"));
return;
}
JsonNode skullVariation = entry.getValue().get("variation");
JsonNode skullVariation = blockData.get("variation");
if (skullVariation != null) {
SKULL_VARIANTS.put(javaBlockState, (byte) skullVariation.intValue());
}
JsonNode skullRotation = entry.getValue().get("skull_rotation");
JsonNode skullRotation = blockData.get("skull_rotation");
if (skullRotation != null) {
SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue());
}
if (entry.getKey().contains("wall_skull") || entry.getKey().contains("wall_head")) {
String direction = entry.getKey().substring(entry.getKey().lastIndexOf("facing=") + 7);
if (javaId.contains("wall_skull") || javaId.contains("wall_head")) {
String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7);
int rotation = 0;
switch (direction.substring(0, direction.length() - 1)) {
case "north":
@ -131,10 +133,16 @@ public class BlockStateValues {
SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation);
}
JsonNode shulkerDirection = entry.getValue().get("shulker_direction");
JsonNode shulkerDirection = blockData.get("shulker_direction");
if (shulkerDirection != null) {
BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue());
}
if (javaId.startsWith("minecraft:water")) {
String strLevel = javaId.substring(javaId.lastIndexOf("level=") + 6, javaId.length() - 1);
int level = Integer.parseInt(strLevel);
WATER_LEVEL.put(javaBlockState, level);
}
}
/**
@ -263,4 +271,15 @@ public class BlockStateValues {
public static byte getShulkerBoxDirection(int state) {
return SHULKERBOX_DIRECTIONS.getOrDefault(state, (byte) -1);
}
/**
* Get the level of water from the block state.
* This is used in FishingHookEntity to create splash sounds when the hook hits the water.
*
* @param state BlockState of the block
* @return The water level or -1 if the block isn't water
*/
public static int getWaterLevel(int state) {
return WATER_LEVEL.getOrDefault(state, -1);
}
}

View File

@ -185,7 +185,7 @@ public class BlockTranslator {
JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId);
BlockStateValues.storeBlockStateValues(entry, javaRuntimeId);
BlockStateValues.storeBlockStateValues(entry.getKey(), javaRuntimeId, entry.getValue());
String cleanJavaIdentifier = entry.getKey().split("\\[")[0];
@ -386,4 +386,11 @@ public class BlockTranslator {
}
return itemIdentifier;
}
/**
* @return a list of all Java block identifiers. For use with command suggestions.
*/
public static String[] getAllBlockIdentifiers() {
return JAVA_ID_TO_JAVA_IDENTIFIER_MAP.values().toArray(new String[0]);
}
}

View File

@ -27,6 +27,8 @@ package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.LongTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.nbt.NbtList;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.nbt.NbtType;
@ -39,7 +41,10 @@ import java.util.LinkedHashMap;
public class EndGatewayBlockEntityTranslator extends BlockEntityTranslator {
@Override
public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
builder.put("Age", (int) ((long) tag.get("Age").getValue()));
Tag ageTag = tag.get("Age");
if (ageTag instanceof LongTag) {
builder.put("Age", (int) ((long) ageTag.getValue()));
}
// Java sometimes does not provide this tag, but Bedrock crashes if it doesn't exist
// Linked coordinates
IntList tagsList = new IntArrayList();

View File

@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.auth.BedrockClientData;
@ -80,7 +81,7 @@ public class SkinManager {
String capeId, byte[] capeData,
SkinProvider.SkinGeometry geometry) {
SerializedSkin serializedSkin = SerializedSkin.of(
skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(capeData), geometry.getGeometryData(), "", true, false,
!capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
);
@ -163,7 +164,7 @@ public class SkinManager {
geometry = SkinProvider.SkinGeometry.getEars(data.isAlex());
// Store the skin and geometry for the ears
SkinProvider.storeEarSkin(entity.getUuid(), skin);
SkinProvider.storeEarSkin(skin);
SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex());
}
}
@ -267,7 +268,10 @@ public class SkinManager {
return new GameProfileData(skinUrl, capeUrl, isAlex);
} catch (Exception exception) {
GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage());
GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName());
if (GeyserConnector.getInstance().getConfig().isDebugMode()) {
exception.printStackTrace();
}
return loadBedrockOrOfflineSkin(profile);
}
}
@ -282,7 +286,7 @@ public class SkinManager {
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) {
if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) {
GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId());
if (session != null) {

View File

@ -79,13 +79,12 @@ public class SkinProvider {
.build();
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false);
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars();
public static String EARS_GEOMETRY;
public static String EARS_GEOMETRY_SLIM;
public static SkinGeometry SKULL_GEOMETRY;
public static final String EARS_GEOMETRY;
public static final String EARS_GEOMETRY_SLIM;
public static final SkinGeometry SKULL_GEOMETRY;
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@ -229,15 +228,15 @@ public class SkinProvider {
return CompletableFuture.completedFuture(officialCape);
}
public static CompletableFuture<Skin> requestEars(String earsUrl, EarsProvider provider, boolean newThread, Skin skin) {
public static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
CompletableFuture<Skin> future;
if (newThread) {
future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE)
future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE)
.whenCompleteAsync((outSkin, throwable) -> { });
} else {
Skin ears = supplyEars(skin, earsUrl, provider); // blocking
Skin ears = supplyEars(skin, earsUrl); // blocking
future = CompletableFuture.completedFuture(ears);
}
return future;
@ -255,7 +254,7 @@ public class SkinProvider {
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
for (EarsProvider provider : EarsProvider.VALUES) {
Skin skin1 = getOrDefault(
requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin),
requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin),
officialSkin, 4
);
if (skin1.isEars()) {
@ -295,12 +294,11 @@ public class SkinProvider {
}
/**
* Stores the ajusted skin with the ear texture to the cache
* Stores the adjusted skin with the ear texture to the cache
*
* @param playerID The UUID to cache it against
* @param skin The skin to cache
*/
public static void storeEarSkin(UUID playerID, Skin skin) {
public static void storeEarSkin(Skin skin) {
cachedSkins.put(skin.getTextureUrl(), skin);
}
@ -324,7 +322,7 @@ public class SkinProvider {
}
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = new byte[0];
byte[] cape = EMPTY_CAPE.getCapeData();
try {
cape = requestImage(capeUrl, provider);
} catch (Exception ignored) {} // just ignore I guess
@ -334,7 +332,7 @@ public class SkinProvider {
return new Cape(
capeUrl,
urlSection[urlSection.length - 1], // get the texture id and use it as cape id
cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(),
cape,
System.currentTimeMillis(),
cape.length == 0
);
@ -345,10 +343,9 @@ public class SkinProvider {
*
* @param existingSkin The players current skin
* @param earsUrl The URL to get the ears texture from
* @param provider The ears texture provider
* @return The updated skin with ears
*/
private static Skin supplyEars(Skin existingSkin, String earsUrl, EarsProvider provider) {
private static Skin supplyEars(Skin existingSkin, String earsUrl) {
try {
// Get the ears texture
BufferedImage ears = ImageIO.read(new URL(earsUrl));
@ -415,14 +412,15 @@ public class SkinProvider {
// if the requested image is a cape
if (provider != null) {
while(image.getWidth() > 64) {
image = scale(image);
if (image.getWidth() > 64) {
image = scale(image, 64, 32);
}
} else {
// Very rarely, skins can be larger than Minecraft's default.
// Bedrock will not render anything above a width of 128.
if (image.getWidth() > 128) {
image = scale(image, 128, image.getHeight() / (image.getWidth() / 128));
}
BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB);
Graphics g = newImage.createGraphics();
g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
g.dispose();
image = newImage;
}
byte[] data = bufferedImageToImageData(image);
@ -506,12 +504,13 @@ public class SkinProvider {
return null;
}
public static BufferedImage scale(BufferedImage bufferedImage) {
BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB);
public static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) {
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = resized.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, null);
g2.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null);
g2.dispose();
bufferedImage.flush();
return resized;
}
@ -578,17 +577,17 @@ public class SkinProvider {
@AllArgsConstructor
@Getter
public static class SkinAndCape {
private Skin skin;
private Cape cape;
private final Skin skin;
private final Cape cape;
}
@AllArgsConstructor
@Getter
public static class Skin {
private UUID skinOwner;
private String textureUrl;
private byte[] skinData;
private long requestedOn;
private final String textureUrl;
private final byte[] skinData;
private final long requestedOn;
private boolean updated;
private boolean ears;
@ -602,19 +601,19 @@ public class SkinProvider {
@AllArgsConstructor
@Getter
public static class Cape {
private String textureUrl;
private String capeId;
private byte[] capeData;
private long requestedOn;
private boolean failed;
private final String textureUrl;
private final String capeId;
private final byte[] capeData;
private final long requestedOn;
private final boolean failed;
}
@AllArgsConstructor
@Getter
public static class SkinGeometry {
private String geometryName;
private String geometryData;
private boolean failed;
private final String geometryName;
private final String geometryData;
private final boolean failed;
/**
* Generate generic geometry

View File

@ -27,65 +27,42 @@ package org.geysermc.connector.skin;
import com.nukkitx.protocol.bedrock.data.skin.ImageData;
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.Collections;
import java.util.UUID;
import java.util.function.Consumer;
public class SkullSkinManager extends SkinManager {
public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId,
String skinId, byte[] skinData) {
public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) {
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
skinId = skinId + "_skull";
SerializedSkin serializedSkin = SerializedSkin.of(
skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
return SerializedSkin.of(
skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
);
PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid);
entry.setName(username);
entry.setEntityId(geyserId);
entry.setSkin(serializedSkin);
entry.setXuid("");
entry.setPlatformChatId("");
entry.setTeacher(false);
entry.setTrustedSkin(true);
return entry;
}
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
Consumer<SkinProvider.Skin> skinConsumer) {
GameProfileData data = GameProfileData.from(entity.getProfile());
SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false)
SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true)
.whenCompleteAsync((skin, throwable) -> {
try {
if (session.getUpstream().isInitialized()) {
PlayerListPacket.Entry updatedEntry = buildSkullEntryManually(
entity.getUuid(),
entity.getUsername(),
entity.getGeyserId(),
skin.getTextureUrl(),
skin.getSkinData()
);
PlayerListPacket playerAddPacket = new PlayerListPacket();
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
playerAddPacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerAddPacket);
// It's a skull. We don't want them in the player list.
PlayerListPacket playerRemovePacket = new PlayerListPacket();
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
playerRemovePacket.getEntries().add(updatedEntry);
session.sendUpstreamPacket(playerRemovePacket);
PlayerSkinPacket packet = new PlayerSkinPacket();
packet.setUuid(entity.getUuid());
packet.setOldSkinName("");
packet.setNewSkinName(skin.getTextureUrl());
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
packet.setTrustedSkin(true);
session.sendUpstreamPacket(packet);
}
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);

View File

@ -26,7 +26,6 @@
package org.geysermc.connector.utils;
import com.nukkitx.protocol.bedrock.packet.SetTitlePacket;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import java.util.concurrent.TimeUnit;
@ -36,11 +35,10 @@ import java.util.concurrent.TimeUnit;
* Much of the work here is from the wonderful folks from ViaRewind: https://github.com/ViaVersion/ViaRewind
*/
public class CooldownUtils {
private static boolean SHOW_COOLDOWN;
private final static boolean SHOW_COOLDOWN;
static {
SHOW_COOLDOWN = GeyserConnector.getInstance().getConfig().isShowCooldown();
public static void setShowCooldown(boolean showCooldown) {
SHOW_COOLDOWN = showCooldown;
}
/**
@ -116,5 +114,4 @@ public class CooldownUtils {
}
return builder.toString();
}
}

View File

@ -90,21 +90,22 @@ public class FileUtils {
*/
public static File fileOrCopiedFromResource(File file, String name, Function<String, String> format) throws IOException {
if (!file.exists()) {
//noinspection ResultOfMethodCallIgnored
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
InputStream input = GeyserConnector.class.getResourceAsStream("/" + name); // resources need leading "/" prefix
try (FileOutputStream fos = new FileOutputStream(file)) {
try (InputStream input = GeyserConnector.class.getResourceAsStream("/" + name)) { // resources need leading "/" prefix
byte[] bytes = new byte[input.available()];
byte[] bytes = new byte[input.available()];
//noinspection ResultOfMethodCallIgnored
input.read(bytes);
input.read(bytes);
for(char c : format.apply(new String(bytes)).toCharArray()) {
fos.write(c);
}
for(char c : format.apply(new String(bytes)).toCharArray()) {
fos.write(c);
fos.flush();
}
}
fos.flush();
input.close();
fos.close();
}
return file;
@ -122,14 +123,13 @@ public class FileUtils {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
try (FileOutputStream fos = new FileOutputStream(file)) {
for (char c : data) {
fos.write(c);
}
for (char c : data) {
fos.write(c);
fos.flush();
}
fos.flush();
fos.close();
}
/**
@ -232,9 +232,10 @@ public class FileUtils {
try {
int size = stream.available();
byte[] bytes = new byte[size];
BufferedInputStream buf = new BufferedInputStream(stream);
buf.read(bytes, 0, bytes.length);
buf.close();
try (BufferedInputStream buf = new BufferedInputStream(stream)) {
//noinspection ResultOfMethodCallIgnored
buf.read(bytes, 0, bytes.length);
}
return bytes;
} catch (IOException e) {
throw new RuntimeException("Error while trying to read input stream!");

View File

@ -69,8 +69,8 @@ public class LanguageUtils {
// Load the locale
if (localeStream != null) {
Properties localeProp = new Properties();
try {
localeProp.load(new InputStreamReader(localeStream, StandardCharsets.UTF_8));
try (InputStreamReader reader = new InputStreamReader(localeStream, StandardCharsets.UTF_8)) {
localeProp.load(reader);
} catch (Exception e) {
throw new AssertionError(getLocaleStringLog("geyser.language.load_failed", locale), e);
}

View File

@ -101,7 +101,7 @@ public class LocaleUtils {
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
}
}
@ -147,6 +147,12 @@ public class LocaleUtils {
}
}
} catch (IOException ignored) { }
if (clientJarInfo == null) {
// Likely failed to download
GeyserConnector.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
return;
}
targetHash = clientJarInfo.getSha1();
} else {
curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
@ -168,9 +174,13 @@ public class LocaleUtils {
return;
}
// Get the hash and download the locale
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
try {
// Get the hash and download the locale
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error("Unable to download locale file hash", e);
}
}
/**
@ -236,31 +246,30 @@ public class LocaleUtils {
WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString());
// Load in the JAR as a zip and extract the file
ZipFile localeJar = new ZipFile(tmpFilePath.toString());
InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"));
FileOutputStream outStream = new FileOutputStream(localeFile);
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) {
try (FileOutputStream outStream = new FileOutputStream(localeFile)) {
// Write the file to the locale dir
byte[] buf = new byte[fileStream.available()];
int length;
while ((length = fileStream.read(buf)) != -1) {
outStream.write(buf, 0, length);
// Write the file to the locale dir
byte[] buf = new byte[fileStream.available()];
int length;
while ((length = fileStream.read(buf)) != -1) {
outStream.write(buf, 0, length);
}
// Flush all changes to disk and cleanup
outStream.flush();
}
}
}
// Flush all changes to disk and cleanup
outStream.flush();
outStream.close();
fileStream.close();
localeJar.close();
// Store the latest jar hash
FileUtils.writeFile(GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray());
// Delete the nolonger needed client/server jar
Files.delete(tmpFilePath);
} catch (Exception e) {
throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.locale.fail.en_us"), e);
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.locale.fail.en_us"), e);
}
}

View File

@ -30,6 +30,8 @@ import org.geysermc.connector.GeyserConnector;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
@ -70,10 +72,12 @@ public class ResourcePack {
pack.sha256 = FileUtils.calculateSHA256(file);
Stream<? extends ZipEntry> stream = null;
try {
ZipFile zip = new ZipFile(file);
zip.stream().forEach((x) -> {
stream = zip.stream();
stream.forEach((x) -> {
if (x.getName().contains("manifest.json")) {
try {
ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class);
@ -94,6 +98,10 @@ public class ResourcePack {
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.resource_pack.broken", file.getName()));
e.printStackTrace();
} finally {
if (stream != null) {
stream.close();
}
}
}
}

View File

@ -88,8 +88,7 @@ public class WebUtils {
}
public static String post(String reqURL, String postContent) throws IOException {
URL url = null;
url = new URL(reqURL);
URL url = new URL(reqURL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "text/plain");
@ -112,7 +111,7 @@ public class WebUtils {
*/
private static String connectionToString(HttpURLConnection con) throws IOException {
// Send the request (we dont use this but its required for getErrorStream() to work)
int code = con.getResponseCode();
con.getResponseCode();
// Read the error message if there is one if not just read normally
InputStream inputStream = con.getErrorStream();
@ -120,18 +119,18 @@ public class WebUtils {
inputStream = con.getInputStream();
}
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
String inputLine;
StringBuffer content = new StringBuffer();
StringBuilder content = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
content.append("\n");
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
content.append("\n");
}
con.disconnect();
}
in.close();
con.disconnect();
return content.toString();
}
}

View File

@ -18,10 +18,19 @@ bedrock:
# This option is for the plugin version only.
clone-remote-port: false
# The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true
motd1: "GeyserMC"
motd2: "Another GeyserMC forced host."
# If either of these are empty, the respective string will default to "Geyser"
motd1: "Geyser"
motd2: "Another Geyser server."
# The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.
server-name: "Geyser"
# Whether to enable PROXY protocol or not for clients. You DO NOT WANT this feature unless you run UDP reverse proxy
# in front of your Geyser instance.
enable-proxy-protocol: false
# A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and
# should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.).
# Keeping this list empty means there is no IP address whitelist.
# Both IP addresses and subnets are supported.
#proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/16" ]
remote:
# The IP address of the remote (Java Edition) server
# If it is "auto", for standalone version the remote address will be set to 127.0.0.1,
@ -81,7 +90,11 @@ legacy-ping-passthrough: false
# Increase if you are getting BrokenPipe errors.
ping-passthrough-interval: 3
# Maximum amount of players that can connect
# Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate
# ping, it may also cause players to time out more easily.
forward-player-ping: false
# Maximum amount of players that can connect. This is only visual at this time and does not actually limit player count.
max-players: 100
# If debug messages should be sent through console

@ -1 +1 @@
Subproject commit 2e52b01cc541c8925346f93be8940087d9af1661
Subproject commit bf0610450ce94507a18286e94af2965550ff9eaa