Skip to content

Register the damage-indicated fluid container handler at the lowest priority - #246

Merged
rubensworks merged 2 commits into
master-26-ltsfrom
claude/damage-indicated-fluid-container-capability-priority
Sep 21, 2026
Merged

rubensworks merged 2 commits into
master-26-ltsfrom
claude/damage-indicated-fluid-container-capability-priority

Conversation

@rubensworks

Copy link
Copy Markdown
Member

Replaces CyclopsMC/EvilCraft#1269, #1270 and #1271, which patched this at the EvilCraft end. Fixing it here is cleaner and covers every subclass in every dependent mod at once.

The bug

DamageIndicatedItemFluidContainer registers a generic FluidHandlerItemCapacity for every subclass, from its constructor, on CyclopsCore's mod bus. When the subclass' own mod registers a more specific handler for the same item, that item has two competing providers.

ItemCapability#getCapability returns the first registered non-null provider. RegisterCapabilitiesEvent goes through ModLoader.postEventWrapContainerInModOrder, and postEventWithWrapInModOrder loops over EventPriority.values() on the outside and ModList.forEachModInOrder on the inside. So at equal priority the winner is decided purely by mod load order.

That order is not stable. A dependency declared with ordering="NONE" creates no edge in ModSorter's graph, so the two mods are ordered by TopologicalSort's tie-break, which is a PriorityQueue keyed on each mod's index in the discovery order. Add or remove unrelated mods and the order can flip.

How it showed up

EvilCraft's creative blood drop registers an infinite handler from ItemCreativeBloodDropConfig, and EvilCraft declares ordering="NONE" for cyclopscore. When cyclopscore won the race, a fresh creative blood drop resolved to the generic finite handler, which on a fresh stack contains nothing — the item silently became useless.

In CI it surfaced as testBloodInfuserEmptyFluidContainerCreativeBloodDrop failing in the cyclops-all-deps pack test only, and only on 26.1.2:

Expected Blood infuser tank contents to be 10000: was 0 on tick 202

EmptyFluidContainerInTankTickAction#canTick saw an empty container and returned false on every tick, so the drain never ran once. Hence was 0 rather than a partial amount. It was never a timeout: the drain moves 100 mB/tick and needs 100 of the 200 available ticks.

The change

The handler registered here is a fallback, so it registers at EventPriority.LOWEST. Any subclass registration, at any higher priority (including the default NORMAL), is therefore always considered first, whatever the mod load order. Subclasses that register nothing of their own are unaffected — the fallback is still the only provider.

Verification

Built CyclopsCore with and without this change, published each to mavenLocal, and ran EvilCraft master-26-lts unmodified against both. To pin the mod ordering deterministically (rather than relying on whichever order this machine happens to produce), EvilCraft's own registration was temporarily moved to EventPriority.LOW — reproducing exactly the "cyclopscore is considered first" condition:

CyclopsCore EvilCraft game tests
stock (NORMAL) Expected Blood infuser tank contents to be 10000: was 0 on tick 202 — the CI failure, byte for byte
this branch (LOWEST) 95/95 passed

The one-line priority change is the only variable between those two runs.

CyclopsCore's own ./gradlew build, test and runGameTestServer all pass, spotlessApply applied.

Note on a regression test

I did not add one here: CyclopsCore has no DamageIndicatedItemFluidContainer subclass of its own and no game-test content items, so covering this would mean inventing a test item plus a competing registration purely for the test — more machinery than the fix. The EvilCraft PRs carried a GameTestsItemCreativeBloodDrop that asserts the creative blood drop's fluid handler directly and fails in one legible line; happy to re-propose that against EvilCraft separately if you want the guard.

Downstream

EvilCraft needs no change once this is released, beyond the usual cyclopscore_version bump. The packtests Build (server, cyclops-all-deps, 26.1.2, neoforge) job goes green once it picks up a CyclopsCore build carrying this.


Generated by Claude Code

…riority

DamageIndicatedItemFluidContainer registers a generic FluidHandlerItemCapacity for
every subclass. A subclass whose own mod registers a more specific handler for the
same item ends up with two competing providers, and NeoForge returns the first
registered non-null one. RegisterCapabilitiesEvent is dispatched per event priority
first and per mod second, so at equal priority the winner is decided by the mod load
order between cyclopscore and the subclass' mod. That order is a topological
tie-break over mod file discovery order whenever the dependency declares
ordering="NONE", so it shifts with the rest of the mod set.

EvilCraft's creative blood drop hits this: when cyclopscore registers first, a fresh
stack resolves to the generic finite handler and reports an empty container, which
silently turns the item into a useless one. It showed up as a blood infuser game test
failing in the bigger pack test mod sets only.

This handler is a fallback, so it now registers at EventPriority.LOWEST and any
subclass registration wins regardless of mod load order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZV1n6KyUT6snFM4rf4hVs
@coveralls

coveralls commented Sep 21, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 31.187%. remained the same — claude/damage-indicated-fluid-container-capability-priority into master-26-lts

@rubensworks
rubensworks merged commit 0bed356 into master-26-lts Sep 21, 2026
2 checks passed
@rubensworks
rubensworks deleted the claude/damage-indicated-fluid-container-capability-priority branch September 21, 2026 14:37
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants