diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java index 414ad29510d..60830a70139 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java @@ -74,7 +74,6 @@ public abstract class Inventory { @Getter protected final ContainerType containerType; - @Getter protected final String title; protected final GeyserItemStack[] items; @@ -101,6 +100,8 @@ public abstract class Inventory { @Setter private boolean displayed; + private final boolean applyIntegratedPackTitlePrefix; + protected Inventory(GeyserSession session, int id, int size, ContainerType containerType) { this(session, "Inventory", id, size, containerType); } @@ -126,6 +127,8 @@ protected Inventory(GeyserSession session, String title, int javaId, int size, C if ((session.getInventoryHolder() != null && session.getInventoryHolder().bedrockId() == bedrockId) || session.isClosingInventory()) { this.bedrockId += 1; } + + this.applyIntegratedPackTitlePrefix = session.integratedPackActive(); } public GeyserItemStack getItem(int slot) { @@ -193,4 +196,34 @@ public void resetNextStateId() { public boolean shouldConfirmContainerClose() { return true; } + + /** + * Gets the title for this inventory + * @return the title to display + */ + public String getTitle() { + return getIntegratedPackTitlePrefix() + this.title; + } + + /** + * A prefix to add to the title if the integrated pack is active. + * Can be good for adding changes within JSON UI. + *

+ * This prefix should always consist of color codes only. + * Color codes prevent the client from cropping the title text for being too long. + * @return a prefix for the title + */ + public String getIntegratedPackTitlePrefix() { + if (!applyIntegratedPackTitlePrefix) return ""; + + return switch (this.containerType) { + case GENERIC_9X1 -> "§z§1§r"; + case GENERIC_9X2 -> "§z§2§r"; + case GENERIC_9X3 -> "§z§3§r"; + case GENERIC_9X4 -> "§z§4§r"; + case GENERIC_9X5 -> "§z§5§r"; + case GENERIC_9X6 -> "§z§6§r"; + default -> ""; + }; + } } diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 8433afe7914..d7f61f08e06 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -337,9 +337,6 @@ public PacketSignal handle(PlayerAuthInputPacket packet) { SetTitlePacket titlePacket = new SetTitlePacket(); titlePacket.setType(SetTitlePacket.Type.ACTIONBAR); titlePacket.setText(GeyserLocale.getPlayerLocaleString("geyser.auth.login.wait", session.locale())); - titlePacket.setFadeInTime(0); - titlePacket.setFadeOutTime(1); - titlePacket.setStayTime(2); titlePacket.setXuid(""); titlePacket.setPlatformOnlineId(""); session.sendUpstreamPacket(titlePacket); diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 6c15b529852..f09b4aebea5 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -192,6 +192,7 @@ import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.ChunkUtils; +import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; @@ -366,6 +367,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private int pendingOrCurrentBedrockInventoryId = -1; + /** + * A check for whether or not we need to clear the attack cooldown we emulate. + */ + @Setter + private boolean needAttackCooldownClear = false; + /** * Use {@link #getNextItemNetId()} instead for consistency */ @@ -1331,6 +1338,8 @@ protected void tick() { this.upstream.getSession().getPeer().sendPacketsImmediately(0, 0, queuedImmediatelyPackets.toArray(new BedrockPacket[0])); queuedImmediatelyPackets.clear(); + + CooldownUtils.tickCooldown(this); } catch (Throwable throwable) { throwable.printStackTrace(); } @@ -1339,6 +1348,24 @@ protected void tick() { worldTicks++; } + public void sendJsonUIData(String key, double value) { + sendJsonUIData(key, String.valueOf(value)); + } + + public void sendJsonUIData(String key, String value) { + String text = "geyseropt:%s:%s".formatted(key, value); + + TextPacket textPacket = new TextPacket(); + textPacket.setPlatformChatId(""); + textPacket.setSourceName(""); + textPacket.setXuid(""); + textPacket.setType(TextPacket.Type.TIP); + textPacket.setNeedsTranslation(false); + textPacket.setMessage(text); + + this.sendUpstreamPacket(textPacket); + } + public void startSneaking(boolean updateMetaData) { // Toggle the shield, if there is no ongoing arm animation // This matches Bedrock Edition behavior as of 1.18.12 @@ -2498,7 +2525,7 @@ public void sendNetworkLatencyStackPacket(long timestamp, boolean ensureEventLoo } sendUpstreamPacket(latencyPacket); } - + public String getDebugInfo() { return "Username: %s, DeviceOs: %s, Version: %s".formatted(bedrockUsername(), platform(), version()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 10f3e9a925c..4a595ee9e7a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -467,7 +467,7 @@ public void translate(GeyserSession session, InventoryTransactionPacket packet) if (session.getPlayerInventory().getItemInHand().getComponent(DataComponentTypes.PIERCING_WEAPON) != null && session.getGameMode() != GameMode.SPECTATOR) { session.sendDownstreamPacket(new ServerboundPlayerActionPacket(PlayerAction.STAB, Vector3i.ZERO, org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction.DOWN, 0)); session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND)); - CooldownUtils.sendCooldown(session); + CooldownUtils.setCooldownHitTime(session); } } } @@ -516,7 +516,7 @@ public void translate(GeyserSession session, InventoryTransactionPacket packet) session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND)); // Since 1.19.10, LevelSoundEventPackets are no longer sent by the client when attacking entities - CooldownUtils.sendCooldown(session); + CooldownUtils.setCooldownHitTime(session); } } break; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java index 137092f7a1f..5cdedbee410 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java @@ -71,7 +71,7 @@ public void translate(GeyserSession session, MobEquipmentPacket packet) { if (!oldItem.isSameItem(newItem)) { // Java sends a cooldown indicator whenever you switch to a new item type - CooldownUtils.sendCooldown(session); + CooldownUtils.setCooldownHitTime(session); } // Update the interactive tag, if an entity is present diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java index 8ac85d2d716..f082c87a2d6 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java @@ -185,7 +185,7 @@ public void translate(GeyserSession session, PlayerAuthInputPacket packet) { } // Java edition sends a cooldown when hitting air. - CooldownUtils.sendCooldown(session); + CooldownUtils.setCooldownHitTime(session); } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java index e99a3136393..15662ec3d2c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java @@ -51,7 +51,7 @@ public void translate(GeyserSession session, LevelSoundEventPacket packet) { if (packet.getSound() == SoundEvent.ATTACK_NODAMAGE || packet.getSound() == SoundEvent.ATTACK || packet.getSound() == SoundEvent.ATTACK_STRONG) { // Send a faux cooldown since Bedrock has no cooldown support // Sent here because Java still sends a cooldown if the player doesn't hit anything but Bedrock always sends a sound - CooldownUtils.sendCooldown(session); + CooldownUtils.setCooldownHitTime(session); } // Used by client to get book from lecterns in survial mode since 1.20.70 diff --git a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java index 33f3c3df78c..3c1807c08c0 100644 --- a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java @@ -28,10 +28,8 @@ import lombok.Getter; import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.cache.PreferencesCache; import org.geysermc.geyser.text.ChatColor; - -import java.util.concurrent.TimeUnit; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; /** * Manages the sending of a cooldown indicator to the Bedrock player as there is no cooldown indicator in Bedrock. @@ -39,67 +37,94 @@ */ public class CooldownUtils { /** - * Starts sending the fake cooldown to the Bedrock client. If the cooldown is not disabled, the sent type is the cooldownPreference in {@link PreferencesCache} - * - * @param session GeyserSession + * Sets the last hit time for use when ticking the attack cooldown */ - public static void sendCooldown(GeyserSession session) { + public static void setCooldownHitTime(GeyserSession session) { + if (session.getGeyser().config().gameplay().showCooldown() == CooldownType.DISABLED) return; + CooldownType sessionPreference = session.getPreferencesCache().getCooldownPreference(); + if (sessionPreference == CooldownType.DISABLED) return; + + if (session.getAttackSpeed() == 0.0 || session.getAttackSpeed() > 20) { + return; // 0.0 usually happens on login and causes issues with visuals; anything above 20 means a plugin like OldCombatMechanics is being used + } + + session.setLastHitTime(System.currentTimeMillis()); + } + + public static void tickCooldown(GeyserSession session) { if (session.getGeyser().config().gameplay().showCooldown() == CooldownType.DISABLED) return; CooldownType sessionPreference = session.getPreferencesCache().getCooldownPreference(); if (sessionPreference == CooldownType.DISABLED) return; + if (session.getGameMode().equals(GameMode.SPECTATOR)) return; // No attack indicator in spectator + if (session.getAttackSpeed() == 0.0 || session.getAttackSpeed() > 20) { + clearCooldown(session); // Let's clear in the off chance there is something already displayed return; // 0.0 usually happens on login and causes issues with visuals; anything above 20 means a plugin like OldCombatMechanics is being used } - // Set the times to stay a bit with no fade in nor out - SetTitlePacket titlePacket = new SetTitlePacket(); - titlePacket.setType(SetTitlePacket.Type.TIMES); - titlePacket.setStayTime(1000); - titlePacket.setText(""); - titlePacket.setXuid(""); - titlePacket.setPlatformOnlineId(""); - session.sendUpstreamPacket(titlePacket); - - session.getWorldCache().markTitleTimesAsIncorrect(); - - // Actionbars don't need an empty title - if (sessionPreference == CooldownType.TITLE) { - // Needs to be sent or no subtitle packet is recognized by the client + + long time = System.currentTimeMillis() - session.getLastHitTime(); + double tickrateMultiplier = Math.max(session.getMillisecondsPerTick() / 50, 1.0); + double cooldown = MathUtils.restrain(((double) time) * session.getAttackSpeed() / (tickrateMultiplier * 1000.0), 1.0); + + if (cooldown < 1.0) { + sendCooldown(session, sessionPreference, cooldown); + } else if (session.isNeedAttackCooldownClear()) { + clearCooldown(session); + } + } + + public static void sendCooldown(GeyserSession session, CooldownType sessionPreference, double cooldown) { + if (session.integratedPackActive()) { + String value = "%s:%d".formatted( + sessionPreference.equals(CooldownType.TITLE) ? + "crs" : + "htb", + Math.round(cooldown * 16) + ); + + session.sendJsonUIData("cooldown", value); + } else { + // Set the times to stay a bit with no fade in nor out + SetTitlePacket titlePacket = new SetTitlePacket(); + titlePacket.setType(SetTitlePacket.Type.TIMES); + titlePacket.setStayTime(1000); + titlePacket.setText(""); + titlePacket.setXuid(""); + titlePacket.setPlatformOnlineId(""); + session.sendUpstreamPacket(titlePacket); + + session.getWorldCache().markTitleTimesAsIncorrect(); + + // Actionbars don't need an empty title + if (sessionPreference == CooldownType.TITLE) { + // Needs to be sent or no subtitle packet is recognized by the client + titlePacket = new SetTitlePacket(); + titlePacket.setType(SetTitlePacket.Type.TITLE); + titlePacket.setText(" "); + titlePacket.setXuid(""); + titlePacket.setPlatformOnlineId(""); + session.sendUpstreamPacket(titlePacket); + } + titlePacket = new SetTitlePacket(); - titlePacket.setType(SetTitlePacket.Type.TITLE); - titlePacket.setText(" "); + if (sessionPreference == CooldownType.ACTIONBAR) { + titlePacket.setType(SetTitlePacket.Type.ACTIONBAR); + } else { + titlePacket.setType(SetTitlePacket.Type.SUBTITLE); + } + titlePacket.setText(CooldownUtils.getTitle(cooldown)); titlePacket.setXuid(""); titlePacket.setPlatformOnlineId(""); session.sendUpstreamPacket(titlePacket); } - session.setLastHitTime(System.currentTimeMillis()); - long lastHitTime = session.getLastHitTime(); // Used later to prevent multiple scheduled cooldown threads - computeCooldown(session, sessionPreference, lastHitTime); + + session.setNeedAttackCooldownClear(true); } - /** - * Keeps updating the cooldown until the bar is complete. - * - * @param session GeyserSession - * @param sessionPreference The type of cooldown the client prefers - * @param lastHitTime The time of the last hit. Used to gauge how long the cooldown is taking. - */ - private static void computeCooldown(GeyserSession session, CooldownType sessionPreference, long lastHitTime) { - if (session.isClosed()) return; // Don't run scheduled tasks if the client left - if (lastHitTime != session.getLastHitTime()) return; // Means another cooldown has started so there's no need to continue this one - SetTitlePacket titlePacket = new SetTitlePacket(); - if (sessionPreference == CooldownType.ACTIONBAR) { - titlePacket.setType(SetTitlePacket.Type.ACTIONBAR); - } else { - titlePacket.setType(SetTitlePacket.Type.SUBTITLE); - } - titlePacket.setText(getTitle(session)); - titlePacket.setXuid(""); - titlePacket.setPlatformOnlineId(""); - session.sendUpstreamPacket(titlePacket); - if (hasCooldown(session)) { - session.scheduleInEventLoop(() -> - computeCooldown(session, sessionPreference, lastHitTime), (long) restrain(session.getMillisecondsPerTick(), 50), TimeUnit.MILLISECONDS); // Updated per tick. 1000 divided by 20 ticks equals 50 + public static void clearCooldown(GeyserSession session) { + if (session.integratedPackActive()) { + session.sendJsonUIData("cooldown", "non"); } else { SetTitlePacket removeTitlePacket = new SetTitlePacket(); removeTitlePacket.setType(SetTitlePacket.Type.CLEAR); @@ -108,27 +133,11 @@ private static void computeCooldown(GeyserSession session, CooldownType sessionP removeTitlePacket.setPlatformOnlineId(""); session.sendUpstreamPacket(removeTitlePacket); } - } - private static boolean hasCooldown(GeyserSession session) { - long time = System.currentTimeMillis() - session.getLastHitTime(); - double tickrateMultiplier = Math.max(session.getMillisecondsPerTick() / 50, 1.0); - double cooldown = restrain(((double) time) * session.getAttackSpeed() / (tickrateMultiplier * 1000.0), 1.0); - return cooldown < 1.0; + session.setNeedAttackCooldownClear(false); } - - private static double restrain(double x, double max) { - if (x < 0d) - return 0d; - return Math.min(x, max); - } - - private static String getTitle(GeyserSession session) { - long time = System.currentTimeMillis() - session.getLastHitTime(); - double tickrateMultiplier = Math.max(session.getMillisecondsPerTick() / 50, 1.0); - double cooldown = restrain(((double) time) * session.getAttackSpeed() / (tickrateMultiplier * 1000.0), 1.0); - + public static String getTitle(double cooldown) { int darkGrey = (int) Math.floor(10d * cooldown); int grey = 10 - darkGrey; StringBuilder builder = new StringBuilder(ChatColor.DARK_GRAY); diff --git a/core/src/main/java/org/geysermc/geyser/util/MathUtils.java b/core/src/main/java/org/geysermc/geyser/util/MathUtils.java index 49a630325c6..9d0cc26358f 100644 --- a/core/src/main/java/org/geysermc/geyser/util/MathUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/MathUtils.java @@ -179,6 +179,12 @@ public static int constrain(int num, int min, int max) { return num; } + public static double restrain(double x, double max) { + if (x < 0d) + return 0d; + return Math.min(x, max); + } + /** * Clamps the value between the low and high boundaries * Copied from {@link org.cloudburstmc.math.GenericMath} with floats instead. diff --git a/core/src/main/resources/GeyserIntegratedPack.mcpack b/core/src/main/resources/GeyserIntegratedPack.mcpack index cf82fe4875b..b3b4e28650f 100644 Binary files a/core/src/main/resources/GeyserIntegratedPack.mcpack and b/core/src/main/resources/GeyserIntegratedPack.mcpack differ