diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 5932ecf41ed..b056816b937 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -93,7 +93,7 @@ public final class EntityDefinitions { public static final EntityDefinition EVOKER_FANGS; public static final EntityDefinition EXPERIENCE_BOTTLE; public static final EntityDefinition EXPERIENCE_ORB; - public static final EntityDefinition EYE_OF_ENDER; + public static final EntityDefinition EYE_OF_ENDER; public static final EntityDefinition FALLING_BLOCK; public static final EntityDefinition FIREBALL; public static final EntityDefinition FIREWORK_ROCKET; @@ -250,7 +250,7 @@ public final class EntityDefinitions { .height(0.8f).width(0.5f) .identifier("minecraft:evocation_fang") .build(); - EYE_OF_ENDER = EntityDefinition.inherited(Entity::new, entityBase) + EYE_OF_ENDER = EntityDefinition.inherited(EnderEyeEntity::new, entityBase) .type(EntityType.EYE_OF_ENDER) .heightAndWidth(0.25f) .identifier("minecraft:eye_of_ender_signal") diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 47ae6777a65..f535255030e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -187,7 +187,7 @@ public InteractionResult interact(Hand hand) { @Override public void tick() { // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing - doTick = !doTick; // Run every 100 ms + doTick = !doTick; // Run every other tick if (!doTick || passengers.isEmpty()) { return; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/EnderEyeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/EnderEyeEntity.java new file mode 100644 index 00000000000..cc5a58f2125 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/EnderEyeEntity.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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.geyser.entity.type; + + +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.UUID; + +public class EnderEyeEntity extends Entity { + public EnderEyeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // Correct sizing + dirtyMetadata.put(EntityDataTypes.SCALE, 0.5f); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 266189e6365..3b0a795b62f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -476,3 +476,4 @@ protected AttributeData calculateAttribute(Attribute javaAttribute, GeyserAttrib return type.getAttribute((float) AttributeUtils.calculateValue(javaAttribute)); } } + diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java index 55334010f14..7e6ae3d22ea 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java @@ -25,12 +25,13 @@ package org.geysermc.geyser.entity.type; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.UUID; @@ -39,7 +40,7 @@ */ public class ThrowableItemEntity extends ThrowableEntity { /** - * Number of ticks since the entity was spawned by the Java server + * Number of draw ticks since the entity was spawned by the Java server */ private int age; private boolean invisible; @@ -48,29 +49,38 @@ public ThrowableItemEntity(GeyserSession session, int entityId, long geyserId, U super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); setFlag(EntityFlag.INVISIBLE, true); invisible = false; + age = 0; + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // Correct sizing + dirtyMetadata.put(EntityDataTypes.SCALE, 0.5f); } private void checkVisibility() { + age++; + + // Prevent projectiles from blocking the player's screen + if (session.isTickingFrozen()) { + // This may seem odd, but it matches java edition + Vector3f playerPos = session.getPlayerEntity().getPosition().sub(0, session.getPlayerEntity().getDefinition().offset(), 0); + setInvisible(playerPos.distanceSquared(position.add(0, definition.offset(), 0)) < 12.25); + } else { + setInvisible(age < 2); + } + if (invisible != getFlag(EntityFlag.INVISIBLE)) { - if (!invisible) { - Vector3f playerPos = session.getPlayerEntity().getPosition(); - // Prevent projectiles from blocking the player's screen - if (age >= 4 || position.distanceSquared(playerPos) > 16) { - setFlag(EntityFlag.INVISIBLE, false); - updateBedrockMetadata(); - } - } else { - setFlag(EntityFlag.INVISIBLE, true); - updateBedrockMetadata(); - } + setFlag(EntityFlag.INVISIBLE, invisible); + updateBedrockMetadata(); } - age++; } @Override - public void tick() { + public void drawTick() { checkVisibility(); - super.tick(); + super.drawTick(); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java b/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java index 06bf45b3dea..f61ff355f66 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Tickable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 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 @@ -26,8 +26,21 @@ package org.geysermc.geyser.entity.type; /** - * Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds. + * Implemented onto anything that should have code ran every Minecraft tick. + * By default, the Java server runs at 20 TPS, 50 milliseconds for each tick. */ public interface Tickable { + /** + * This function gets called every tick at all times, even when the server requests that + * the game should be frozen. This should be used for updating things that are always + * client side updated on Java, regardless of if the server is frozen or not. + */ + default void drawTick() { + } + + /** + * This function gets called every game tick as long as the + * game tick loop isn't frozen. + */ void tick(); } 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 607a58e0b88..7bf381a6f02 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 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 @@ -66,6 +66,7 @@ import org.cloudburstmc.protocol.bedrock.data.GamePublishSetting; import org.cloudburstmc.protocol.bedrock.data.GameRuleData; import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType; @@ -85,6 +86,7 @@ import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket; import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket; import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket; @@ -173,6 +175,7 @@ import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.MathUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.mcprotocollib.auth.GameProfile; @@ -599,7 +602,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private boolean advancedTooltips = false; /** - * The thread that will run every 50 milliseconds - one Minecraft tick. + * The thread that will run every game tick. */ private ScheduledFuture tickThread = null; @@ -643,7 +646,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { /** * Stores cookies sent by the Java server. */ - @Setter @Getter + @Setter private Map cookies = new Object2ObjectOpenHashMap<>(); private final GeyserCameraData cameraData; @@ -652,6 +655,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private MinecraftProtocol protocol; + @Getter + private int nanosecondsPerTick = 50000000; + @Getter + private float millisecondsPerTick = 50.0f; + private boolean tickingFrozen = false; + /** + * The amount of ticks requested by the server that the game should proceed with, even if the game tick loop is frozen. + */ + @Setter + private int stepTicks = 0; + public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) { this.geyser = geyser; this.upstream = new UpstreamSession(bedrockServerSession); @@ -874,31 +888,31 @@ public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.Authentic } task.cleanup(); // player is online -> remove pending authentication immediately return task.getAuthentication().handle((result, ex) -> { - if (ex != null) { - geyser.getLogger().error("Failed to log in with Microsoft code!", ex); - disconnect(ex.toString()); - return false; - } + if (ex != null) { + geyser.getLogger().error("Failed to log in with Microsoft code!", ex); + disconnect(ex.toString()); + return false; + } - StepMCProfile.MCProfile mcProfile = result.session().getMcProfile(); - StepMCToken.MCToken mcToken = mcProfile.getMcToken(); + StepMCProfile.MCProfile mcProfile = result.session().getMcProfile(); + StepMCToken.MCToken mcToken = mcProfile.getMcToken(); - this.protocol = new MinecraftProtocol( - new GameProfile(mcProfile.getId(), mcProfile.getName()), - mcToken.getAccessToken() - ); + this.protocol = new MinecraftProtocol( + new GameProfile(mcProfile.getId(), mcProfile.getName()), + mcToken.getAccessToken() + ); - try { - connectDownstream(); - } catch (Throwable t) { - t.printStackTrace(); - return false; - } + try { + connectDownstream(); + } catch (Throwable t) { + t.printStackTrace(); + return false; + } - // Save our auth chain for later use - geyser.saveAuthChain(bedrockUsername(), GSON.toJson(result.step().toJson(result.session()))); - return true; - }).getNow(false); + // Save our auth chain for later use + geyser.saveAuthChain(bedrockUsername(), GSON.toJson(result.step().toJson(result.session()))); + return true; + }).getNow(false); } /** @@ -919,7 +933,7 @@ private void connectDownstream() { boolean floodgate = this.remoteServer.authType() == AuthType.FLOODGATE; // Start ticking - tickThread = eventLoop.scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); + tickThread = eventLoop.scheduleAtFixedRate(this::tick, nanosecondsPerTick, nanosecondsPerTick, TimeUnit.NANOSECONDS); this.protocol.setUseDefaultListeners(false); @@ -1236,8 +1250,21 @@ public ScheduledFuture scheduleInEventLoop(Runnable runnable, long duration, }, duration, timeUnit); } + public void updateTickingState(float tickRate, boolean frozen) { + tickThread.cancel(false); + + this.tickingFrozen = frozen; + + tickRate = MathUtils.clamp(tickRate, 1.0f, 10000.0f); + + millisecondsPerTick = 1000.0f / tickRate; + + nanosecondsPerTick = MathUtils.ceil(1000000000.0f / tickRate); + tickThread = eventLoop.scheduleAtFixedRate(this::tick, nanosecondsPerTick, nanosecondsPerTick, TimeUnit.NANOSECONDS); + } + /** - * Called every 50 milliseconds - one Minecraft tick. + * Called every Minecraft tick. */ protected void tick() { try { @@ -1275,13 +1302,21 @@ protected void tick() { isInWorldBorderWarningArea = false; } + boolean gameShouldUpdate = !tickingFrozen || stepTicks > 0; + if (stepTicks > 0) { + --stepTicks; + } + Entity vehicle = playerEntity.getVehicle(); if (vehicle instanceof ClientVehicle clientVehicle && vehicle.isValid()) { clientVehicle.getVehicleComponent().tickVehicle(); } for (Tickable entity : entityCache.getTickableEntities()) { - entity.tick(); + entity.drawTick(); + if (gameShouldUpdate) { + entity.tick(); + } } if (armAnimationTicks >= 0) { @@ -1813,7 +1848,7 @@ public void setDaylightCycle(boolean doCycle) { * Send a gamerule value to the client * * @param gameRule The gamerule to send - * @param value The value of the gamerule + * @param value The value of the gamerule */ public void sendGameRule(String gameRule, Object value) { GameRulesChangedPacket gameRulesChangedPacket = new GameRulesChangedPacket(); @@ -2014,7 +2049,7 @@ public float getEyeHeight() { @Override public UUID javaUuid() { - return playerEntity != null ? playerEntity.getUuid() : null ; + return playerEntity != null ? playerEntity.getUuid() : null; } @Override diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java index fb5137b05a9..f34ec49d8fd 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java @@ -118,9 +118,13 @@ private void forceSyncCorrectTitleTimes() { SetTitlePacket titlePacket = new SetTitlePacket(); titlePacket.setType(SetTitlePacket.Type.TIMES); titlePacket.setText(""); - titlePacket.setFadeInTime(trueTitleFadeInTime); - titlePacket.setStayTime(trueTitleStayTime); - titlePacket.setFadeOutTime(trueTitleFadeOutTime); + + // We need a tick rate multiplier as otherwise the timings are incorrect on different tick rates because + // bedrock can only run at 20 TPS (50ms = 1 tick) + int tickrateMultiplier = Math.round(session.getMillisecondsPerTick()) / 50; + titlePacket.setFadeInTime(trueTitleFadeInTime * tickrateMultiplier); + titlePacket.setStayTime(trueTitleStayTime * tickrateMultiplier); + titlePacket.setFadeOutTime(trueTitleFadeOutTime * tickrateMultiplier); titlePacket.setPlatformOnlineId(""); titlePacket.setXuid(""); 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 35ad942d041..64681723eaa 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 @@ -66,7 +66,7 @@ public void translate(GeyserSession session, MobEquipmentPacket packet) { // (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.scheduleInEventLoop(() -> session.useItem(Hand.MAIN_HAND), - 50, TimeUnit.MILLISECONDS); + session.getNanosecondsPerTick(), TimeUnit.NANOSECONDS); } if (oldItem.getJavaId() != newItem.getJavaId()) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaTickingStateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaTickingStateTranslator.java new file mode 100644 index 00000000000..85d4974cf8c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaTickingStateTranslator.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 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.geyser.translator.protocol.java; + +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundTickingStatePacket; + +@Translator(packet = ClientboundTickingStatePacket.class) +public class JavaTickingStateTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundTickingStatePacket packet) { + session.updateTickingState(packet.getTickRate(), packet.isFrozen()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaTickingStepTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaTickingStepTranslator.java new file mode 100644 index 00000000000..f898b762adc --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaTickingStepTranslator.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 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.geyser.translator.protocol.java; + +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundTickingStepPacket; + +@Translator(packet = ClientboundTickingStepPacket.class) +public class JavaTickingStepTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundTickingStepPacket packet) { + session.setStepTicks(packet.getTickSteps()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java index 8e07a7d89af..a8a121e03b7 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java @@ -54,7 +54,7 @@ public void translate(GeyserSession session, ClientboundCooldownPacket packet) { if (cooldownCategory != null) { PlayerStartItemCooldownPacket bedrockPacket = new PlayerStartItemCooldownPacket(); bedrockPacket.setItemCategory(cooldownCategory); - bedrockPacket.setCooldownDuration(packet.getCooldownTicks()); + bedrockPacket.setCooldownDuration(Math.round(packet.getCooldownTicks() * (session.getMillisecondsPerTick() / 50))); session.sendUpstreamPacket(bedrockPacket); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/title/JavaSetTitlesAnimationTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/title/JavaSetTitlesAnimationTranslator.java index 4bc5ba0c512..d310690d5b8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/title/JavaSetTitlesAnimationTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/title/JavaSetTitlesAnimationTranslator.java @@ -40,13 +40,15 @@ public void translate(GeyserSession session, ClientboundSetTitlesAnimationPacket int stayTime = packet.getStay(); int fadeOutTime = packet.getFadeOut(); session.getWorldCache().setTitleTimes(fadeInTime, stayTime, fadeOutTime); - + // We need a tick rate multiplier as otherwise the timings are incorrect on different tick rates because + // bedrock can only run at 20 TPS (50ms = 1 tick) + int tickrateMultiplier = Math.round(session.getMillisecondsPerTick()) / 50; SetTitlePacket titlePacket = new SetTitlePacket(); titlePacket.setType(SetTitlePacket.Type.TIMES); titlePacket.setText(""); - titlePacket.setFadeInTime(fadeInTime); - titlePacket.setFadeOutTime(fadeOutTime); - titlePacket.setStayTime(stayTime); + titlePacket.setFadeInTime(fadeInTime * tickrateMultiplier); + titlePacket.setFadeOutTime(fadeOutTime * tickrateMultiplier); + titlePacket.setStayTime(stayTime * tickrateMultiplier); titlePacket.setXuid(""); titlePacket.setPlatformOnlineId(""); session.sendUpstreamPacket(titlePacket); 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 c020e96b2c6..dd42c96bd4c 100644 --- a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.util; import lombok.Getter; +import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PreferencesCache; @@ -50,6 +51,7 @@ public static CooldownType getDefaultShowCooldown() { /** * 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 */ public static void sendCooldown(GeyserSession session) { @@ -57,7 +59,9 @@ public static void sendCooldown(GeyserSession session) { 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 + 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 + } // Set the times to stay a bit with no fade in nor out SetTitlePacket titlePacket = new SetTitlePacket(); titlePacket.setType(SetTitlePacket.Type.TIMES); @@ -83,13 +87,15 @@ public static void sendCooldown(GeyserSession session) { /** * 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 + 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); @@ -102,7 +108,7 @@ private static void computeCooldown(GeyserSession session, CooldownType sessionP session.sendUpstreamPacket(titlePacket); if (hasCooldown(session)) { session.scheduleInEventLoop(() -> - computeCooldown(session, sessionPreference, lastHitTime), 50, TimeUnit.MILLISECONDS); // Updated per tick. 1000 divided by 20 ticks equals 50 + computeCooldown(session, sessionPreference, lastHitTime), (long) restrain(session.getMillisecondsPerTick(), 50), TimeUnit.MILLISECONDS); // Updated per tick. 1000 divided by 20 ticks equals 50 } else { SetTitlePacket removeTitlePacket = new SetTitlePacket(); removeTitlePacket.setType(SetTitlePacket.Type.CLEAR); @@ -115,8 +121,9 @@ private static void computeCooldown(GeyserSession session, CooldownType sessionP private static boolean hasCooldown(GeyserSession session) { long time = System.currentTimeMillis() - session.getLastHitTime(); - double cooldown = restrain(((double) time) * session.getAttackSpeed() / 1000d, 1.5); - return cooldown < 1.1; + 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; } @@ -128,7 +135,8 @@ private static double restrain(double x, double max) { private static String getTitle(GeyserSession session) { long time = System.currentTimeMillis() - session.getLastHitTime(); - double cooldown = restrain(((double) time) * session.getAttackSpeed() / 1000d, 1); + double tickrateMultiplier = Math.max(session.getMillisecondsPerTick() / 50, 1.0); + double cooldown = restrain(((double) time) * session.getAttackSpeed() / (tickrateMultiplier * 1000.0), 1.0); int darkGrey = (int) Math.floor(10d * cooldown); int grey = 10 - darkGrey; @@ -157,7 +165,6 @@ public enum CooldownType { * Convert the CooldownType string (from config) to the enum, DISABLED on fail * * @param name CooldownType string - * * @return The converted CooldownType */ public static CooldownType getByName(String name) {