← Player guide

Fabric mod · Minecraft 1.21.1 and 1.21.11

ZollaS's Arsenal

A configurable bow and crossbow system: volley fire, rapid fire, explosive arrows, arrow velocity from 1 to 10000 m/s, immunity to blast knockback, and a one-press arrow clear, all driven from an in-game menu on Right Shift. It is built for two Minecraft versions and keeps shared worlds safe by limiting guests. This is the working map of the project — how a shot travels from keypress to arrow, what each file is for, and the traps already paid for once.

version2.0.1 authorZollaS minecraft1.21.1 · 1.21.11 mod idzollas_arsenal packagenet.zollas.arsenal namespacezollas_arsenal jdk21

How a shot travels

Nothing gameplay-related happens on the client. It reads input and draws the menu; every arrow is spawned server-side.

Client to server packet flow The client sends UpdateSettingsPayload when a menu control changes, and FireRequestPayload on click or while the use key is held. The server stores settings per player, applies a rate clamp, then FireHandler spawns the arrow entities. CLIENT SERVER Right Shift · ZollasScreen toggle or slider changes UpdateSettingsPayload PlayerSettingsManager one settings object per player UUID Use key · client tick once on click, repeats if rapid fire FireRequestPayload Rate clamp · canFireNow a spammy client cannot outrun it FireHandler spawns N arrows across the cone UseItemCallback cancels the vanilla draw

While any toggle is on, vanilla bow charging is cancelled outright — otherwise a normal shot would stack on top of the custom volley. One real arrow is consumed per fire event, not per projectile, so volleys stay affordable. Settings are re-sent on joining any world, and in multiplayer FireHandler applies the guest limits from trap 15 before a single arrow spawns.

File map

Two copies of the same code, one per Minecraft version: ZollasArsenal-1.21.1 for 1.21.1 (18 Java files, about 1,560 lines) and ZollasArsenal-1.21.11 (15 files, about 1,330 lines). The columns give each file's length in each copy; a dash means that version does not have it. A feature added to one copy has to be ported to the other by hand.

src/main/java/net/zollas/arsenal server · common
1.21.11.21.11
ZollasArsenal.javaEntry point. Registers the four packet types and their receivers, the vanilla bow override, and respawn and disconnect handling.193190
ZollasSettings.javaThe nine settings, their bounds, and clamping. Velocity is stored in m/s.6969
mixin/PersistentProjectileEntityMixin.javaPatches vanilla's arrow to detonate and to save its explosive flag. On 1.21.1 it also corrects the impact point — see trap 14.9285
entity/ArrowImpact.javaThe blast, and on 1.21.1 the impact correction, as ordinary code outside the mixin.8271
entity/ExplosiveArrow.javaInterface the mixin puts on every arrow, carrying the explosive flag.1515
entity/ExplosiveArrowEntity.javaLegacy. Still registered so arrows in old 1.21.1 saves load; nothing fires it.168—
entity/ModEntities.javaRegisters that legacy entity type.30—
network/FireRequestPayload.javaClient to server, one per shot. Carries only which hand.2727
network/UpdateSettingsPayload.javaClient to server, on every menu change, on close, and on joining a world.5656
network/GiveWeaponPayload.javaClient to server, behind the Bow and Crossbow buttons. Host and operators only.2424
network/ClearArrowsPayload.javaClient to server, empty. Clears arrows in every dimension. Host and operators only.2727
server/FireHandler.javaSpawns the volley: cone-scatter maths, ammo, the m/s conversion, and the guest limits.167167
server/ArsenalPermissions.javaWho counts as host or operator, and the limits everyone else fires under.4345
server/PlayerSettingsManager.javaSettings per player, the fire-rate clamp, the blast knockback modifier, and the one-time limits notice.121121
src/client/java/net/zollas/arsenal/client client only
1.21.11.21.11
gui/ZollasScreen.javaThe menu: four toggles, four sliders, the Bow and Crossbow buttons, and the Clear Arrows and No Blast Knockback corner buttons.251253
ZollasArsenalClient.javaBoth keybinds, the tick loop, fire pacing, and settings sync on joining a world.168171
ExplosiveArrowRenderer.javaDraws the legacy arrow entity.21—
ArsenalClientState.javaHolds the one settings instance the screen and tick loop share.88
resources and build config supporting
1.21.11.21.11
fabric.mod.jsonMod id, display name, author, entrypoints, and the mixin config. The file most worth checking first when something odd happens.11
assets/zollas_arsenal/lang/en_us.jsonEverything the player reads: menu labels, the menu title, and the Controls category name.2119
zollas_arsenal.mixins.jsonDeclares the arrow mixin to Fabric.1212
data/minecraft/tags/entity_type/arrows.jsonPuts the legacy arrow in vanilla's arrows tag. Folder is entity_type, singular, since 1.21.6—
assets/template-mod/icon.pngLeftover from the Fabric template. Nothing references it.——
build.gradleLoom, split source sets, Java 21 release target. The 1.21.11 copy uses the fabric-loom-remap plugin.6866
gradle.propertiesMinecraft, mapping and library versions, the mod version, and the explicit JDK 21 path.2023
gradle/wrapper/gradle-wrapper.propertiesGradle 8.8 for 1.21.1, which Loom 1.7.4 needs; 9.5.1 for 1.21.11. Do not swap them.99

Traps already hit

Each of these cost a debugging session once. The numbers are permanent, so "number six" always means the same trap, and new ones are only ever added to the end.

01

The mod id and entrypoints in fabric.mod.json

Must be zollas_arsenal, with net.zollas.arsenal.ZollasArsenal as main and net.zollas.arsenal.client.ZollasArsenalClient as client.

If a build ever loads a mod called "template-mod", a leftover template file has overwritten this one again.

02

ClientPlayNetworking comes from Fabric, not Minecraft

The import is net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking. There is no such class under net.minecraft.client.network.

Got wrong twice.

03

Math.round on a double returns a long

Assigning it to the arrowsAmount int field needs an explicit (int) cast.

04

The abstract method is getDefaultItemStack

In 1.21.1 Yarn mappings PersistentProjectileEntity declares getDefaultItemStack(), not asItemStack().

05

A custom entity without a renderer crashes on sight

The EntityRendererRegistry.register call in onInitializeClient is load-bearing. Drop it and the game throws the instant an explosive arrow needs drawing.

Symptom to recognise: everything renders fine until the moment one is fired.

5b

dimensions takes two floats

In this API version FabricEntityTypeBuilder.dimensions(...) takes width and height directly, not an EntityDimensions object.

06

Every spawned entity needs its own ItemStack copy

Passing one shared stack into many entities and then decrementing it empties the stack every one of them holds. Always hand each entity a copyWithCount(1).

This surfaced only much later, as "cannot encode empty ItemStack" on autosave — the longest hunt in the project.

07

Receiver callbacks hand you a PlayerEntity

Not a ServerPlayerEntity. Cast it explicitly before use.

08

Explosions shove arrows that are still in flight

The blast applies velocity to every nearby entity, arrows included. explode() snapshots nearby projectile velocities before the explosion and restores them after, so a chain detonation does not scatter the rest of the volley.

09

setVelocity does not take metres per second

It takes blocks per tick. One block is one metre and there are twenty ticks in a second, so the menu value is divided by TICKS_PER_SECOND in FireHandler before it reaches the arrow. The old hard-coded 3.0f was 60 m/s all along, which is now the slider's default.

The top of the slider is 500 blocks per tick, which carries the arrow past the loaded chunks before the next tick runs — it is culled, nothing lands, and the bow appears dead. FireHandler therefore caps what physics receives at MAX_BLOCKS_PER_TICK, currently 100. The menu still displays whatever the player chose.

10

An arrow's coordinates during a hit callback are stale

They describe where it began the tick, not where it struck. explode() takes its position from the HitResult, and snapTo() moves the entity onto the impact point before super runs — vanilla emits the landing sound at the entity's own coordinates, so the explosion and the sound each needed handling.

Found through the velocity slider: at the top of the range the detonation and then the thud both arrived next to the shooter, a whole flight behind the target. At vanilla speed the same bug was there and invisible.

A vanilla ArrowEntity cannot be corrected from outside, which first pushed the mod onto a bespoke entity — and that broke @e[type=arrow] and tipped arrows, since the selector matches one exact id and nothing can be aliased onto it. The mixin replaced that: the mod fires genuine minecraft:arrow entities again, and the patch supplies both the impact correction and the detonation.

Land the arrow a tenth of a block short of the impact point, not exactly on it. Vanilla reads a landed arrow's facing from the vector between its position and the hit point, then nudges it back along that vector so the shaft protrudes. Snap it exactly onto the hit point and that vector is zero — the arrow loses its orientation and lies flat in the surface.

11

Static maps outlive the world; world time does not

PlayerSettingsManager keys both its maps by player UUID in static fields, so they survive into the next world. getWorld().getTime() does not — it restarts near zero in a freshly created world. A fire tick recorded at 500000 in an established world therefore sits in the future of a new one, and the rate limiter refuses every shot until the new clock catches up.

The symptom was a brand new world where no toggle would fire, that healed after closing and reopening the game — because that is what clears a static map. canFireNow now treats any backwards jump as a fresh start, and per-player state is dropped on disconnect.

12

The client must push its settings on joining a world

The server's copy now resets per world, so ClientPlayConnectionEvents.JOIN has to re-send it. Any setting added later must ride along in that payload or it will silently run at its default.

Do not send directly from the JOIN callback: the channel is not reliably ready yet, canSend returns false, and the sync is dropped with no error. The join sets a flag and the client tick retries until it goes through.

13

Cancel blast push with the attribute, not a mixin

Vanilla's explosion scales its push on a living entity by one minus explosion_knockback_resistance, and explosion damage types sit in the no_knockback tag — so the blast push is the only thing that moves a player. No Blast Knockback adds a temporary +1.0 modifier to that attribute. Both facts were checked against the 1.21.1 jar.

Temporary modifiers never reach the save file, so they cannot outlive the mod — but they also vanish when a dying player's entity is replaced. ServerPlayerEvents.AFTER_RESPAWN reapplies it from the settings, which survive because they are keyed by UUID.

14

The impact correction is 1.21.1 only — on 1.21.11 it makes arrows bounce

Mojang's projectile rework changed landing. In 1.21.11, applyCollision moves the arrow onto the hit point before onBlockHit runs, so the stale position behind trap 10 no longer exists. onBlockHit then zeroes the velocity and never moves the arrow forward again.

Pulling the arrow back 0.1 block on 1.21.11 therefore left it hovering past the 0.06 margin shouldFall() checks: it dropped, landed, was pulled back, and bounced forever. The 1.21.11 build has no correction at all. The 1.21.11 behaviour was read from that version's bytecode. The 1.21.1 build keeps its correction because, there, the tick moves the arrow forward after the hit and cancels the pull-back out — reasoned from that version's landing code, not re-checked in bytecode, and no bounce has been reported on it.

15

In multiplayer, guests get limits and the host keeps everything

ArsenalPermissions.isTrusted is true for the world's host — the player whose game is the server, as when inviting friends through Essential — or anyone with operator level 2. Everyone else cannot use Clear Arrows or the weapon buttons, never fires explosive arrows, and is held to 20 arrows a volley and 5 shots a second. They are told once in chat. No Blast Knockback is unrestricted because it only affects the player using it.

Limits are applied when a shot fires, never written into the stored settings, so a later /op takes effect on the very next shot. The permission API differs per version: hasPermissionLevel(2) and isHost(getGameProfile()) on 1.21.1, getPermissions().hasPermission(new Permission.Level(PermissionLevel.GAMEMASTERS)) and isHost(getPlayerConfigEntry()) on 1.21.11. Every client-to-server message is now behind canSend, so the mod stays quiet on a server that lacks it. Everyone must run the same mod version — the settings packet layout changes between versions.

Build environment

This combination is known to work. The pinned versions are not preferences — each one is load-bearing.

Java
JDK 21

Not 17, not 25. Both 1.21.1 and 1.21.11 require it. The system default here is Java 8, which is why the path is written out explicitly in gradle.properties.

Gradle wrapper · 1.21.1 copy
gradle-8.8-bin.zip

Fabric Loom 1.7.4 cannot run on Gradle 9. A fresh template download defaults to 9.5.1, so the wrapper properties file has to be corrected by hand after regenerating it.

JDK path in gradle.properties
org.gradle.java.home

Written as a Windows short path, because a non-ASCII character in the username breaks properties-file parsing otherwise.

Temp directory
set TEMP to C:\uatmp

The daemon fails to start otherwise, with "invalid argument: connect" deep in Java's pipe setup. Java opens an internal socket file under the temp path, and the accented character in the Windows username breaks it. Same root cause as the gradle.properties short path, different symptom.

1.21.11 port
ZollasArsenal-1.21.11 · Loom 1.17 · Gradle 9.5.1

A separate copy, so the 1.21.1 build keeps working. Yarn 1.21.11+build.6 and Fabric API 0.141.6. Renames to know: getWorld became getEntityWorld, getPos became getEntityPos, NBT saving moved to ReadView/WriteView, keybind categories became registered objects, and text colours need a full ARGB value. The retired custom arrow entity was left out of this copy.

No refmap is correct
mixin names baked in at remap

Loom 1.17 writes Minecraft's runtime method names directly into the compiled mixin, so the 1.21.11 jar has no refmap file. Checked: all four patch points resolve to real 1.21.11 methods.

2.0.0 is a clean break
new mod id, namespace and package

Version 2.0.0 renamed the mod id, the resource namespace, the Java package, the main classes and the project folders. Fabric no longer sees a 2.0.0 jar as the same mod as anything older, so an old jar left in a mods folder loads alongside it instead of being refused — always delete the old one. Keybinds are stored under the new names, so they started over at their defaults, and every player needs 2.0.0 or later.

Source sets
src/main · src/client

Split by Loom. Client-only classes must not be referenced from common code, or the mod breaks on a dedicated server.