Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
208e509
Create world border using particles
Novampr Dec 27, 2025
36c0817
Fix the integrated pack check from my testing
Novampr Dec 27, 2025
35c2d73
Fix the integrated pack check from my testing
Novampr Dec 27, 2025
ffc6af4
Use 2 particles since rotation is broken (This does not work either)
Novampr Dec 29, 2025
a4c35bb
Updated Attack Indicator (WIP)
Novampr Jan 2, 2026
fd52c7d
Start on less row inventories
Novampr Jan 20, 2026
71c48bf
Merge master
Novampr Feb 4, 2026
db630a9
Fix chest rows, make attack cooldowns tick based, add colors to world…
Novampr Feb 5, 2026
3370049
Merge branch 'master' into feat/integrated_pack_v1.1.0
Novampr Feb 5, 2026
1b801ab
Fix my mistakes in merging (or IntelliJ's, haven't found out)
Novampr Feb 5, 2026
7c94ada
Move cooldown related methods into CooldownUtils
Novampr Feb 5, 2026
6ff175a
Clean up imports
Novampr Feb 5, 2026
59ff155
Make it compile again
Novampr Feb 5, 2026
7295ec1
Revert some world border changes
Novampr Feb 5, 2026
76a2da6
Fix attack cooldown
Novampr Feb 6, 2026
63436e1
Revert world border changes
Novampr Feb 6, 2026
e5df031
Missed one
Novampr Feb 6, 2026
101d434
Formatting :skull:
Novampr Feb 6, 2026
bdc3aa6
IntelliJ why
Novampr Feb 6, 2026
cc2c3e6
World Border be gone
Novampr Feb 6, 2026
5c91b46
Update Integrated Pack
Novampr Feb 6, 2026
fdad56e
Rename variable to make a bit more sense
Novampr Feb 6, 2026
617072f
Merge branch 'master' into feat/integrated_pack_v1.1.0
Novampr Feb 8, 2026
1d47b40
Move the title prefix into the Inventory class
Novampr Feb 8, 2026
f033dc6
Revert some formatting on ChestInventoryTranslator
Novampr Feb 8, 2026
47c7348
Fix up javadocs
Novampr Feb 8, 2026
3abccb2
Merge master
Novampr Feb 17, 2026
97059fc
Update GeyserIntegratedPack.mcpack with spear third-person fix
Novampr Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ public abstract class Inventory {
@Getter
protected final ContainerType containerType;

@Getter
protected final String title;

protected final GeyserItemStack[] items;
Expand All @@ -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);
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
* <p>
* <b>This prefix should always consist of color codes only.</b>
* 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 -> "";
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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();
}
Expand All @@ -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
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
143 changes: 76 additions & 67 deletions core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,78 +28,103 @@
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.
* Much of the work here is from the wonderful folks from <a href="https://github.com/ViaVersion/ViaRewind">ViaRewind</a>
*/
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);
Expand All @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/java/org/geysermc/geyser/util/MathUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Binary file modified core/src/main/resources/GeyserIntegratedPack.mcpack
Binary file not shown.