Description
Too many crossbow reload sounds are played when 2+ entities charge crossbows at the same time. The extra sounds are hard to hear with vanilla reload sounds because they blend together, but much easier to hear with custom sounds. I have attached a resource pack that changes the crossbow start reload sound with the banjo note block sound to make it easier to hear. This bug makes custom crossbow reloading sounds painful to listen to and requires non-vanilla workarounds to fix.
This bug has been in the game for nearly 8 years, added when crossbows were first introduced in snapshot 18w43a on October 24, 2018.
Steps to Reproduce
Enable the attached resource pack.
Summon two pillagers holding crossbows and a villager for them to target.
Make sure both pillagers reload at overlapping by slightly offset times.
Listen for more than two banjo sounds in rapid succession.
Any combination of 2+ crossbow users can trigger this, including players and piglins.
Observed Result
More than two loading-start and/or loading-middle sounds play during overlapping reloads. In the worst case, an entity’s start sound repeats every tick until the other entity reaches 20% charge.
Expected Result
Each reload should play its start and middle sounds once, regardless of other entities reloading.
Technical Cause
Minecraft registers one CrossbowItem object shared by every crossbow stack and entity. That shared object stores state in two booleans:
private boolean startSoundPlayed = false;
private boolean midLoadSoundPlayed = false;CrossbowItem.onUseTick resets these fields whenever the entity currently being processed is below 20% charge, then uses the same fields to decide whether to play that entity’s sounds. For example:
Entity A reaches 20%, plays its start sound, and sets the shared startSoundPlayed field to true.
Entity B is below 20%, so its use tick resets the shared field to false.
A sees false on its next tick and plays the sound again.
B continues resetting the field until it reaches the 20% threshold.
The shared middle-sound field has the same problem at 50%. The end sound is unaffected because charged state belongs to the individual ItemStack.
Minimum Code Fix
Remove both shared boolean fields and their two resets in CrossbowItem.use. Replace the start/middle threshold logic in onUseTick with per-user threshold-crossing checks:
public void onUseTick(
Level level,
LivingEntity entity,
ItemStack itemStack,
int ticksRemaining
) {
if (!level.isClientSide()) {
ChargingSounds sounds = getChargingSounds(itemStack);
int elapsedTicks = itemStack.getUseDuration(entity) - ticksRemaining;
int chargeTicks = getChargeDuration(itemStack, entity);
float tickPercent = (float)elapsedTicks / (float)chargeTicks;
float previousTickPercent =
(float)(elapsedTicks - 1) / (float)chargeTicks;
if (tickPercent >= START_SOUND_PERCENT
&& !(previousTickPercent >= START_SOUND_PERCENT)) {
sounds.start().ifPresent(sound -> level.playSound(
null,
entity.getX(),
entity.getY(),
entity.getZ(),
sound.value(),
SoundSource.PLAYERS,
0.5F,
1.0F
));
}
if (tickPercent >= MID_SOUND_PERCENT
&& !(previousTickPercent >= MID_SOUND_PERCENT)) {
sounds.mid().ifPresent(sound -> level.playSound(
null,
entity.getX(),
entity.getY(),
entity.getZ(),
sound.value(),
SoundSource.PLAYERS,
0.5F,
1.0F
));
}
if (tickPercent >= 1.0F
&& !isCharged(itemStack)
&& tryLoadProjectiles(entity, itemStack)) {
sounds.end().ifPresent(sound -> level.playSound(
null,
entity.getX(),
entity.getY(),
entity.getZ(),
sound.value(),
entity.getSoundSource(),
1.0F,
1.0F / (level.getRandom().nextFloat() * 0.5F + 1.0F)
+ 0.2F
));
}
}
}The negated previous-progress test is intentional. With a zero-tick data-driven charge duration, the first previous value is NaN; treating it as not previously reached preserves the existing first-tick behavior.
This removes per-use state from the registered item object without adding entity data, item components, or synchronization, and it is independent of entity processing order.
Fabric Proof of Concept
I uploaded a Fabric mod for Java 26.2 on GitHub here demonstrating an alternate fix. It injects at the start of CrossbowItem.onUseTick, derives the two vanilla flag values from the current entity's previous use tick, and then lets vanilla handle sound selection, playback, projectile loading, and completion. The upstream fix in the above code is cleaner because it removes the fields while the mod is intentionally narrow to avoid replacing the rest of the charging logic.
Thank you for helping us improve Minecraft! We saved your files: