forked from GeyserMC/Geyser
Implement book editing (#1117)
* Implement book editing Updates the PR created by @ForceUpdate1 for 1.16 support. Seems to work fine now that hand support is in MCProtocolLib. Co-authored-by: Camotoy <20743703+DoctorMacc@users.noreply.github.com> * Remove debug line * Simplify code Currently still borked for creative mode. * Fix books on creative * Bug fixes * Fix NPE? * Blind fixes * Send Book update before any player actions * Remove debug prints * Fix out of bounds for page replace and add * Fix editing desync and remove empty pages from the end * Send edit packet after signing * Refactor * Clean up and fix creative * Apply suggestions from code review Co-authored-by: rtm516 <rtm516@users.noreply.github.com> Co-authored-by: ForceUpdate1 <mneuhaus44@gmail.com> Co-authored-by: Camotoy <20743703+DoctorMacc@users.noreply.github.com> Co-authored-by: David Choo <davchoo3@gmail.com> Co-authored-by: rtm516 <rtm516@users.noreply.github.com>
This commit is contained in:
parent
7cefb5713e
commit
fe23c79053
9 changed files with 223 additions and 2 deletions
|
@ -113,6 +113,7 @@ public class GeyserSession implements CommandSender {
|
||||||
private final SessionPlayerEntity playerEntity;
|
private final SessionPlayerEntity playerEntity;
|
||||||
private PlayerInventory inventory;
|
private PlayerInventory inventory;
|
||||||
|
|
||||||
|
private BookEditCache bookEditCache;
|
||||||
private ChunkCache chunkCache;
|
private ChunkCache chunkCache;
|
||||||
private EntityCache entityCache;
|
private EntityCache entityCache;
|
||||||
private EntityEffectCache effectCache;
|
private EntityEffectCache effectCache;
|
||||||
|
@ -342,6 +343,7 @@ public class GeyserSession implements CommandSender {
|
||||||
this.connector = connector;
|
this.connector = connector;
|
||||||
this.upstream = new UpstreamSession(bedrockServerSession);
|
this.upstream = new UpstreamSession(bedrockServerSession);
|
||||||
|
|
||||||
|
this.bookEditCache = new BookEditCache(this);
|
||||||
this.chunkCache = new ChunkCache(this);
|
this.chunkCache = new ChunkCache(this);
|
||||||
this.entityCache = new EntityCache(this);
|
this.entityCache = new EntityCache(this);
|
||||||
this.effectCache = new EntityEffectCache();
|
this.effectCache = new EntityEffectCache();
|
||||||
|
@ -599,6 +601,7 @@ public class GeyserSession implements CommandSender {
|
||||||
tickThread.cancel(true);
|
tickThread.cancel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.bookEditCache = null;
|
||||||
this.chunkCache = null;
|
this.chunkCache = null;
|
||||||
this.entityCache = null;
|
this.entityCache = null;
|
||||||
this.effectCache = null;
|
this.effectCache = null;
|
||||||
|
|
75
connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java
vendored
Normal file
75
connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.session.cache;
|
||||||
|
|
||||||
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
|
||||||
|
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.network.translators.item.ItemRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages updating the current writable book.
|
||||||
|
*
|
||||||
|
* Java sends book updates less frequently than Bedrock, and this can cause issues with servers that rate limit
|
||||||
|
* book packets. Because of this, we need to ensure packets are only send every second or so at maximum.
|
||||||
|
*/
|
||||||
|
public class BookEditCache {
|
||||||
|
private final GeyserSession session;
|
||||||
|
@Setter
|
||||||
|
private ClientEditBookPacket packet;
|
||||||
|
/**
|
||||||
|
* Stores the last time a book update packet was sent to the server.
|
||||||
|
*/
|
||||||
|
private long lastBookUpdate;
|
||||||
|
|
||||||
|
public BookEditCache(GeyserSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if there is a book edit update to send, and if so, send it.
|
||||||
|
*/
|
||||||
|
public void checkForSend() {
|
||||||
|
if (packet == null) {
|
||||||
|
// No new packet has to be sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent kicks due to rate limiting - specifically on Spigot servers
|
||||||
|
if ((System.currentTimeMillis() - lastBookUpdate) < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't send the update if the player isn't not holding a book, shouldn't happen if we catch all interactions
|
||||||
|
ItemStack itemStack = session.getInventory().getItemInHand();
|
||||||
|
if (itemStack == null || itemStack.getId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) {
|
||||||
|
packet = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.getDownstream().getSession().send(packet);
|
||||||
|
packet = null;
|
||||||
|
lastBookUpdate = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* 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.translators.bedrock;
|
||||||
|
|
||||||
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
|
||||||
|
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
|
||||||
|
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||||
|
import com.nukkitx.protocol.bedrock.packet.BookEditPacket;
|
||||||
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
|
import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Translator(packet = BookEditPacket.class)
|
||||||
|
public class BedrockBookEditTranslator extends PacketTranslator<BookEditPacket> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void translate(BookEditPacket packet, GeyserSession session) {
|
||||||
|
ItemStack itemStack = session.getInventory().getItemInHand();
|
||||||
|
if (itemStack != null) {
|
||||||
|
CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag("");
|
||||||
|
ItemStack bookItem = new ItemStack(itemStack.getId(), itemStack.getAmount(), tag);
|
||||||
|
List<Tag> pages = tag.contains("pages") ? new LinkedList<>(((ListTag) tag.get("pages")).getValue()) : new LinkedList<>();
|
||||||
|
|
||||||
|
int page = packet.getPageNumber();
|
||||||
|
// Creative edits the NBT for us
|
||||||
|
if (session.getGameMode() != GameMode.CREATIVE) {
|
||||||
|
switch (packet.getAction()) {
|
||||||
|
case ADD_PAGE: {
|
||||||
|
// Add empty pages in between
|
||||||
|
for (int i = pages.size(); i < page; i++) {
|
||||||
|
pages.add(i, new StringTag("", ""));
|
||||||
|
}
|
||||||
|
pages.add(page, new StringTag("", packet.getText()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Called whenever a page is modified
|
||||||
|
case REPLACE_PAGE: {
|
||||||
|
if (page < pages.size()) {
|
||||||
|
pages.set(page, new StringTag("", packet.getText()));
|
||||||
|
} else {
|
||||||
|
// Add empty pages in between
|
||||||
|
for (int i = pages.size(); i < page; i++) {
|
||||||
|
pages.add(i, new StringTag("", ""));
|
||||||
|
}
|
||||||
|
pages.add(page, new StringTag("", packet.getText()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case DELETE_PAGE: {
|
||||||
|
if (page < pages.size()) {
|
||||||
|
pages.remove(page);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SWAP_PAGES: {
|
||||||
|
int page2 = packet.getSecondaryPageNumber();
|
||||||
|
if (page < pages.size() && page2 < pages.size()) {
|
||||||
|
Collections.swap(pages, page, page2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SIGN_BOOK: {
|
||||||
|
tag.put(new StringTag("author", packet.getAuthor()));
|
||||||
|
tag.put(new StringTag("title", packet.getTitle()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove empty pages at the end
|
||||||
|
while (pages.size() > 0) {
|
||||||
|
StringTag currentPage = (StringTag) pages.get(pages.size() - 1);
|
||||||
|
if (currentPage.getValue() == null || currentPage.getValue().isEmpty()) {
|
||||||
|
pages.remove(pages.size() - 1);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag.put(new ListTag("pages", pages));
|
||||||
|
session.getInventory().setItem(36 + session.getInventory().getHeldItemSlot(), bookItem);
|
||||||
|
InventoryTranslator.INVENTORY_TRANSLATORS.get(null).updateInventory(session, session.getInventory());
|
||||||
|
|
||||||
|
session.getBookEditCache().setPacket(new ClientEditBookPacket(bookItem, packet.getAction() == BookEditPacket.Action.SIGN_BOOK, session.getInventory().getHeldItemSlot()));
|
||||||
|
// There won't be any more book updates after this, so we can try sending the edit packet immediately
|
||||||
|
if (packet.getAction() == BookEditPacket.Action.SIGN_BOOK) {
|
||||||
|
session.getBookEditCache().checkForSend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,9 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void translate(InventoryTransactionPacket packet, GeyserSession session) {
|
public void translate(InventoryTransactionPacket packet, GeyserSession session) {
|
||||||
|
// Send book updates before opening inventories
|
||||||
|
session.getBookEditCache().checkForSend();
|
||||||
|
|
||||||
switch (packet.getTransactionType()) {
|
switch (packet.getTransactionType()) {
|
||||||
case NORMAL:
|
case NORMAL:
|
||||||
Inventory inventory = session.getInventoryCache().getOpenInventory();
|
Inventory inventory = session.getInventoryCache().getOpenInventory();
|
||||||
|
|
|
@ -45,6 +45,9 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send book update before switching hotbar slot
|
||||||
|
session.getBookEditCache().checkForSend();
|
||||||
|
|
||||||
session.getInventory().setHeldItemSlot(packet.getHotbarSlot());
|
session.getInventory().setHeldItemSlot(packet.getHotbarSlot());
|
||||||
|
|
||||||
ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot());
|
ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot());
|
||||||
|
|
|
@ -57,6 +57,11 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Send book update before any player action
|
||||||
|
if (packet.getAction() != PlayerActionPacket.Action.RESPAWN) {
|
||||||
|
session.getBookEditCache().checkForSend();
|
||||||
|
}
|
||||||
|
|
||||||
Vector3i vector = packet.getBlockPosition();
|
Vector3i vector = packet.getBlockPosition();
|
||||||
Position position = new Position(vector.getX(), vector.getY(), vector.getZ());
|
Position position = new Position(vector.getX(), vector.getY(), vector.getZ());
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
||||||
|
|
||||||
session.setLastMovementTimestamp(System.currentTimeMillis());
|
session.setLastMovementTimestamp(System.currentTimeMillis());
|
||||||
|
|
||||||
|
// Send book update before the player moves
|
||||||
|
session.getBookEditCache().checkForSend();
|
||||||
|
|
||||||
if (session.confirmTeleport(packet.getPosition().toDouble().sub(0, EntityType.PLAYER.getOffset(), 0))) {
|
if (session.confirmTeleport(packet.getPosition().toDouble().sub(0, EntityType.PLAYER.getOffset(), 0))) {
|
||||||
// head yaw, pitch, head yaw
|
// head yaw, pitch, head yaw
|
||||||
Vector3f rotation = Vector3f.from(packet.getRotation().getY(), packet.getRotation().getX(), packet.getRotation().getY());
|
Vector3f rotation = Vector3f.from(packet.getRotation().getY(), packet.getRotation().getX(), packet.getRotation().getY());
|
||||||
|
|
|
@ -95,6 +95,10 @@ public class ItemRegistry {
|
||||||
* Wheat item entry, used in AbstractHorseEntity.java
|
* Wheat item entry, used in AbstractHorseEntity.java
|
||||||
*/
|
*/
|
||||||
public static ItemEntry WHEAT;
|
public static ItemEntry WHEAT;
|
||||||
|
/**
|
||||||
|
* Writable book item entry, used in BedrockBookEditTranslator.java
|
||||||
|
*/
|
||||||
|
public static ItemEntry WRITABLE_BOOK;
|
||||||
|
|
||||||
public static int BARRIER_INDEX = 0;
|
public static int BARRIER_INDEX = 0;
|
||||||
|
|
||||||
|
@ -190,6 +194,9 @@ public class ItemRegistry {
|
||||||
case "minecraft:wheat":
|
case "minecraft:wheat":
|
||||||
WHEAT = ITEM_ENTRIES.get(itemIndex);
|
WHEAT = ITEM_ENTRIES.get(itemIndex);
|
||||||
break;
|
break;
|
||||||
|
case "minecraft:writable_book":
|
||||||
|
WRITABLE_BOOK = ITEM_ENTRIES.get(itemIndex);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,9 +78,8 @@ public class BookPagesTranslator extends NbtItemStackTranslator {
|
||||||
CompoundTag pageTag = (CompoundTag) tag;
|
CompoundTag pageTag = (CompoundTag) tag;
|
||||||
|
|
||||||
StringTag textTag = pageTag.get("text");
|
StringTag textTag = pageTag.get("text");
|
||||||
pages.add(new StringTag(MessageTranslator.convertToJavaMessage(textTag.getValue())));
|
pages.add(new StringTag("", textTag.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
itemTag.remove("pages");
|
itemTag.remove("pages");
|
||||||
itemTag.put(new ListTag("pages", pages));
|
itemTag.put(new ListTag("pages", pages));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue