3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-11-20 06:50:09 +01:00

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

# Conflicts:
#	connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
Dieser Commit ist enthalten in:
Tim203 2021-03-23 01:35:02 +01:00
Commit 107cd5bd5a
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 064EE9F5BF7C3EE8
45 geänderte Dateien mit 830 neuen und 638 gelöschten Zeilen

Datei anzeigen

@ -52,7 +52,7 @@ public class GeyserSpigotFallbackWorldManager extends GeyserSpigotWorldManager {
} }
@Override @Override
public boolean hasMoreBlockDataThanChunkCache() { public boolean hasOwnChunkCache() {
return false; return false;
} }

Datei anzeigen

@ -140,7 +140,7 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
} }
@Override @Override
public boolean hasMoreBlockDataThanChunkCache() { public boolean hasOwnChunkCache() {
return true; return true;
} }
@ -235,6 +235,7 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
NbtMap blockEntityTag = lecternTag.build(); NbtMap blockEntityTag = lecternTag.build();
BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z)); BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z));
}; };
if (isChunkLoad) { if (isChunkLoad) {
// Delay to ensure the chunk is sent first, and then the lectern data // Delay to ensure the chunk is sent first, and then the lectern data
Bukkit.getScheduler().runTaskLater(this.plugin, lecternInfoGet, 5); Bukkit.getScheduler().runTaskLater(this.plugin, lecternInfoGet, 5);

Datei anzeigen

@ -13,7 +13,7 @@
<properties> <properties>
<netty.version>4.1.59.Final</netty.version> <netty.version>4.1.59.Final</netty.version>
<fastutil.version>8.5.2</fastutil.version> <fastutil.version>8.5.2</fastutil.version>
<adventure.version>4.5.0</adventure.version> <adventure.version>4.7.0</adventure.version>
</properties> </properties>
<dependencies> <dependencies>
@ -151,9 +151,9 @@
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.steveice10</groupId> <groupId>com.github.GeyserMC</groupId>
<artifactId>PacketLib</artifactId> <artifactId>PacketLib</artifactId>
<version>54f761c</version> <version>b77a427</version>
<scope>compile</scope> <scope>compile</scope>
<exclusions> <exclusions>
<exclusion> <!-- Move this exclusion back to MCProtocolLib it gets the latest PacketLib --> <exclusion> <!-- Move this exclusion back to MCProtocolLib it gets the latest PacketLib -->

Datei anzeigen

@ -40,7 +40,6 @@ import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.configuration.GeyserConfiguration;
import org.geysermc.connector.metrics.Metrics; import org.geysermc.connector.metrics.Metrics;
import org.geysermc.connector.network.ConnectorServerEventHandler; import org.geysermc.connector.network.ConnectorServerEventHandler;
import org.geysermc.connector.network.remote.RemoteServer;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.BiomeTranslator; import org.geysermc.connector.network.translators.BiomeTranslator;
import org.geysermc.connector.network.translators.EntityIdentifierRegistry; import org.geysermc.connector.network.translators.EntityIdentifierRegistry;
@ -103,9 +102,8 @@ public class GeyserConnector {
private static GeyserConnector instance; private static GeyserConnector instance;
private RemoteServer remoteServer;
@Setter @Setter
private AuthType authType; private AuthType defaultAuthType;
private FloodgateCipher cipher; private FloodgateCipher cipher;
private FloodgateSkinUploader skinUploader; private FloodgateSkinUploader skinUploader;
@ -175,7 +173,7 @@ public class GeyserConnector {
String remoteAddress = config.getRemote().getAddress(); String remoteAddress = config.getRemote().getAddress();
int remotePort = config.getRemote().getPort(); int remotePort = config.getRemote().getPort();
// Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry.
if ((config.isLegacyPingPassthrough() || platformType == PlatformType.STANDALONE) && !remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) {
try { try {
// Searches for a server address and a port from a SRV record of the specified host name // Searches for a server address and a port from a SRV record of the specified host name
InitialDirContext ctx = new InitialDirContext(); InitialDirContext ctx = new InitialDirContext();
@ -195,8 +193,7 @@ public class GeyserConnector {
} }
} }
remoteServer = new RemoteServer(config.getRemote().getAddress(), remotePort); defaultAuthType = AuthType.getByName(config.getRemote().getAuthType());
authType = AuthType.getByName(config.getRemote().getAuthType());
if (authType == AuthType.FLOODGATE) { if (authType == AuthType.FLOODGATE) {
try { try {
@ -355,8 +352,7 @@ public class GeyserConnector {
generalThreadPool.shutdown(); generalThreadPool.shutdown();
bedrockServer.close(); bedrockServer.close();
players.clear(); players.clear();
remoteServer = null; defaultAuthType = null;
authType = null;
this.getCommandManager().getCommands().clear(); this.getCommandManager().getCommands().clear();
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.done")); bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.done"));
@ -392,6 +388,7 @@ public class GeyserConnector {
* @param xuid the Xbox user identifier * @param xuid the Xbox user identifier
* @return the player or <code>null</code> if there is no player online with this xuid * @return the player or <code>null</code> if there is no player online with this xuid
*/ */
@SuppressWarnings("unused") // API usage
public GeyserSession getPlayerByXuid(String xuid) { public GeyserSession getPlayerByXuid(String xuid) {
for (GeyserSession session : players) { for (GeyserSession session : players) {
if (session.getAuthData() != null && session.getAuthData().getXboxUUID().equals(xuid)) { if (session.getAuthData() != null && session.getAuthData().getXboxUUID().equals(xuid)) {

Datei anzeigen

@ -28,12 +28,6 @@ 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.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType; import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.AttributeData; import com.nukkitx.protocol.bedrock.data.AttributeData;
import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityData;
@ -50,11 +44,9 @@ import org.geysermc.connector.entity.attribute.AttributeType;
import org.geysermc.connector.entity.living.ArmorStandEntity; import org.geysermc.connector.entity.living.ArmorStandEntity;
import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.player.PlayerEntity;
import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.utils.AttributeUtils;
import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.utils.AttributeUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -278,29 +270,6 @@ public class Entity {
if (!this.is(ArmorStandEntity.class)) { if (!this.is(ArmorStandEntity.class)) {
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, (xd & 0x20) == 0x20); metadata.getFlags().setFlag(EntityFlag.INVISIBLE, (xd & 0x20) == 0x20);
} }
// Shield code
if (session.getPlayerEntity().getEntityId() == entityId && metadata.getFlags().getFlag(EntityFlag.SNEAKING)) {
PlayerInventory playerInv = session.getPlayerInventory();
if ((playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) ||
(playerInv.getOffhand().getJavaId() == ItemRegistry.SHIELD.getJavaId())) {
ClientPlayerUseItemPacket useItemPacket;
metadata.getFlags().setFlag(EntityFlag.BLOCKING, true);
if (playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) {
useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
}
// Else we just assume it's the offhand, to simplify logic and to assure the packet gets sent
else {
useItemPacket = new ClientPlayerUseItemPacket(Hand.OFF_HAND);
}
session.sendDownstreamPacket(useItemPacket);
}
} else if (session.getPlayerEntity().getEntityId() == entityId && !metadata.getFlags().getFlag(EntityFlag.SNEAKING) && metadata.getFlags().getFlag(EntityFlag.BLOCKING)) {
metadata.getFlags().setFlag(EntityFlag.BLOCKING, false);
metadata.getFlags().setFlag(EntityFlag.IS_AVOIDING_BLOCK, true);
ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, new Position(0, 0, 0), BlockFace.DOWN);
session.sendDownstreamPacket(releaseItemPacket);
}
} }
break; break;
case 1: // Air/bubbles case 1: // Air/bubbles
@ -331,15 +300,12 @@ public class Entity {
case 6: // Pose change case 6: // Pose change
if (entityMetadata.getValue().equals(Pose.SLEEPING)) { if (entityMetadata.getValue().equals(Pose.SLEEPING)) {
metadata.getFlags().setFlag(EntityFlag.SLEEPING, true); metadata.getFlags().setFlag(EntityFlag.SLEEPING, true);
// Has to be a byte or it does not work
metadata.put(EntityData.PLAYER_FLAGS, (byte) 2);
metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.2f); metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.2f);
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.2f); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.2f);
} else if (metadata.getFlags().getFlag(EntityFlag.SLEEPING)) { } else if (metadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
metadata.getFlags().setFlag(EntityFlag.SLEEPING, false); metadata.getFlags().setFlag(EntityFlag.SLEEPING, false);
metadata.put(EntityData.BOUNDING_BOX_WIDTH, getEntityType().getWidth()); metadata.put(EntityData.BOUNDING_BOX_WIDTH, getEntityType().getWidth());
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, getEntityType().getHeight()); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, getEntityType().getHeight());
metadata.put(EntityData.PLAYER_FLAGS, (byte) 0);
} }
break; break;
} }

Datei anzeigen

@ -96,27 +96,25 @@ public class FishingHookEntity extends ThrowableEntity {
boolean touchingWater = false; boolean touchingWater = false;
boolean collided = false; boolean collided = false;
for (Vector3i blockPos : collidableBlocks) { for (Vector3i blockPos : collidableBlocks) {
if (0 <= blockPos.getY() && blockPos.getY() <= 255) { int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos); BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ());
BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ()); if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) {
if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) { // TODO Push bounding box out of collision to improve movement
// TODO Push bounding box out of collision to improve movement collided = true;
collided = true; }
}
int waterLevel = BlockStateValues.getWaterLevel(blockID); int waterLevel = BlockStateValues.getWaterLevel(blockID);
if (BlockTranslator.isWaterlogged(blockID)) { if (BlockTranslator.isWaterlogged(blockID)) {
waterLevel = 0; 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 (waterLevel >= 0) { if (position.getY() <= waterMaxY) {
double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0; touchingWater = true;
// Falling water is a full block
if (waterLevel >= 8) {
waterMaxY = blockPos.getY() + 1;
}
if (position.getY() <= waterMaxY) {
touchingWater = true;
}
} }
} }
} }
@ -177,10 +175,8 @@ public class FishingHookEntity extends ThrowableEntity {
*/ */
protected boolean isInAir(GeyserSession session) { protected boolean isInAir(GeyserSession session) {
if (session.getConnector().getConfig().isCacheChunks()) { if (session.getConnector().getConfig().isCacheChunks()) {
if (0 <= position.getFloorY() && position.getFloorY() <= 255) { int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); return block == BlockTranslator.JAVA_AIR_ID;
return block == BlockTranslator.JAVA_AIR_ID;
}
} }
return false; return false;
} }

Datei anzeigen

@ -99,6 +99,13 @@ public class LivingEntity extends Entity {
// Bed has to be updated, or else player is floating in the air // Bed has to be updated, or else player is floating in the air
ChunkUtils.updateBlock(session, bed, bedPosition); ChunkUtils.updateBlock(session, bed, bedPosition);
} }
// Indicate that the player should enter the sleep cycle
// Has to be a byte or it does not work
// (Bed position is what actually triggers sleep - "pose" is only optional)
metadata.put(EntityData.PLAYER_FLAGS, (byte) 2);
} else {
// Player is no longer sleeping
metadata.put(EntityData.PLAYER_FLAGS, (byte) 0);
} }
break; break;
} }

Datei anzeigen

@ -26,7 +26,6 @@
package org.geysermc.connector.entity.living.merchant; package org.geysermc.connector.entity.living.merchant;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData; import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData;
import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
@ -101,11 +100,17 @@ public class VillagerEntity extends AbstractMerchantEntity {
@Override @Override
public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) { public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
// No need to worry about extra processing to compensate for sleeping
super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
return;
}
int z = 0; int z = 0;
int bedId = 0; int bedId = 0;
float bedPositionSubtractorW = 0; float bedPositionSubtractorW = 0;
float bedPositionSubtractorN = 0; float bedPositionSubtractorN = 0;
Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION); Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION, null);
if (session.getConnector().getConfig().isCacheChunks() && bedPosition != null) { if (session.getConnector().getConfig().isCacheChunks() && bedPosition != null) {
bedId = session.getConnector().getWorldManager().getBlockAt(session, bedPosition); bedId = session.getConnector().getWorldManager().getBlockAt(session, bedPosition);
} }
@ -117,39 +122,33 @@ public class VillagerEntity extends AbstractMerchantEntity {
MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket(); MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
moveEntityPacket.setRuntimeEntityId(geyserId); moveEntityPacket.setRuntimeEntityId(geyserId);
//Sets Villager position and rotation when sleeping //Sets Villager position and rotation when sleeping
if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING)) { Pattern r = Pattern.compile("facing=([a-z]+)");
moveEntityPacket.setPosition(position); Matcher m = r.matcher(bedRotationZ);
moveEntityPacket.setRotation(getBedrockRotation()); if (m.find()) {
} else { switch (m.group(0)) {
//String Setup case "facing=south":
Pattern r = Pattern.compile("facing=([a-z]+)"); //bed is facing south
Matcher m = r.matcher(bedRotationZ); z = 180;
if (m.find()) { bedPositionSubtractorW = -.5f;
switch (m.group(0)) { break;
case "facing=south": case "facing=east":
//bed is facing south //bed is facing east
z = 180; z = 90;
bedPositionSubtractorW = -.5f; bedPositionSubtractorW = -.5f;
break; break;
case "facing=east": case "facing=west":
//bed is facing east //bed is facing west
z = 90; z = 270;
bedPositionSubtractorW = -.5f; bedPositionSubtractorW = .5f;
break; break;
case "facing=west": case "facing=north":
//bed is facing west //rotation does not change because north is 0
z = 270; bedPositionSubtractorN = .5f;
bedPositionSubtractorW = .5f; break;
break;
case "facing=north":
//rotation does not change because north is 0
bedPositionSubtractorN = .5f;
break;
}
} }
moveEntityPacket.setRotation(Vector3f.from(0, 0, z));
moveEntityPacket.setPosition(Vector3f.from(position.getX() + bedPositionSubtractorW, position.getY(), position.getZ() + bedPositionSubtractorN));
} }
moveEntityPacket.setRotation(Vector3f.from(0, 0, z));
moveEntityPacket.setPosition(Vector3f.from(position.getX() + bedPositionSubtractorW, position.getY(), position.getZ() + bedPositionSubtractorN));
moveEntityPacket.setOnGround(isOnGround); moveEntityPacket.setOnGround(isOnGround);
moveEntityPacket.setTeleported(false); moveEntityPacket.setTeleported(false);
session.sendUpstreamPacket(moveEntityPacket); session.sendUpstreamPacket(moveEntityPacket);

Datei anzeigen

@ -27,10 +27,13 @@ package org.geysermc.connector.inventory;
import com.github.steveice10.mc.protocol.data.game.window.WindowType; import com.github.steveice10.mc.protocol.data.game.window.WindowType;
import lombok.Getter; import lombok.Getter;
import org.geysermc.connector.network.session.GeyserSession;
public class Generic3X3Container extends Container { public class Generic3X3Container extends Container {
/** /**
* Whether we need to set the container type as {@link com.nukkitx.protocol.bedrock.data.inventory.ContainerType#DROPPER} * Whether we need to set the container type as {@link com.nukkitx.protocol.bedrock.data.inventory.ContainerType#DROPPER}.
*
* Used at {@link org.geysermc.connector.network.translators.inventory.translators.Generic3X3InventoryTranslator#openInventory(GeyserSession, Inventory)}
*/ */
@Getter @Getter
private boolean isDropper = false; private boolean isDropper = false;

Datei anzeigen

@ -62,6 +62,10 @@ public class GeyserItemStack {
this.netId = netId; this.netId = netId;
} }
public static GeyserItemStack from(ItemStack itemStack) {
return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getNbt());
}
public int getJavaId() { public int getJavaId() {
return isEmpty() ? 0 : javaId; return isEmpty() ? 0 : javaId;
} }
@ -74,10 +78,6 @@ public class GeyserItemStack {
return isEmpty() ? null : nbt; return isEmpty() ? null : nbt;
} }
public void setNetId(int netId) {
this.netId = netId;
}
public int getNetId() { public int getNetId() {
return isEmpty() ? 0 : netId; return isEmpty() ? 0 : netId;
} }
@ -90,10 +90,6 @@ public class GeyserItemStack {
amount -= sub; amount -= sub;
} }
public static GeyserItemStack from(ItemStack itemStack) {
return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getNbt());
}
public ItemStack getItemStack() { public ItemStack getItemStack() {
return getItemStack(amount); return getItemStack(amount);
} }

Datei anzeigen

@ -101,7 +101,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
public boolean handle(ResourcePackClientResponsePacket packet) { public boolean handle(ResourcePackClientResponsePacket packet) {
switch (packet.getStatus()) { switch (packet.getStatus()) {
case COMPLETED: case COMPLETED:
session.connect(connector.getRemoteServer()); session.connect();
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.connect", session.getAuthData().getName())); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.connect", session.getAuthData().getName()));
break; break;
@ -176,7 +176,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
public boolean handle(SetLocalPlayerAsInitializedPacket packet) { public boolean handle(SetLocalPlayerAsInitializedPacket packet) {
LanguageUtils.loadGeyserLocale(session.getLocale()); LanguageUtils.loadGeyserLocale(session.getLocale());
if (!session.isLoggedIn() && !session.isLoggingIn() && session.getConnector().getAuthType() == AuthType.ONLINE) { if (!session.isLoggedIn() && !session.isLoggingIn() && session.getRemoteAuthType() == AuthType.ONLINE) {
// TODO it is safer to key authentication on something that won't change (UUID, not username) // TODO it is safer to key authentication on something that won't change (UUID, not username)
if (!couldLoginUserByName(session.getAuthData().getName())) { if (!couldLoginUserByName(session.getAuthData().getName())) {
LoginEncryptionUtils.buildAndShowLoginWindow(session); LoginEncryptionUtils.buildAndShowLoginWindow(session);

Datei anzeigen

@ -1,37 +0,0 @@
/*
* 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.remote;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RemoteServer {
private String address;
private int port;
}

Datei anzeigen

@ -79,7 +79,6 @@ import org.geysermc.connector.entity.player.SessionPlayerEntity;
import org.geysermc.connector.entity.player.SkullPlayerEntity; import org.geysermc.connector.entity.player.SkullPlayerEntity;
import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.remote.RemoteServer;
import org.geysermc.connector.network.session.auth.AuthData; import org.geysermc.connector.network.session.auth.AuthData;
import org.geysermc.connector.network.session.auth.BedrockClientData; import org.geysermc.connector.network.session.auth.BedrockClientData;
import org.geysermc.connector.network.session.cache.*; import org.geysermc.connector.network.session.cache.*;
@ -112,13 +111,21 @@ public class GeyserSession implements CommandSender {
private final GeyserConnector connector; private final GeyserConnector connector;
private final UpstreamSession upstream; private final UpstreamSession upstream;
private RemoteServer remoteServer;
private Client downstream; private Client downstream;
@Setter @Setter
private AuthData authData; private AuthData authData;
@Setter @Setter
private BedrockClientData clientData; private BedrockClientData clientData;
/* Setter for GeyserConnect */
@Setter
private String remoteAddress;
@Setter
private int remotePort;
@Setter
private AuthType remoteAuthType;
/* Setter for GeyserConnect */
@Deprecated @Deprecated
@Setter @Setter
private boolean microsoftAccount; private boolean microsoftAccount;
@ -256,6 +263,12 @@ public class GeyserSession implements CommandSender {
@Setter @Setter
private Entity ridingVehicleEntity; private Entity ridingVehicleEntity;
/**
* The entity that the client is currently looking at.
*/
@Setter
private Entity mouseoverEntity;
@Setter @Setter
private Int2ObjectMap<Recipe> craftingRecipes; private Int2ObjectMap<Recipe> craftingRecipes;
private final Set<String> unlockedRecipes; private final Set<String> unlockedRecipes;
@ -431,9 +444,14 @@ public class GeyserSession implements CommandSender {
}); });
} }
public void connect(RemoteServer remoteServer) { /**
* Send all necessary packets to load Bedrock into the server
*/
public void connect() {
startGame(); startGame();
this.remoteServer = remoteServer; this.remoteAddress = connector.getConfig().getRemote().getAddress();
this.remotePort = connector.getConfig().getRemote().getPort();
this.remoteAuthType = connector.getDefaultAuthType();
// Set the hardcoded shield ID to the ID we just defined in StartGamePacket // Set the hardcoded shield ID to the ID we just defined in StartGamePacket
upstream.getSession().getHardcodedBlockingId().set(ItemRegistry.SHIELD.getBedrockId()); upstream.getSession().getHardcodedBlockingId().set(ItemRegistry.SHIELD.getBedrockId());
@ -478,8 +496,8 @@ public class GeyserSession implements CommandSender {
} }
public void login() { public void login() {
if (connector.getAuthType() != AuthType.ONLINE) { if (this.remoteAuthType != AuthType.ONLINE) {
if (connector.getAuthType() == AuthType.OFFLINE) { if (this.remoteAuthType == AuthType.OFFLINE) {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.offline")); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.offline"));
} else { } else {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.floodgate")); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.floodgate"));
@ -588,12 +606,13 @@ public class GeyserSession implements CommandSender {
* After getting whatever credentials needed, we attempt to join the Java server. * After getting whatever credentials needed, we attempt to join the Java server.
*/ */
private void connectDownstream() { private void connectDownstream() {
boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; boolean floodgate = this.remoteAuthType == AuthType.FLOODGATE;
// Start ticking // Start ticking
tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS);
downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); downstream = new Client(this.remoteAddress, this.remotePort, protocol, new TcpSessionFactory());
disableSrvResolving();
if (connector.getConfig().getRemote().isUseProxyProtocol()) { if (connector.getConfig().getRemote().isUseProxyProtocol()) {
downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
@ -652,7 +671,7 @@ public class GeyserSession implements CommandSender {
disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
return; return;
} }
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteAddress));
playerEntity.setUuid(protocol.getProfile().getId()); playerEntity.setUuid(protocol.getProfile().getId());
playerEntity.setUsername(protocol.getProfile().getName()); playerEntity.setUsername(protocol.getProfile().getName());
@ -673,7 +692,7 @@ public class GeyserSession implements CommandSender {
public void disconnected(DisconnectedEvent event) { public void disconnected(DisconnectedEvent event) {
loggingIn = false; loggingIn = false;
loggedIn = false; loggedIn = false;
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteAddress, event.getReason()));
if (event.getCause() != null) { if (event.getCause() != null) {
event.getCause().printStackTrace(); event.getCause().printStackTrace();
} }
@ -691,7 +710,7 @@ public class GeyserSession implements CommandSender {
playerEntity.setUuid(profile.getId()); playerEntity.setUuid(profile.getId());
// Check if they are not using a linked account // Check if they are not using a linked account
if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { if (remoteAuthType == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
SkinManager.handleBedrockSkin(playerEntity, clientData); SkinManager.handleBedrockSkin(playerEntity, clientData);
} }
@ -781,6 +800,18 @@ public class GeyserSession implements CommandSender {
this.sneaking = sneaking; this.sneaking = sneaking;
collisionManager.updatePlayerBoundingBox(); collisionManager.updatePlayerBoundingBox();
collisionManager.updateScaffoldingFlags(); collisionManager.updateScaffoldingFlags();
if (mouseoverEntity != null) {
// Horses, etc can change their property depending on if you're sneaking
InteractiveTagManager.updateTag(this, mouseoverEntity);
}
}
/**
* Will be overwritten for GeyserConnect.
*/
protected void disableSrvResolving() {
this.downstream.getSession().setFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, false);
} }
@Override @Override

Datei anzeigen

@ -29,23 +29,24 @@ import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.chunk.Column; import com.github.steveice10.mc.protocol.data.game.chunk.Column;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.geysermc.connector.bootstrap.GeyserBootstrap;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.MathUtils; import org.geysermc.connector.utils.MathUtils;
public class ChunkCache { public class ChunkCache {
private static final int MINIMUM_WORLD_HEIGHT = 0;
private final boolean cache; private final boolean cache;
private final Long2ObjectMap<Column> chunks = new Long2ObjectOpenHashMap<>(); private final Long2ObjectMap<Column> chunks;
public ChunkCache(GeyserSession session) { public ChunkCache(GeyserSession session) {
if (session.getConnector().getWorldManager().getClass() == GeyserBootstrap.DEFAULT_CHUNK_MANAGER.getClass()) { if (session.getConnector().getWorldManager().hasOwnChunkCache()) {
this.cache = session.getConnector().getConfig().isCacheChunks();
} else {
this.cache = false; // To prevent Spigot from initializing this.cache = false; // To prevent Spigot from initializing
} else {
this.cache = session.getConnector().getConfig().isCacheChunks();
} }
chunks = cache ? new Long2ObjectOpenHashMap<>() : null;
} }
public Column addToCache(Column chunk) { public Column addToCache(Column chunk) {
@ -86,6 +87,11 @@ public class ChunkCache {
return; return;
} }
if (y < MINIMUM_WORLD_HEIGHT || (y >> 4) > column.getChunks().length - 1) {
// Y likely goes above or below the height limit of this world
return;
}
Chunk chunk = column.getChunks()[y >> 4]; Chunk chunk = column.getChunks()[y >> 4];
if (chunk != null) { if (chunk != null) {
chunk.set(x & 0xF, y & 0xF, z & 0xF, block); chunk.set(x & 0xF, y & 0xF, z & 0xF, block);
@ -102,6 +108,11 @@ public class ChunkCache {
return BlockTranslator.JAVA_AIR_ID; return BlockTranslator.JAVA_AIR_ID;
} }
if (y < MINIMUM_WORLD_HEIGHT || (y >> 4) > column.getChunks().length - 1) {
// Y likely goes above or below the height limit of this world
return BlockTranslator.JAVA_AIR_ID;
}
Chunk chunk = column.getChunks()[y >> 4]; Chunk chunk = column.getChunks()[y >> 4];
if (chunk != null) { if (chunk != null) {
return chunk.get(x & 0xF, y & 0xF, z & 0xF); return chunk.get(x & 0xF, y & 0xF, z & 0xF);

Datei anzeigen

@ -94,7 +94,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
boolean dropAll = worldAction.getToItem().getCount() > 1; boolean dropAll = worldAction.getToItem().getCount() > 1;
ClientPlayerActionPacket dropAllPacket = new ClientPlayerActionPacket( ClientPlayerActionPacket dropAllPacket = new ClientPlayerActionPacket(
dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM, dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
new Position(0, 0, 0), BlockUtils.POSITION_ZERO,
BlockFace.DOWN BlockFace.DOWN
); );
session.sendDownstreamPacket(dropAllPacket); session.sendDownstreamPacket(dropAllPacket);
@ -292,7 +292,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
case ITEM_RELEASE: case ITEM_RELEASE:
if (packet.getActionType() == 0) { if (packet.getActionType() == 0) {
// Followed to the Minecraft Protocol specification outlined at wiki.vg // Followed to the Minecraft Protocol specification outlined at wiki.vg
ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, new Position(0,0,0), ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO,
BlockFace.DOWN); BlockFace.DOWN);
session.sendDownstreamPacket(releaseItemPacket); session.sendDownstreamPacket(releaseItemPacket);
} }

Datei anzeigen

@ -28,20 +28,23 @@ package org.geysermc.connector.network.translators.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction; import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.packet.ItemFrameDropItemPacket; import com.nukkitx.protocol.bedrock.packet.ItemFrameDropItemPacket;
import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
/**
* Pre-1.16.210: used for both survival and creative item frame item removal
*
* 1.16.210: only used in creative.
*/
@Translator(packet = ItemFrameDropItemPacket.class) @Translator(packet = ItemFrameDropItemPacket.class)
public class BedrockItemFrameDropItemTranslator extends PacketTranslator<ItemFrameDropItemPacket> { public class BedrockItemFrameDropItemTranslator extends PacketTranslator<ItemFrameDropItemPacket> {
@Override @Override
public void translate(ItemFrameDropItemPacket packet, GeyserSession session) { public void translate(ItemFrameDropItemPacket packet, GeyserSession session) {
Vector3i position = Vector3i.from(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()); ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()),
ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, position),
InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking()); InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
session.sendDownstreamPacket(interactPacket); session.sendDownstreamPacket(interactPacket);
} }

Datei anzeigen

@ -56,7 +56,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
BlockFace.values()[0], BlockFace.values()[0],
Hand.MAIN_HAND, Hand.MAIN_HAND,
packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ(), //TODO 0, 0, 0, // Java doesn't care about these when dealing with a lectern
false); false);
session.sendDownstreamPacket(blockPacket); session.sendDownstreamPacket(blockPacket);
} else { } else {
@ -65,6 +65,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
session.getConnector().getLogger().debug("Expected lectern but it wasn't open!"); session.getConnector().getLogger().debug("Expected lectern but it wasn't open!");
return; return;
} }
LecternContainer lecternContainer = (LecternContainer) session.getOpenInventory(); LecternContainer lecternContainer = (LecternContainer) session.getOpenInventory();
if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) { if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) {
// The same page means Bedrock is closing the window // The same page means Bedrock is closing the window
@ -76,9 +77,10 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
// Each "page" on Java is just one page (think a spiral notebook folded back to only show one page) // Each "page" on Java is just one page (think a spiral notebook folded back to only show one page)
int newJavaPage = (packet.getPage() * 2); int newJavaPage = (packet.getPage() * 2);
int currentJavaPage = (lecternContainer.getCurrentBedrockPage() * 2); int currentJavaPage = (lecternContainer.getCurrentBedrockPage() * 2);
// Send as many click button packets as we need to // Send as many click button packets as we need to
// Java has the option to specify exact page numbers by adding 100 to the number, but buttonId variable // Java has the option to specify exact page numbers by adding 100 to the number, but buttonId variable
// is a byte and therefore this stops us at 128 // is a byte when transmitted over the network and therefore this stops us at 128
if (newJavaPage > currentJavaPage) { if (newJavaPage > currentJavaPage) {
for (int i = currentJavaPage; i < newJavaPage; i++) { for (int i = currentJavaPage; i < newJavaPage; i++) {
ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 2); ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 2);

Datei anzeigen

@ -25,14 +25,19 @@
package org.geysermc.connector.network.translators.bedrock; package org.geysermc.connector.network.translators.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerChangeHeldItemPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerChangeHeldItemPacket;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket;
import org.geysermc.connector.utils.CooldownUtils; import org.geysermc.connector.utils.CooldownUtils;
import org.geysermc.connector.utils.InteractiveTagManager;
import java.util.concurrent.TimeUnit;
@Translator(packet = MobEquipmentPacket.class) @Translator(packet = MobEquipmentPacket.class)
public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipmentPacket> { public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipmentPacket> {
@ -53,7 +58,20 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot()); ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot());
session.sendDownstreamPacket(changeHeldItemPacket); session.sendDownstreamPacket(changeHeldItemPacket);
if (session.isSneaking() && session.getPlayerInventory().getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) {
// Activate shield since we are already sneaking
// (No need to send a release item packet - Java doesn't do this when swapping items)
// Required to do it a tick later or else it doesn't register
session.getConnector().getGeneralThreadPool().schedule(() -> session.sendDownstreamPacket(new ClientPlayerUseItemPacket(Hand.MAIN_HAND)),
50, TimeUnit.MILLISECONDS);
}
// Java sends a cooldown indicator whenever you switch an item // Java sends a cooldown indicator whenever you switch an item
CooldownUtils.sendCooldown(session); CooldownUtils.sendCooldown(session);
// Update the interactive tag, if an entity is present
if (session.getMouseoverEntity() != null) {
InteractiveTagManager.updateTag(session, session.getMouseoverEntity());
}
} }
} }

Datei anzeigen

@ -26,29 +26,28 @@
package org.geysermc.connector.network.translators.bedrock.entity.player; package org.geysermc.connector.network.translators.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.entity.player.*;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState;
import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace; import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerAbilitiesPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.*;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.PlayerActionType; import com.nukkitx.protocol.bedrock.data.PlayerActionType;
import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket; import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket;
import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket; import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket;
import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.utils.BlockUtils; import org.geysermc.connector.utils.BlockUtils;
@ -101,11 +100,37 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
case START_SNEAK: case START_SNEAK:
ClientPlayerStatePacket startSneakPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.START_SNEAKING); ClientPlayerStatePacket startSneakPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.START_SNEAKING);
session.sendDownstreamPacket(startSneakPacket); session.sendDownstreamPacket(startSneakPacket);
// Toggle the shield, if relevant
PlayerInventory playerInv = session.getPlayerInventory();
if ((playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) ||
(playerInv.getOffhand().getJavaId() == ItemRegistry.SHIELD.getJavaId())) {
ClientPlayerUseItemPacket useItemPacket;
if (playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) {
useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
} else {
// Else we just assume it's the offhand, to simplify logic and to assure the packet gets sent
useItemPacket = new ClientPlayerUseItemPacket(Hand.OFF_HAND);
}
session.sendDownstreamPacket(useItemPacket);
session.getPlayerEntity().getMetadata().getFlags().setFlag(EntityFlag.BLOCKING, true);
session.getPlayerEntity().updateBedrockMetadata(session);
}
session.setSneaking(true); session.setSneaking(true);
break; break;
case STOP_SNEAK: case STOP_SNEAK:
ClientPlayerStatePacket stopSneakPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.STOP_SNEAKING); ClientPlayerStatePacket stopSneakPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.STOP_SNEAKING);
session.sendDownstreamPacket(stopSneakPacket); session.sendDownstreamPacket(stopSneakPacket);
// Stop shield, if necessary
if (session.getPlayerEntity().getMetadata().getFlags().getFlag(EntityFlag.BLOCKING)) {
ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO, BlockFace.DOWN);
session.sendDownstreamPacket(releaseItemPacket);
session.getPlayerEntity().getMetadata().getFlags().setFlag(EntityFlag.BLOCKING, false);
session.getPlayerEntity().updateBedrockMetadata(session);
}
session.setSneaking(false); session.setSneaking(false);
break; break;
case START_SPRINT: case START_SPRINT:
@ -184,6 +209,18 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
session.sendUpstreamPacket(continueBreakPacket); session.sendUpstreamPacket(continueBreakPacket);
break; break;
case ABORT_BREAK: case ABORT_BREAK:
if (session.getGameMode() != GameMode.CREATIVE) {
// As of 1.16.210: item frame items are taken out here.
// Survival also sends START_BREAK, but by attaching our process here adventure mode also works
long entityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition());
if (entityId != -1) {
ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) entityId,
InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
session.sendDownstreamPacket(interactPacket);
break;
}
}
ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, BlockFace.DOWN); ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, BlockFace.DOWN);
session.sendDownstreamPacket(abortBreakingPacket); session.sendDownstreamPacket(abortBreakingPacket);
LevelEventPacket stopBreak = new LevelEventPacket(); LevelEventPacket stopBreak = new LevelEventPacket();

Datei anzeigen

@ -31,62 +31,21 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket;
import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
import com.nukkitx.protocol.bedrock.packet.InteractPacket; import com.nukkitx.protocol.bedrock.packet.InteractPacket;
import lombok.Getter;
import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.living.animal.horse.AbstractHorseEntity; import org.geysermc.connector.entity.living.animal.horse.AbstractHorseEntity;
import org.geysermc.connector.entity.living.animal.horse.HorseEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.utils.InteractiveTagManager;
import java.util.Arrays;
import java.util.List;
@Translator(packet = InteractPacket.class) @Translator(packet = InteractPacket.class)
public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> { public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> {
/**
* A list of all foods a horse/donkey can eat on Java Edition.
* Used to display interactive tag if needed.
*/
private static final List<String> DONKEY_AND_HORSE_FOODS = Arrays.asList("golden_apple", "enchanted_golden_apple",
"golden_carrot", "sugar", "apple", "wheat", "hay_block");
/**
* A list of all flowers. Used for feeding bees.
*/
private static final List<String> FLOWERS = Arrays.asList("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet",
"red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose",
"sunflower", "lilac", "rose_bush", "peony");
/**
* All entity types that can be leashed on Java Edition
*/
private static final List<EntityType> LEASHABLE_MOB_TYPES = Arrays.asList(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN,
EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE,
EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM,
EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT,
EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN);
private static final List<EntityType> SADDLEABLE_WHEN_TAMED_MOB_TYPES = Arrays.asList(EntityType.DONKEY, EntityType.HORSE,
EntityType.ZOMBIE_HORSE, EntityType.MULE);
/**
* A list of all foods a wolf can eat on Java Edition.
* Used to display interactive tag if needed.
*/
private static final List<String> WOLF_FOODS = Arrays.asList("pufferfish", "tropical_fish", "chicken", "cooked_chicken",
"porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton",
"cooked_rabbit");
@Override @Override
public void translate(InteractPacket packet, GeyserSession session) { public void translate(InteractPacket packet, GeyserSession session) {
Entity entity; Entity entity;
@ -122,241 +81,16 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
// Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc // Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc
if (packet.getRuntimeEntityId() != 0) { if (packet.getRuntimeEntityId() != 0) {
Entity interactEntity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId()); Entity interactEntity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
if (interactEntity == null) session.setMouseoverEntity(interactEntity);
if (interactEntity == null) {
return; return;
EntityDataMap entityMetadata = interactEntity.getMetadata();
ItemEntry itemEntry = session.getPlayerInventory().getItemInHand() == GeyserItemStack.EMPTY ? ItemEntry.AIR : ItemRegistry.getItem(session.getPlayerInventory().getItemInHand().getItemStack());
String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", "");
// TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen
// TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food
InteractiveTag interactiveTag = InteractiveTag.NONE;
if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) {
// Unleash the entity
interactiveTag = InteractiveTag.REMOVE_LEASH;
} else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) &&
((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) ||
interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) {
// Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed)
interactiveTag = InteractiveTag.SADDLE;
} else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null &&
session.getPlayerInventory().getItemInHand().getNbt().contains("display")) {
// Holding a named name tag
interactiveTag = InteractiveTag.NAME;
} else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) &&
entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == -1L) {
// Holding a leash and the mob is leashable for sure
// (Plugins can change this behavior so that's something to look into in the far far future)
interactiveTag = InteractiveTag.LEASH;
} else {
switch (interactEntity.getEntityType()) {
case BEE:
if (FLOWERS.contains(javaIdentifierStripped)) {
interactiveTag = InteractiveTag.FEED;
}
break;
case BOAT:
interactiveTag = InteractiveTag.BOARD_BOAT;
break;
case CAT:
if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
// Tamed and owned by player - can sit/stand
interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
break;
}
break;
case CHICKEN:
if (javaIdentifierStripped.contains("seeds")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case MOOSHROOM:
// Shear the mooshroom
if (javaIdentifierStripped.equals("shears")) {
interactiveTag = InteractiveTag.MOOSHROOM_SHEAR;
break;
}
// Bowls are acceptable here
else if (javaIdentifierStripped.equals("bowl")) {
interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW;
break;
}
// Fall down to COW as this works on mooshrooms
case COW:
if (javaIdentifierStripped.equals("wheat")) {
interactiveTag = InteractiveTag.FEED;
} else if (javaIdentifierStripped.equals("bucket")) {
// Milk the cow
interactiveTag = InteractiveTag.MILK;
}
break;
case CREEPER:
if (javaIdentifierStripped.equals("flint_and_steel")) {
// Today I learned that you can ignite a creeper with flint and steel! Huh.
interactiveTag = InteractiveTag.IGNITE_CREEPER;
}
break;
case DONKEY:
case LLAMA:
case MULE:
if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED)
&& javaIdentifierStripped.equals("chest")) {
// Can attach a chest
interactiveTag = InteractiveTag.ATTACH_CHEST;
break;
}
// Intentional fall-through
case HORSE:
case SKELETON_HORSE:
case TRADER_LLAMA:
case ZOMBIE_HORSE:
boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED);
if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) {
interactiveTag = InteractiveTag.OPEN_CONTAINER;
break;
}
// have another switch statement as, while these share mount attributes they don't share food
switch (interactEntity.getEntityType()) {
case LLAMA:
case TRADER_LLAMA:
if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) {
interactiveTag = InteractiveTag.FEED;
break;
}
case DONKEY:
case HORSE:
// Undead can't eat
if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) {
interactiveTag = InteractiveTag.FEED;
break;
}
}
if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) {
// Can't ride a baby
if (tamed) {
interactiveTag = InteractiveTag.RIDE_HORSE;
} else if (itemEntry.equals(ItemEntry.AIR)) {
// Can't hide an untamed entity without having your hand empty
interactiveTag = InteractiveTag.MOUNT;
}
}
break;
case FOX:
if (javaIdentifierStripped.equals("sweet_berries")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case HOGLIN:
if (javaIdentifierStripped.equals("crimson_fungus")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case MINECART:
interactiveTag = InteractiveTag.RIDE_MINECART;
break;
case MINECART_CHEST:
case MINECART_COMMAND_BLOCK:
case MINECART_HOPPER:
interactiveTag = InteractiveTag.OPEN_CONTAINER;
break;
case OCELOT:
if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case PANDA:
if (javaIdentifierStripped.equals("bamboo")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case PARROT:
if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case PIG:
if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) {
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
interactiveTag = InteractiveTag.MOUNT;
}
break;
case PIGLIN:
if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) {
interactiveTag = InteractiveTag.BARTER;
}
break;
case RABBIT:
if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case SHEEP:
if (javaIdentifierStripped.equals("wheat")) {
interactiveTag = InteractiveTag.FEED;
} else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) {
if (javaIdentifierStripped.equals("shears")) {
// Shear the sheep
interactiveTag = InteractiveTag.SHEAR;
} else if (javaIdentifierStripped.contains("_dye")) {
// Dye the sheep
interactiveTag = InteractiveTag.DYE;
}
}
break;
case STRIDER:
if (javaIdentifierStripped.equals("warped_fungus")) {
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
interactiveTag = InteractiveTag.RIDE_STRIDER;
}
break;
case TURTLE:
if (javaIdentifierStripped.equals("seagrass")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case VILLAGER:
if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0
&& entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby
interactiveTag = InteractiveTag.TRADE;
}
break;
case WANDERING_TRADER:
interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably.
break;
case WOLF:
if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) {
// Bone and untamed - can tame
interactiveTag = InteractiveTag.TAME;
} else if (WOLF_FOODS.contains(javaIdentifierStripped)) {
// Compatible food in hand - feed
// Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
// Tamed and owned by player - can sit/stand
interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
}
break;
case ZOMBIE_VILLAGER:
// We can't guarantee the existence of the weakness effect so we just always show it.
if (javaIdentifierStripped.equals("golden_apple")) {
interactiveTag = InteractiveTag.CURE;
}
break;
default:
break;
}
} }
session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue());
session.getPlayerEntity().updateBedrockMetadata(session); InteractiveTagManager.updateTag(session, interactEntity);
} else { } else {
if (!session.getPlayerEntity().getMetadata().getString(EntityData.INTERACTIVE_TAG).isEmpty()) { if (session.getMouseoverEntity() != null) {
// No interactive tag should be sent // No interactive tag should be sent
session.setMouseoverEntity(null);
session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, ""); session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, "");
session.getPlayerEntity().updateBedrockMetadata(session); session.getPlayerEntity().updateBedrockMetadata(session);
} }
@ -368,7 +102,7 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
if (ridingEntity instanceof AbstractHorseEntity) { if (ridingEntity instanceof AbstractHorseEntity) {
if (ridingEntity.getMetadata().getFlags().getFlag(EntityFlag.TAMED)) { if (ridingEntity.getMetadata().getFlags().getFlag(EntityFlag.TAMED)) {
// We should request to open the horse inventory instead // We should request to open the horse inventory instead
ClientPlayerStatePacket openHorseWindowPacket = new ClientPlayerStatePacket((int)session.getPlayerEntity().getEntityId(), PlayerState.OPEN_HORSE_INVENTORY); ClientPlayerStatePacket openHorseWindowPacket = new ClientPlayerStatePacket((int) session.getPlayerEntity().getEntityId(), PlayerState.OPEN_HORSE_INVENTORY);
session.sendDownstreamPacket(openHorseWindowPacket); session.sendDownstreamPacket(openHorseWindowPacket);
} }
} else { } else {
@ -385,65 +119,4 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
break; break;
} }
} }
/**
* All interactive tags in enum form. For potential API usage.
*/
public enum InteractiveTag {
NONE(true),
IGNITE_CREEPER("creeper"),
EDIT,
LEAVE_BOAT("exit.boat"),
FEED,
FISH("fishing"),
MILK,
MOOSHROOM_SHEAR("mooshear"),
MOOSHROOM_MILK_STEW("moostew"),
BOARD_BOAT("ride.boat"),
RIDE_MINECART("ride.minecart"),
RIDE_HORSE("ride.horse"),
RIDE_STRIDER("ride.strider"),
SHEAR,
SIT,
STAND,
TALK,
TAME,
DYE,
CURE,
OPEN_CONTAINER("opencontainer"),
CREATE_MAP("createMap"),
TAKE_PICTURE("takepicture"),
SADDLE,
MOUNT,
BOOST,
WRITE,
LEASH,
REMOVE_LEASH("unleash"),
NAME,
ATTACH_CHEST("attachchest"),
TRADE,
POSE_ARMOR_STAND("armorstand.pose"),
EQUIP_ARMOR_STAND("armorstand.equip"),
READ,
WAKE_VILLAGER("wakevillager"),
BARTER;
/**
* The full string that should be passed on to the client.
*/
@Getter
private final String value;
InteractiveTag(boolean isNone) {
this.value = "";
}
InteractiveTag(String value) {
this.value = "action.interact." + value;
}
InteractiveTag() {
this.value = "action.interact." + name().toLowerCase();
}
}
} }

Datei anzeigen

@ -32,6 +32,8 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
import com.github.steveice10.mc.protocol.data.game.window.WindowType; import com.github.steveice10.mc.protocol.data.game.window.WindowType;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest; import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest;
import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
@ -39,7 +41,11 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.geysermc.connector.inventory.*; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.inventory.CartographyContainer;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.click.Click; import org.geysermc.connector.network.translators.inventory.click.Click;
import org.geysermc.connector.network.translators.inventory.click.ClickPlan; import org.geysermc.connector.network.translators.inventory.click.ClickPlan;
@ -108,8 +114,8 @@ public abstract class InventoryTranslator {
public abstract void updateInventory(GeyserSession session, Inventory inventory); public abstract void updateInventory(GeyserSession session, Inventory inventory);
public abstract void updateSlot(GeyserSession session, Inventory inventory, int slot); public abstract void updateSlot(GeyserSession session, Inventory inventory, int slot);
public abstract int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData); public abstract int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData);
public abstract int javaSlotToBedrock(int javaSlot); //TODO public abstract int javaSlotToBedrock(int javaSlot);
public abstract BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot); //TODO public abstract BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot);
public abstract SlotType getSlotType(int javaSlot); public abstract SlotType getSlotType(int javaSlot);
public abstract Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory); public abstract Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory);
@ -138,7 +144,7 @@ public abstract class InventoryTranslator {
* If {@link #shouldHandleRequestFirst(StackRequestActionData, Inventory)} returns true, this will be called * If {@link #shouldHandleRequestFirst(StackRequestActionData, Inventory)} returns true, this will be called
*/ */
public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
return null; return rejectRequest(request);
} }
public void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) { public void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
@ -151,22 +157,32 @@ public abstract class InventoryTranslator {
if (shouldHandleRequestFirst(firstAction, inventory)) { if (shouldHandleRequestFirst(firstAction, inventory)) {
// Some special request that shouldn't be processed normally // Some special request that shouldn't be processed normally
response = translateSpecialRequest(session, inventory, request); response = translateSpecialRequest(session, inventory, request);
} else if (firstAction.getType() == StackRequestActionType.CRAFT_RECIPE) {
response = translateCraftingRequest(session, inventory, request);
} else if (firstAction.getType() == StackRequestActionType.CRAFT_RECIPE_AUTO) {
response = translateAutoCraftingRequest(session, inventory, request);
} else if (firstAction.getType() == StackRequestActionType.CRAFT_CREATIVE) {
// This is also used for pulling items out of creative
response = translateCreativeRequest(session, inventory, request);
} else { } else {
response = translateRequest(session, inventory, request); switch (firstAction.getType()) {
case CRAFT_RECIPE:
response = translateCraftingRequest(session, inventory, request);
break;
case CRAFT_RECIPE_AUTO:
response = translateAutoCraftingRequest(session, inventory, request);
break;
case CRAFT_CREATIVE:
// This is also used for pulling items out of creative
response = translateCreativeRequest(session, inventory, request);
break;
default:
response = translateRequest(session, inventory, request);
break;
}
} }
} else { } else {
response = rejectRequest(request); response = rejectRequest(request);
} }
if (response.getResult() == ItemStackResponsePacket.ResponseStatus.ERROR) {
if (response.getResult() != ItemStackResponsePacket.ResponseStatus.OK) {
// Sync our copy of the inventory with Bedrock's to prevent desyncs
refresh = true; refresh = true;
} }
responsePacket.getEntries().add(response); responsePacket.getEntries().add(response);
} }
session.sendUpstreamPacket(responsePacket); session.sendUpstreamPacket(responsePacket);
@ -191,11 +207,10 @@ public abstract class InventoryTranslator {
transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) { transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) {
return rejectRequest(request, false); return rejectRequest(request, false);
} }
session.getConnector().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.getName()); if (session.getConnector().getConfig().isDebugMode()) {
session.getConnector().getLogger().error("Source: " + transferAction.getSource().toString() + " Result: " + checkNetId(session, inventory, transferAction.getSource())); session.getConnector().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.getName());
session.getConnector().getLogger().error("Destination: " + transferAction.getDestination().toString() + " Result: " + checkNetId(session, inventory, transferAction.getDestination())); dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination());
session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(transferAction.getSource()))); }
session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(transferAction.getDestination())));
return rejectRequest(request); return rejectRequest(request);
} }
@ -278,11 +293,10 @@ public abstract class InventoryTranslator {
case SWAP: { case SWAP: {
SwapStackRequestActionData swapAction = (SwapStackRequestActionData) action; SwapStackRequestActionData swapAction = (SwapStackRequestActionData) action;
if (!(checkNetId(session, inventory, swapAction.getSource()) && checkNetId(session, inventory, swapAction.getDestination()))) { if (!(checkNetId(session, inventory, swapAction.getSource()) && checkNetId(session, inventory, swapAction.getDestination()))) {
session.getConnector().getLogger().error("DEBUG: About to reject SWAP request made by " + session.getName()); if (session.getConnector().getConfig().isDebugMode()) {
session.getConnector().getLogger().error("Source: " + swapAction.getSource().toString() + " Result: " + checkNetId(session, inventory, swapAction.getSource())); session.getConnector().getLogger().error("DEBUG: About to reject SWAP request made by " + session.getName());
session.getConnector().getLogger().error("Destination: " + swapAction.getDestination().toString() + " Result: " + checkNetId(session, inventory, swapAction.getDestination())); dumpStackRequestDetails(session, inventory, swapAction.getSource(), swapAction.getDestination());
session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(swapAction.getSource()))); }
session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(swapAction.getDestination())));
return rejectRequest(request); return rejectRequest(request);
} }
@ -357,17 +371,28 @@ public abstract class InventoryTranslator {
if (inventory instanceof CartographyContainer) { if (inventory instanceof CartographyContainer) {
// TODO add this for more inventories? Only seems to glitch out the cartography table, though. // TODO add this for more inventories? Only seems to glitch out the cartography table, though.
ConsumeStackRequestActionData consumeData = (ConsumeStackRequestActionData) action; ConsumeStackRequestActionData consumeData = (ConsumeStackRequestActionData) action;
int sourceSlot = bedrockSlotToJava(consumeData.getSource()); int sourceSlot = bedrockSlotToJava(consumeData.getSource());
if (sourceSlot == 0 && inventory.getItem(1).isEmpty()) { if ((sourceSlot == 0 && inventory.getItem(1).isEmpty()) || (sourceSlot == 1 && inventory.getItem(0).isEmpty())) {
// Java doesn't allow an item to be renamed; this is why CARTOGRAPHY_ADDITIONAL could remain empty for Bedrock // Java doesn't allow an item to be renamed; this is why one of the slots could remain empty for Bedrock
// We check this during slot 0 since setting the inventory slots here messes up shouldRejectItemPlace // We check this now since setting the inventory slots here messes up shouldRejectItemPlace
return rejectRequest(request, false); return rejectRequest(request, false);
} }
GeyserItemStack item = inventory.getItem(sourceSlot); if (sourceSlot == 1) {
item.setAmount(item.getAmount() - consumeData.getCount()); // Decrease the item count, but only after both slots are checked.
if (item.isEmpty()) { // Otherwise, the slot 1 check will fail
inventory.setItem(sourceSlot, GeyserItemStack.EMPTY, session); GeyserItemStack item = inventory.getItem(sourceSlot);
item.setAmount(item.getAmount() - consumeData.getCount());
if (item.isEmpty()) {
inventory.setItem(sourceSlot, GeyserItemStack.EMPTY, session);
}
GeyserItemStack itemZero = inventory.getItem(0);
itemZero.setAmount(itemZero.getAmount() - consumeData.getCount());
if (itemZero.isEmpty()) {
inventory.setItem(0, GeyserItemStack.EMPTY, session);
}
} }
affectedSlots.add(sourceSlot); affectedSlots.add(sourceSlot);
} }
@ -682,8 +707,10 @@ public abstract class InventoryTranslator {
return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots())); return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots()));
} }
/**
* Handled in {@link PlayerInventoryTranslator}
*/
public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
// Handled in PlayerInventoryTranslator
return rejectRequest(request); return rejectRequest(request);
} }
@ -736,13 +763,22 @@ public abstract class InventoryTranslator {
* as bad (false). * as bad (false).
*/ */
public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) { public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) {
if (throwError) { if (throwError && GeyserConnector.getInstance().getConfig().isDebugMode()) {
// Currently for debugging, but might be worth it to keep in the future if something goes terribly wrong.
new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace();
} }
return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.ERROR, request.getRequestId(), Collections.emptyList()); return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.ERROR, request.getRequestId(), Collections.emptyList());
} }
/**
* Print out the contents of an ItemStackRequest, should the net ID check fail.
*/
protected void dumpStackRequestDetails(GeyserSession session, Inventory inventory, StackRequestSlotInfoData source, StackRequestSlotInfoData destination) {
session.getConnector().getLogger().error("Source: " + source.toString() + " Result: " + checkNetId(session, inventory, source));
session.getConnector().getLogger().error("Destination: " + destination.toString() + " Result: " + checkNetId(session, inventory, destination));
session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(source)));
session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(destination)));
}
public boolean checkNetId(GeyserSession session, Inventory inventory, StackRequestSlotInfoData slotInfoData) { public boolean checkNetId(GeyserSession session, Inventory inventory, StackRequestSlotInfoData slotInfoData) {
int netId = slotInfoData.getStackNetworkId(); int netId = slotInfoData.getStackNetworkId();
// "In my testing, sometimes the client thinks the netId of an item in the crafting grid is 1, even though we never said it was. // "In my testing, sometimes the client thinks the netId of an item in the crafting grid is 1, even though we never said it was.
@ -823,7 +859,17 @@ public abstract class InventoryTranslator {
public static ItemStackResponsePacket.ItemEntry makeItemEntry(int bedrockSlot, GeyserItemStack itemStack) { public static ItemStackResponsePacket.ItemEntry makeItemEntry(int bedrockSlot, GeyserItemStack itemStack) {
ItemStackResponsePacket.ItemEntry itemEntry; ItemStackResponsePacket.ItemEntry itemEntry;
if (!itemStack.isEmpty()) { if (!itemStack.isEmpty()) {
itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) itemStack.getAmount(), itemStack.getNetId(), "", 0); // As of 1.16.210: Bedrock needs confirmation on what the current item durability is.
// If 0 is sent, then Bedrock thinks the item is not damaged
int durability = 0;
if (itemStack.getNbt() != null) {
Tag damage = itemStack.getNbt().get("Damage");
if (damage instanceof IntTag) {
durability = ((IntTag) damage).getValue();
}
}
itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) itemStack.getAmount(), itemStack.getNetId(), "", durability);
} else { } else {
itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) 0, 0, "", 0); itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) 0, 0, "", 0);
} }

Datei anzeigen

@ -74,7 +74,7 @@ public class BlockInventoryHolder extends InventoryHolder {
// Check to see if there is an existing block we can use that the player just selected. // Check to see if there is an existing block we can use that the player just selected.
// First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range.
// (This could be a virtual inventory that the player is opening) // (This could be a virtual inventory that the player is opening)
if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) { if (checkInteractionPosition(session)) {
// Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid
int javaBlockId = session.getConnector().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); int javaBlockId = session.getConnector().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition());
String[] javaBlockString = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(javaBlockId, "minecraft:air").split("\\["); String[] javaBlockString = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(javaBlockId, "minecraft:air").split("\\[");
@ -101,6 +101,16 @@ public class BlockInventoryHolder extends InventoryHolder {
setCustomName(session, position, inventory, defaultJavaBlockState); setCustomName(session, position, inventory, defaultJavaBlockState);
} }
/**
* Will be overwritten in the beacon inventory translator to remove the check, since virtual inventories can't exist.
*
* @return if the player's last interaction position and current position match. Used to ensure that we don't select
* a block to hold the inventory that's wildly out of range.
*/
protected boolean checkInteractionPosition(GeyserSession session) {
return session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition());
}
/** /**
* @return true if this Java block ID can be used for player inventory. * @return true if this Java block ID can be used for player inventory.
*/ */

Datei anzeigen

@ -44,13 +44,40 @@ import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder;
import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater;
import org.geysermc.connector.utils.InventoryUtils;
import java.util.Collections; import java.util.Collections;
public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator { public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator {
public BeaconInventoryTranslator() { public BeaconInventoryTranslator() {
super(1, "minecraft:beacon", ContainerType.BEACON, UIInventoryUpdater.INSTANCE); super(1, new BlockInventoryHolder("minecraft:beacon", ContainerType.BEACON) {
@Override
public void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
if (!session.getConnector().getConfig().isCacheChunks()) {
// Beacons cannot work without knowing their physical location
return;
}
super.prepareInventory(translator, session, inventory);
}
@Override
protected boolean checkInteractionPosition(GeyserSession session) {
// Since we can't fall back to a virtual inventory, let's make opening one easier
return true;
}
@Override
public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
if (!session.getConnector().getConfig().isCacheChunks() || !((BeaconContainer) inventory).isUsingRealBlock()) {
InventoryUtils.closeInventory(session, inventory.getId(), false);
return;
}
super.openInventory(translator, session, inventory);
}
}, UIInventoryUpdater.INSTANCE);
} }
@Override @Override

Datei anzeigen

@ -91,6 +91,8 @@ public class BrewingInventoryTranslator extends AbstractBlockInventoryTranslator
return 3; return 3;
case 3: case 3:
return 0; return 0;
case 4:
return 4;
} }
return super.javaSlotToBedrock(slot); return super.javaSlotToBedrock(slot);
} }
@ -105,7 +107,7 @@ public class BrewingInventoryTranslator extends AbstractBlockInventoryTranslator
case 3: case 3:
return new BedrockContainerSlot(ContainerSlotType.BREWING_INPUT, 0); return new BedrockContainerSlot(ContainerSlotType.BREWING_INPUT, 0);
case 4: case 4:
return new BedrockContainerSlot(ContainerSlotType.BREWING_INPUT, 0); return new BedrockContainerSlot(ContainerSlotType.BREWING_FUEL, 4);
} }
return super.javaSlotToBedrockContainer(slot); return super.javaSlotToBedrockContainer(slot);
} }

Datei anzeigen

@ -46,27 +46,27 @@ public class CartographyInventoryTranslator extends AbstractBlockInventoryTransl
public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer, public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer,
int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) { int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) {
if (javaDestinationSlot == 0) { if (javaDestinationSlot == 0) {
// Bedrock Edition can use paper in slot 0 // Bedrock Edition can use paper or an empty map in slot 0
GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot); GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot);
return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:paper"); return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:paper") || itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:map");
} else if (javaDestinationSlot == 1) { } else if (javaDestinationSlot == 1) {
// Bedrock Edition can use a compass to create locator maps in the ADDITIONAL slot // Bedrock Edition can use a compass to create locator maps, or use a filled map, in the ADDITIONAL slot
GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot); GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot);
return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:compass"); return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:compass") || itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:filled_map");
} }
return false; return false;
} }
@Override @Override
public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) {
if (slotInfoData.getContainer() == ContainerSlotType.CARTOGRAPHY_INPUT) { switch (slotInfoData.getContainer()) {
return 0; case CARTOGRAPHY_INPUT:
} return 0;
if (slotInfoData.getContainer() == ContainerSlotType.CARTOGRAPHY_ADDITIONAL) { case CARTOGRAPHY_ADDITIONAL:
return 1; return 1;
} case CARTOGRAPHY_RESULT:
if (slotInfoData.getContainer() == ContainerSlotType.CARTOGRAPHY_RESULT || slotInfoData.getContainer() == ContainerSlotType.CREATIVE_OUTPUT) { case CREATIVE_OUTPUT:
return 2; return 2;
} }
return super.bedrockSlotToJava(slotInfoData); return super.bedrockSlotToJava(slotInfoData);
} }

Datei anzeigen

@ -39,8 +39,9 @@ public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslato
@Override @Override
public SlotType getSlotType(int javaSlot) { public SlotType getSlotType(int javaSlot) {
if (javaSlot == 0) if (javaSlot == 0) {
return SlotType.OUTPUT; return SlotType.OUTPUT;
}
return SlotType.NORMAL; return SlotType.NORMAL;
} }

Datei anzeigen

@ -51,22 +51,19 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
public LecternInventoryTranslator() { public LecternInventoryTranslator() {
super(1); super(1);
this.updater = new LecternInventoryUpdater(); this.updater = new InventoryUpdater();
} }
@Override @Override
public void prepareInventory(GeyserSession session, Inventory inventory) { public void prepareInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override
public void openInventory(GeyserSession session, Inventory inventory) { public void openInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override
public void closeInventory(GeyserSession session, Inventory inventory) { public void closeInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override
@ -81,7 +78,6 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
@Override @Override
public void updateInventory(GeyserSession session, Inventory inventory) { public void updateInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override
@ -171,8 +167,4 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
} }
return builder; return builder;
} }
private static class LecternInventoryUpdater extends InventoryUpdater {
}
} }

Datei anzeigen

@ -44,6 +44,7 @@ import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
import org.geysermc.connector.network.translators.inventory.SlotType;
import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater;
import org.geysermc.connector.network.translators.item.translators.BannerTranslator; import org.geysermc.connector.network.translators.item.translators.BannerTranslator;
@ -144,7 +145,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), index); ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), index);
session.sendDownstreamPacket(packet); session.sendDownstreamPacket(packet);
GeyserItemStack inputCopy = inventory.getItem(0).copy(); GeyserItemStack inputCopy = inventory.getItem(0).copy(1);
inputCopy.setNetId(session.getNextItemNetId()); inputCopy.setNetId(session.getNextItemNetId());
// Add the pattern manually, for better item synchronization // Add the pattern manually, for better item synchronization
if (inputCopy.getNbt() == null) { if (inputCopy.getNbt() == null) {
@ -219,4 +220,12 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
} }
return super.javaSlotToBedrock(slot); return super.javaSlotToBedrock(slot);
} }
@Override
public SlotType getSlotType(int javaSlot) {
if (javaSlot == 3) {
return SlotType.OUTPUT;
}
return super.getSlotType(javaSlot);
}
} }

Datei anzeigen

@ -41,17 +41,14 @@ public abstract class AbstractHorseInventoryTranslator extends BaseInventoryTran
@Override @Override
public void prepareInventory(GeyserSession session, Inventory inventory) { public void prepareInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override
public void openInventory(GeyserSession session, Inventory inventory) { public void openInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override
public void closeInventory(GeyserSession session, Inventory inventory) { public void closeInventory(GeyserSession session, Inventory inventory) {
} }
@Override @Override

Datei anzeigen

@ -64,5 +64,4 @@ public class HorseInventoryUpdater extends InventoryUpdater {
session.sendUpstreamPacket(slotPacket); session.sendUpstreamPacket(slotPacket);
return true; return true;
} }
} }

Datei anzeigen

@ -35,7 +35,7 @@ import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
import java.util.Arrays; import java.util.Arrays;
public abstract class InventoryUpdater { public class InventoryUpdater {
public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
ItemData[] bedrockItems = new ItemData[36]; ItemData[] bedrockItems = new ItemData[36];
for (int i = 0; i < 36; i++) { for (int i = 0; i < 36; i++) {

Datei anzeigen

@ -35,29 +35,28 @@ import org.geysermc.connector.network.translators.item.ItemEntry;
@ItemRemapper @ItemRemapper
public class LeatherArmorTranslator extends NbtItemStackTranslator { public class LeatherArmorTranslator extends NbtItemStackTranslator {
private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate", "minecraft:leather_leggings", "minecraft:leather_boots"}; private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate",
"minecraft:leather_leggings", "minecraft:leather_boots", "minecraft:leather_horse_armor"};
@Override @Override
public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) {
if (!itemTag.contains("display")) { CompoundTag displayTag = itemTag.get("display");
if (displayTag == null) {
return; return;
} }
CompoundTag displayTag = itemTag.get("display"); IntTag color = displayTag.get("color");
if (displayTag.contains("color")) { if (color != null) {
IntTag color = displayTag.get("color"); itemTag.put(new IntTag("customColor", color.getValue()));
if (color != null) { displayTag.remove("color");
itemTag.put(new IntTag("customColor", color.getValue()));
displayTag.remove("color");
}
} }
} }
@Override @Override
public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) {
if (!itemTag.contains("customColor")) { IntTag color = itemTag.get("customColor");
if (color == null) {
return; return;
} }
IntTag color = itemTag.get("customColor");
CompoundTag displayTag = itemTag.get("display"); CompoundTag displayTag = itemTag.get("display");
if (displayTag == null) { if (displayTag == null) {
displayTag = new CompoundTag("display"); displayTag = new CompoundTag("display");

Datei anzeigen

@ -32,6 +32,7 @@ import org.geysermc.connector.network.translators.Translator;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityMetadataPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityMetadataPacket;
import org.geysermc.connector.utils.InteractiveTagManager;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
@Translator(packet = ServerEntityMetadataPacket.class) @Translator(packet = ServerEntityMetadataPacket.class)
@ -39,9 +40,11 @@ public class JavaEntityMetadataTranslator extends PacketTranslator<ServerEntityM
@Override @Override
public void translate(ServerEntityMetadataPacket packet, GeyserSession session) { public void translate(ServerEntityMetadataPacket packet, GeyserSession session) {
Entity entity = session.getEntityCache().getEntityByJavaId(packet.getEntityId()); Entity entity;
if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) { if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
entity = session.getPlayerEntity(); entity = session.getPlayerEntity();
} else {
entity = session.getEntityCache().getEntityByJavaId(packet.getEntityId());
} }
if (entity == null) return; if (entity == null) return;
@ -61,5 +64,10 @@ public class JavaEntityMetadataTranslator extends PacketTranslator<ServerEntityM
} }
entity.updateBedrockMetadata(session); entity.updateBedrockMetadata(session);
// Update the interactive tag, if necessary
if (session.getMouseoverEntity() != null && session.getMouseoverEntity().getEntityId() == entity.getEntityId()) {
InteractiveTagManager.updateTag(session, entity);
}
} }
} }

Datei anzeigen

@ -134,7 +134,6 @@ public class JavaSetSlotTranslator extends PacketTranslator<ServerSetSlotPacket>
height += -firstRow + 1; height += -firstRow + 1;
width += -firstCol + 1; width += -firstCol + 1;
//TODO
recipes: recipes:
for (Recipe recipe : session.getCraftingRecipes().values()) { for (Recipe recipe : session.getCraftingRecipes().values()) {
if (recipe.getType() == RecipeType.CRAFTING_SHAPED) { if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {

Datei anzeigen

@ -58,6 +58,7 @@ public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPac
return; return;
} }
// Retrieve the fake villager involved in the trade, and update its metadata to match with the window information
MerchantContainer merchantInventory = (MerchantContainer) openInventory; MerchantContainer merchantInventory = (MerchantContainer) openInventory;
merchantInventory.setVillagerTrades(packet.getTrades()); merchantInventory.setVillagerTrades(packet.getTrades());
Entity villager = merchantInventory.getVillager(); Entity villager = merchantInventory.getVillager();
@ -66,6 +67,7 @@ public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPac
villager.getMetadata().put(EntityData.TRADE_XP, packet.getExperience()); villager.getMetadata().put(EntityData.TRADE_XP, packet.getExperience());
villager.updateBedrockMetadata(session); villager.updateBedrockMetadata(session);
// Construct the packet that opens the trading window
UpdateTradePacket updateTradePacket = new UpdateTradePacket(); UpdateTradePacket updateTradePacket = new UpdateTradePacket();
updateTradePacket.setTradeTier(packet.getVillagerLevel() - 1); updateTradePacket.setTradeTier(packet.getVillagerLevel() - 1);
updateTradePacket.setContainerId((short) packet.getWindowId()); updateTradePacket.setContainerId((short) packet.getWindowId());

Datei anzeigen

@ -73,7 +73,7 @@ public class GeyserWorldManager extends WorldManager {
} }
@Override @Override
public boolean hasMoreBlockDataThanChunkCache() { public boolean hasOwnChunkCache() {
// This implementation can only fetch data from the session chunk cache // This implementation can only fetch data from the session chunk cache
return false; return false;
} }

Datei anzeigen

@ -88,14 +88,14 @@ public abstract class WorldManager {
public abstract void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk section); public abstract void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk section);
/** /**
* Checks whether or not this world manager has access to more block data than the chunk cache. * Checks whether or not this world manager requires a separate chunk cache/has access to more block data than the chunk cache.
* <p> * <p>
* Some world managers (e.g. Spigot) can provide access to block data outside of the chunk cache, and even with chunk caching disabled. This * Some world managers (e.g. Spigot) can provide access to block data outside of the chunk cache, and even with chunk caching disabled. This
* method provides a means to check if this manager has this capability. * method provides a means to check if this manager has this capability.
* *
* @return whether or not this world manager has access to more block data than the chunk cache * @return whether or not this world manager has access to more block data than the chunk cache
*/ */
public abstract boolean hasMoreBlockDataThanChunkCache(); public abstract boolean hasOwnChunkCache();
/** /**
* Gets the Java biome data for the specified chunk. * Gets the Java biome data for the specified chunk.

Datei anzeigen

@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction;
/** /**
* Used for block entities if the Java block state contains Bedrock block information. * Used for block entities if the Java block state contains Bedrock block information.
@ -199,7 +200,13 @@ public class BlockStateValues {
return FLOWER_POT_VALUES; return FLOWER_POT_VALUES;
} }
public static Int2BooleanMap getLecternBookStates() { /**
* This returns a Map interface so IntelliJ doesn't complain about {@link Int2BooleanMap#compute(int, BiFunction)}
* not returning null.
*
* @return the lectern book state map pointing to book present state
*/
public static Map<Integer, Boolean> getLecternBookStates() {
return LECTERN_BOOK_STATES; return LECTERN_BOOK_STATES;
} }

Datei anzeigen

@ -112,8 +112,6 @@ public abstract class BlockTranslator {
*/ */
private final Map<String, NbtMap> javaIdentifierToBedrockTag; private final Map<String, NbtMap> javaIdentifierToBedrockTag;
private static final int BLOCK_STATE_VERSION = 17825808;
/** /**
* Stores the raw blocks JSON until it is no longer needed. * Stores the raw blocks JSON until it is no longer needed.
*/ */
@ -413,6 +411,10 @@ public abstract class BlockTranslator {
return bedrockWaterId; return bedrockWaterId;
} }
/**
* @return the "block state version" generated in the Bedrock block palette that completes an NBT indication of a
* block state.
*/
public abstract int getBlockStateVersion(); public abstract int getBlockStateVersion();
public byte[] getEmptyChunkData() { public byte[] getEmptyChunkData() {

Datei anzeigen

@ -26,6 +26,7 @@
package org.geysermc.connector.network.translators.world.block.entity; package org.geysermc.connector.network.translators.world.block.entity;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.nbt.NbtMapBuilder;
import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.utils.SignUtils; import org.geysermc.connector.utils.SignUtils;
@ -35,7 +36,7 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
/** /**
* Maps a color stored in a sign's Color tag to a Bedrock Edition formatting code. * Maps a color stored in a sign's Color tag to a Bedrock Edition formatting code.
* <br> * <br>
* The color names correspond to dye names, because of this we can't use {@link MessageTranslator#getColor(String)}. * The color names correspond to dye names, because of this we can't use a more global method.
* *
* @param javaColor The dye color stored in the sign's Color tag. * @param javaColor The dye color stored in the sign's Color tag.
* @return A Bedrock Edition formatting code for valid dye colors, otherwise an empty string. * @return A Bedrock Edition formatting code for valid dye colors, otherwise an empty string.
@ -101,27 +102,34 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), ""); String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), "");
signLine = MessageTranslator.convertMessageLenient(signLine); signLine = MessageTranslator.convertMessageLenient(signLine);
// Trim any trailing formatting codes
if (signLine.length() > 2 && signLine.toCharArray()[signLine.length() - 2] == '\u00a7') {
signLine = signLine.substring(0, signLine.length() - 2);
}
// Check the character width on the sign to ensure there is no overflow that is usually hidden // Check the character width on the sign to ensure there is no overflow that is usually hidden
// to Java Edition clients but will appear to Bedrock clients // to Java Edition clients but will appear to Bedrock clients
int signWidth = 0; int signWidth = 0;
StringBuilder finalSignLine = new StringBuilder(); StringBuilder finalSignLine = new StringBuilder();
boolean previousCharacterWasFormatting = false; // Color changes do not count for maximum width
for (char c : signLine.toCharArray()) { for (char c : signLine.toCharArray()) {
signWidth += SignUtils.getCharacterWidth(c); if (c == '\u00a7') {
// Don't count this character
previousCharacterWasFormatting = true;
} else if (previousCharacterWasFormatting) {
// Don't count this character either
previousCharacterWasFormatting = false;
} else {
signWidth += SignUtils.getCharacterWidth(c);
}
if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) { if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) {
finalSignLine.append(c); finalSignLine.append(c);
} else { } else {
// Adding the character would make Bedrock move to the next line - Java doesn't do that, so we do not want to
break; break;
} }
} }
// Java Edition 1.14 added the ability to change the text color of the whole sign using dye // Java Edition 1.14 added the ability to change the text color of the whole sign using dye
if (tag.contains("Color")) { Tag color = tag.get("Color");
signText.append(getBedrockSignColor(tag.get("Color").getValue().toString())); if (color != null) {
signText.append(getBedrockSignColor(color.getValue().toString()));
} }
signText.append(finalSignLine.toString()); signText.append(finalSignLine.toString());

Datei anzeigen

@ -286,7 +286,7 @@ public class SkinManager {
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl(); String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) { if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getDefaultAuthType() != AuthType.ONLINE) {
GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId());
if (session != null) { if (session != null) {

Datei anzeigen

@ -26,17 +26,19 @@
package org.geysermc.connector.utils; package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.data.game.entity.Effect; import com.github.steveice10.mc.protocol.data.game.entity.Effect;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ToolItemEntry; import org.geysermc.connector.network.translators.item.ToolItemEntry;
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import java.util.Optional;
public class BlockUtils { public class BlockUtils {
/**
* A static constant of {@link Position} with all values being zero.
*/
public static final Position POSITION_ZERO = new Position(0, 0, 0);
private static boolean correctTool(String blockToolType, String itemToolType) { private static boolean correctTool(String blockToolType, String itemToolType) {
return (blockToolType.equals("sword") && itemToolType.equals("sword")) || return (blockToolType.equals("sword") && itemToolType.equals("sword")) ||

Datei anzeigen

@ -91,7 +91,7 @@ public class ChunkUtils {
BitSet waterloggedPaletteIds = new BitSet(); BitSet waterloggedPaletteIds = new BitSet();
BitSet pistonOrFlowerPaletteIds = new BitSet(); BitSet pistonOrFlowerPaletteIds = new BitSet();
boolean worldManagerHasMoreBlockDataThanCache = session.getConnector().getWorldManager().hasMoreBlockDataThanChunkCache(); boolean worldManagerHasMoreBlockDataThanCache = session.getConnector().getWorldManager().hasOwnChunkCache();
// If the received packet was a full chunk update, null sections in the chunk are guaranteed to also be null in the world manager // If the received packet was a full chunk update, null sections in the chunk are guaranteed to also be null in the world manager
boolean shouldCheckWorldManagerOnMissingSections = isNonFullChunk && worldManagerHasMoreBlockDataThanCache; boolean shouldCheckWorldManagerOnMissingSections = isNonFullChunk && worldManagerHasMoreBlockDataThanCache;
@ -372,27 +372,30 @@ public class ChunkUtils {
} }
session.sendUpstreamPacket(waterPacket); session.sendUpstreamPacket(waterPacket);
if (BlockStateValues.getLecternBookStates().containsKey(blockState)) { BlockStateValues.getLecternBookStates().compute(blockState, (key, newLecternHasBook) -> {
boolean lecternCachedHasBook = session.getLecternCache().contains(position); // Determine if this block is a lectern
boolean newLecternHasBook = BlockStateValues.getLecternBookStates().get(blockState); if (newLecternHasBook != null) {
if (!session.getConnector().getWorldManager().shouldExpectLecternHandled() && lecternCachedHasBook != newLecternHasBook) { boolean lecternCachedHasBook = session.getLecternCache().contains(position);
// Refresh the block entirely - it either has a book or no longer has a book if (!session.getConnector().getWorldManager().shouldExpectLecternHandled() && lecternCachedHasBook != newLecternHasBook) {
NbtMap newLecternTag; // Refresh the block entirely - it either has a book or no longer has a book
if (newLecternHasBook) { NbtMap newLecternTag;
newLecternTag = session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false); if (newLecternHasBook) {
newLecternTag = session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
} else {
session.getLecternCache().remove(position);
newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build();
}
BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
} else { } else {
session.getLecternCache().remove(position); // As of right now, no tag can be added asynchronously
newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build(); session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
} }
BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
} else { } else {
// As of right now, no tag can be added asynchronously // Lectern has been destroyed, if it existed
session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false); session.getLecternCache().remove(position);
} }
} else { return newLecternHasBook;
// Lectern has been destroyed, if it existed });
session.getLecternCache().remove(position);
}
// Since Java stores bed colors/skull information as part of the namespaced ID and Bedrock stores it as a tag // Since Java stores bed colors/skull information as part of the namespaced ID and Bedrock stores it as a tag
// This is the only place I could find that interacts with the Java block state and block updates // This is the only place I could find that interacts with the Java block state and block updates

Datei anzeigen

@ -0,0 +1,376 @@
/*
* 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.utils;
import com.google.common.collect.ImmutableSet;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import lombok.Getter;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.living.animal.horse.HorseEntity;
import org.geysermc.connector.entity.type.EntityType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.item.ItemEntry;
import java.util.EnumSet;
import java.util.Set;
public class InteractiveTagManager {
/**
* A list of all foods a horse/donkey can eat on Java Edition.
* Used to display interactive tag if needed.
*/
private static final Set<String> DONKEY_AND_HORSE_FOODS = ImmutableSet.of("golden_apple", "enchanted_golden_apple",
"golden_carrot", "sugar", "apple", "wheat", "hay_block");
/**
* A list of all flowers. Used for feeding bees.
*/
private static final Set<String> FLOWERS = ImmutableSet.of("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet",
"red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose",
"sunflower", "lilac", "rose_bush", "peony");
/**
* All entity types that can be leashed on Java Edition
*/
private static final Set<EntityType> LEASHABLE_MOB_TYPES = EnumSet.of(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN,
EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE,
EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM,
EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT,
EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN);
private static final Set<EntityType> SADDLEABLE_WHEN_TAMED_MOB_TYPES = EnumSet.of(EntityType.DONKEY, EntityType.HORSE,
EntityType.ZOMBIE_HORSE, EntityType.MULE);
/**
* A list of all foods a wolf can eat on Java Edition.
* Used to display interactive tag if needed.
*/
private static final Set<String> WOLF_FOODS = ImmutableSet.of("pufferfish", "tropical_fish", "chicken", "cooked_chicken",
"porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton",
"cooked_rabbit");
/**
* Update the suggestion that the client currently has on their screen for this entity (for example, "Feed" or "Ride")
*
* @param session the Bedrock client session
* @param interactEntity the entity that the client is currently facing.
*/
public static void updateTag(GeyserSession session, Entity interactEntity) {
EntityDataMap entityMetadata = interactEntity.getMetadata();
ItemEntry itemEntry = session.getPlayerInventory().getItemInHand().getItemEntry();
String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", "");
// TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen
// TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food
InteractiveTag interactiveTag = InteractiveTag.NONE;
if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) {
// Unleash the entity
interactiveTag = InteractiveTag.REMOVE_LEASH;
} else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) &&
((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !session.isSneaking()) ||
interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) {
// Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed)
interactiveTag = InteractiveTag.SADDLE;
} else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null &&
session.getPlayerInventory().getItemInHand().getNbt().contains("display")) {
// Holding a named name tag
interactiveTag = InteractiveTag.NAME;
} else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) &&
entityMetadata.getLong(EntityData.LEASH_HOLDER_EID, -1L) == -1L) {
// Holding a leash and the mob is leashable for sure
// (Plugins can change this behavior so that's something to look into in the far far future)
interactiveTag = InteractiveTag.LEASH;
} else {
switch (interactEntity.getEntityType()) {
case BEE:
if (FLOWERS.contains(javaIdentifierStripped)) {
interactiveTag = InteractiveTag.FEED;
}
break;
case BOAT:
interactiveTag = InteractiveTag.BOARD_BOAT;
break;
case CAT:
if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
// Tamed and owned by player - can sit/stand
interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
break;
}
break;
case CHICKEN:
if (javaIdentifierStripped.contains("seeds")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case MOOSHROOM:
// Shear the mooshroom
if (javaIdentifierStripped.equals("shears")) {
interactiveTag = InteractiveTag.MOOSHROOM_SHEAR;
break;
}
// Bowls are acceptable here
else if (javaIdentifierStripped.equals("bowl")) {
interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW;
break;
}
// Fall down to COW as this works on mooshrooms
case COW:
if (javaIdentifierStripped.equals("wheat")) {
interactiveTag = InteractiveTag.FEED;
} else if (javaIdentifierStripped.equals("bucket")) {
// Milk the cow
interactiveTag = InteractiveTag.MILK;
}
break;
case CREEPER:
if (javaIdentifierStripped.equals("flint_and_steel")) {
// Today I learned that you can ignite a creeper with flint and steel! Huh.
interactiveTag = InteractiveTag.IGNITE_CREEPER;
}
break;
case DONKEY:
case LLAMA:
case MULE:
if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED)
&& javaIdentifierStripped.equals("chest")) {
// Can attach a chest
interactiveTag = InteractiveTag.ATTACH_CHEST;
break;
}
// Intentional fall-through
case HORSE:
case SKELETON_HORSE:
case TRADER_LLAMA:
case ZOMBIE_HORSE:
boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED);
if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) {
interactiveTag = InteractiveTag.OPEN_CONTAINER;
break;
}
// have another switch statement as, while these share mount attributes they don't share food
switch (interactEntity.getEntityType()) {
case LLAMA:
case TRADER_LLAMA:
if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) {
interactiveTag = InteractiveTag.FEED;
break;
}
case DONKEY:
case HORSE:
// Undead can't eat
if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) {
interactiveTag = InteractiveTag.FEED;
break;
}
}
if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) {
// Can't ride a baby
if (tamed) {
interactiveTag = InteractiveTag.RIDE_HORSE;
} else if (itemEntry.equals(ItemEntry.AIR)) {
// Can't hide an untamed entity without having your hand empty
interactiveTag = InteractiveTag.MOUNT;
}
}
break;
case FOX:
if (javaIdentifierStripped.equals("sweet_berries")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case HOGLIN:
if (javaIdentifierStripped.equals("crimson_fungus")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case MINECART:
interactiveTag = InteractiveTag.RIDE_MINECART;
break;
case MINECART_CHEST:
case MINECART_COMMAND_BLOCK:
case MINECART_HOPPER:
interactiveTag = InteractiveTag.OPEN_CONTAINER;
break;
case OCELOT:
if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case PANDA:
if (javaIdentifierStripped.equals("bamboo")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case PARROT:
if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case PIG:
if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) {
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
interactiveTag = InteractiveTag.MOUNT;
}
break;
case PIGLIN:
if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) {
interactiveTag = InteractiveTag.BARTER;
}
break;
case RABBIT:
if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case SHEEP:
if (javaIdentifierStripped.equals("wheat")) {
interactiveTag = InteractiveTag.FEED;
} else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) {
if (javaIdentifierStripped.equals("shears")) {
// Shear the sheep
interactiveTag = InteractiveTag.SHEAR;
} else if (javaIdentifierStripped.contains("_dye")) {
// Dye the sheep
interactiveTag = InteractiveTag.DYE;
}
}
break;
case STRIDER:
if (javaIdentifierStripped.equals("warped_fungus")) {
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
interactiveTag = InteractiveTag.RIDE_STRIDER;
}
break;
case TURTLE:
if (javaIdentifierStripped.equals("seagrass")) {
interactiveTag = InteractiveTag.FEED;
}
break;
case VILLAGER:
if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0
&& entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby
interactiveTag = InteractiveTag.TRADE;
}
break;
case WANDERING_TRADER:
interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably.
break;
case WOLF:
if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) {
// Bone and untamed - can tame
interactiveTag = InteractiveTag.TAME;
} else if (WOLF_FOODS.contains(javaIdentifierStripped)) {
// Compatible food in hand - feed
// Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this
interactiveTag = InteractiveTag.FEED;
} else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
// Tamed and owned by player - can sit/stand
interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
}
break;
case ZOMBIE_VILLAGER:
// We can't guarantee the existence of the weakness effect so we just always show it.
if (javaIdentifierStripped.equals("golden_apple")) {
interactiveTag = InteractiveTag.CURE;
}
break;
default:
break;
}
}
session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue());
session.getPlayerEntity().updateBedrockMetadata(session);
}
/**
* All interactive tags in enum form. For potential API usage.
*/
public enum InteractiveTag {
NONE(true),
IGNITE_CREEPER("creeper"),
EDIT,
LEAVE_BOAT("exit.boat"),
FEED,
FISH("fishing"),
MILK,
MOOSHROOM_SHEAR("mooshear"),
MOOSHROOM_MILK_STEW("moostew"),
BOARD_BOAT("ride.boat"),
RIDE_MINECART("ride.minecart"),
RIDE_HORSE("ride.horse"),
RIDE_STRIDER("ride.strider"),
SHEAR,
SIT,
STAND,
TALK,
TAME,
DYE,
CURE,
OPEN_CONTAINER("opencontainer"),
CREATE_MAP("createMap"),
TAKE_PICTURE("takepicture"),
SADDLE,
MOUNT,
BOOST,
WRITE,
LEASH,
REMOVE_LEASH("unleash"),
NAME,
ATTACH_CHEST("attachchest"),
TRADE,
POSE_ARMOR_STAND("armorstand.pose"),
EQUIP_ARMOR_STAND("armorstand.equip"),
READ,
WAKE_VILLAGER("wakevillager"),
BARTER;
/**
* The full string that should be passed on to the client.
*/
@Getter
private final String value;
InteractiveTag(boolean isNone) {
this.value = "";
}
InteractiveTag(String value) {
this.value = "action.interact." + value;
}
InteractiveTag() {
this.value = "action.interact." + name().toLowerCase();
}
}
}

Datei anzeigen

@ -46,8 +46,8 @@ public class MessageTranslatorTest {
// RGB downgrade test // RGB downgrade test
messages.put("{\"extra\":[{\"text\":\" \"},{\"color\":\"gold\",\"text\":\"The \"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"#3AA9FF\",\"bold\":true,\"text\":\"CubeCraft\"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"gold\",\"text\":\" Network \"},{\"color\":\"green\",\"text\":\"[1.8/1.9+]\\n \"},{\"color\":\"#f5e342\",\"text\":\"\"},{\"color\":\"#b042f5\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#c142f5\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#d342f5\",\"bold\":true,\"text\":\"W\"},{\"color\":\"#e442f5\",\"bold\":true,\"text\":\":\"},{\"color\":\"#f542f5\",\"bold\":true,\"text\":\" \"},{\"color\":\"#bcf542\",\"bold\":true,\"text\":\"A\"},{\"color\":\"#acee3f\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#9ce73c\",\"bold\":true,\"text\":\"O\"},{\"color\":\"#8ce039\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#7cd936\",\"bold\":true,\"text\":\"G\"},{\"color\":\"#6cd233\",\"bold\":true,\"text\":\" \"},{\"color\":\"#5ccb30\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#4cc42d\",\"bold\":true,\"text\":\"L\"},{\"color\":\"#3cbd2a\",\"bold\":true,\"text\":\"I\"},{\"color\":\"#2cb627\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#1caf24\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#0ca821\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#f5e342\",\"text\":\" \"},{\"color\":\"#6d7c87\",\"text\":\"(kinda sus) \"},{\"color\":\"#f5e342\",\"text\":\"\"}],\"text\":\"\"}", messages.put("{\"extra\":[{\"text\":\" \"},{\"color\":\"gold\",\"text\":\"The \"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"#3AA9FF\",\"bold\":true,\"text\":\"CubeCraft\"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"gold\",\"text\":\" Network \"},{\"color\":\"green\",\"text\":\"[1.8/1.9+]\\n \"},{\"color\":\"#f5e342\",\"text\":\"\"},{\"color\":\"#b042f5\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#c142f5\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#d342f5\",\"bold\":true,\"text\":\"W\"},{\"color\":\"#e442f5\",\"bold\":true,\"text\":\":\"},{\"color\":\"#f542f5\",\"bold\":true,\"text\":\" \"},{\"color\":\"#bcf542\",\"bold\":true,\"text\":\"A\"},{\"color\":\"#acee3f\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#9ce73c\",\"bold\":true,\"text\":\"O\"},{\"color\":\"#8ce039\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#7cd936\",\"bold\":true,\"text\":\"G\"},{\"color\":\"#6cd233\",\"bold\":true,\"text\":\" \"},{\"color\":\"#5ccb30\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#4cc42d\",\"bold\":true,\"text\":\"L\"},{\"color\":\"#3cbd2a\",\"bold\":true,\"text\":\"I\"},{\"color\":\"#2cb627\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#1caf24\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#0ca821\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#f5e342\",\"text\":\" \"},{\"color\":\"#6d7c87\",\"text\":\"(kinda sus) \"},{\"color\":\"#f5e342\",\"text\":\"\"}],\"text\":\"\"}",
" §r§6The §r§c§k||§r§3§lCubeCraft§r§c§k||§r§6 Network §r§a[1.8/1.9+]\n" + " §r§6The §r§d§k||§r§b§lCubeCraft§r§d§k||§r§6 Network §r§a[1.8/1.9+]\n" +
" §r§e✦ §r§d§lN§r§d§lE§r§d§lW§r§d§l:§r§d§l §r§e§lA§r§e§lM§r§a§lO§r§a§lN§r§a§lG§r§a§l §r§a§lS§r§a§lL§r§2§lI§r§2§lM§r§2§lE§r§2§lS§r§e §r§8(kinda sus) §r§e✦"); " §r§e✦ §r§d§lN§r§d§lE§r§d§lW§r§d§l:§r§d§l §r§e§lA§r§e§lM§r§e§lO§r§a§lN§r§a§lG§r§a§l §r§a§lS§r§2§lL§r§2§lI§r§2§lM§r§2§lE§r§2§lS§r§e §r§b(kinda sus) §r§e✦");
// Color code format resetting // Color code format resetting
messages.put("{\"text\":\"\",\"extra\":[{\"text\":\"\",\"extra\":[{\"text\":\"[\",\"color\":\"gray\"},{\"text\":\"H\",\"color\":\"yellow\"},{\"text\":\"]\",\"color\":\"gray\"},{\"text\":\" \",\"color\":\"white\"},{\"text\":\"GUEST\",\"color\":\"#b7b7b7\",\"bold\":true}]},{\"text\":\"\",\"extra\":[{\"text\":\" \",\"bold\":true},{\"text\":\"»\",\"color\":\"blue\"},{\"text\":\" \",\"color\":\"gray\"}]},{\"text\":\"\",\"extra\":[{\"text\":\"rtm516\",\"color\":\"white\"},{\"text\":\": \",\"color\":\"gray\"},{\"text\":\"\",\"color\":\"white\"}]},{\"text\":\"\",\"extra\":[{\"text\":\"This is an amazing bedrock test message\",\"color\":\"white\"}]}]}\n", messages.put("{\"text\":\"\",\"extra\":[{\"text\":\"\",\"extra\":[{\"text\":\"[\",\"color\":\"gray\"},{\"text\":\"H\",\"color\":\"yellow\"},{\"text\":\"]\",\"color\":\"gray\"},{\"text\":\" \",\"color\":\"white\"},{\"text\":\"GUEST\",\"color\":\"#b7b7b7\",\"bold\":true}]},{\"text\":\"\",\"extra\":[{\"text\":\" \",\"bold\":true},{\"text\":\"»\",\"color\":\"blue\"},{\"text\":\" \",\"color\":\"gray\"}]},{\"text\":\"\",\"extra\":[{\"text\":\"rtm516\",\"color\":\"white\"},{\"text\":\": \",\"color\":\"gray\"},{\"text\":\"\",\"color\":\"white\"}]},{\"text\":\"\",\"extra\":[{\"text\":\"This is an amazing bedrock test message\",\"color\":\"white\"}]}]}\n",