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 PlayerInventory inventory;
 | 
			
		||||
 | 
			
		||||
    private BookEditCache bookEditCache;
 | 
			
		||||
    private ChunkCache chunkCache;
 | 
			
		||||
    private EntityCache entityCache;
 | 
			
		||||
    private EntityEffectCache effectCache;
 | 
			
		||||
| 
						 | 
				
			
			@ -342,6 +343,7 @@ public class GeyserSession implements CommandSender {
 | 
			
		|||
        this.connector = connector;
 | 
			
		||||
        this.upstream = new UpstreamSession(bedrockServerSession);
 | 
			
		||||
 | 
			
		||||
        this.bookEditCache = new BookEditCache(this);
 | 
			
		||||
        this.chunkCache = new ChunkCache(this);
 | 
			
		||||
        this.entityCache = new EntityCache(this);
 | 
			
		||||
        this.effectCache = new EntityEffectCache();
 | 
			
		||||
| 
						 | 
				
			
			@ -599,6 +601,7 @@ public class GeyserSession implements CommandSender {
 | 
			
		|||
            tickThread.cancel(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.bookEditCache = null;
 | 
			
		||||
        this.chunkCache = null;
 | 
			
		||||
        this.entityCache = 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
 | 
			
		||||
    public void translate(InventoryTransactionPacket packet, GeyserSession session) {
 | 
			
		||||
        // Send book updates before opening inventories
 | 
			
		||||
        session.getBookEditCache().checkForSend();
 | 
			
		||||
 | 
			
		||||
        switch (packet.getTransactionType()) {
 | 
			
		||||
            case NORMAL:
 | 
			
		||||
                Inventory inventory = session.getInventoryCache().getOpenInventory();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,9 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
 | 
			
		|||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Send book update before switching hotbar slot
 | 
			
		||||
        session.getBookEditCache().checkForSend();
 | 
			
		||||
 | 
			
		||||
        session.getInventory().setHeldItemSlot(packet.getHotbarSlot());
 | 
			
		||||
 | 
			
		||||
        ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,6 +57,11 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
 | 
			
		|||
        if (entity == null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // Send book update before any player action
 | 
			
		||||
        if (packet.getAction() != PlayerActionPacket.Action.RESPAWN) {
 | 
			
		||||
            session.getBookEditCache().checkForSend();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Vector3i vector = packet.getBlockPosition();
 | 
			
		||||
        Position position = new Position(vector.getX(), vector.getY(), vector.getZ());
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
 | 
			
		|||
 | 
			
		||||
        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))) {
 | 
			
		||||
            // head yaw, pitch, head yaw
 | 
			
		||||
            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
 | 
			
		||||
     */
 | 
			
		||||
    public static ItemEntry WHEAT;
 | 
			
		||||
    /**
 | 
			
		||||
     * Writable book item entry, used in BedrockBookEditTranslator.java
 | 
			
		||||
     */
 | 
			
		||||
    public static ItemEntry WRITABLE_BOOK;
 | 
			
		||||
 | 
			
		||||
    public static int BARRIER_INDEX = 0;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +194,9 @@ public class ItemRegistry {
 | 
			
		|||
                case "minecraft:wheat":
 | 
			
		||||
                    WHEAT = ITEM_ENTRIES.get(itemIndex);
 | 
			
		||||
                    break;
 | 
			
		||||
                case "minecraft:writable_book":
 | 
			
		||||
                    WRITABLE_BOOK = ITEM_ENTRIES.get(itemIndex);
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,9 +78,8 @@ public class BookPagesTranslator extends NbtItemStackTranslator {
 | 
			
		|||
            CompoundTag pageTag = (CompoundTag) tag;
 | 
			
		||||
 | 
			
		||||
            StringTag textTag = pageTag.get("text");
 | 
			
		||||
            pages.add(new StringTag(MessageTranslator.convertToJavaMessage(textTag.getValue())));
 | 
			
		||||
            pages.add(new StringTag("", textTag.getValue()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        itemTag.remove("pages");
 | 
			
		||||
        itemTag.put(new ListTag("pages", pages));
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue