Skip to content

Fixes For Accelerator, Diverter, and Target Chamber - #289

Merged
igentuman merged 10 commits into
igentuman:1.20from
SkeletonAdventure:fix/accelerator-item-capability
Jul 28, 2026
Merged

Fixes For Accelerator, Diverter, and Target Chamber#289
igentuman merged 10 commits into
igentuman:1.20from
SkeletonAdventure:fix/accelerator-item-capability

Conversation

@SkeletonAdventure

Copy link
Copy Markdown

I built a linear accelerator, ring accelerator, diverter, and target chamber in a few arrangement in my test world, these are all the fixes that were needed to get those actually working.

  • NPE crash when a neighboring accelerator beam port has no live controller
    AcceleratorBeamPortBE/TargetChamberBeamPortBE.extractParticle() called
    targetPort.controller().getCapability(...) without null-checking the
    neighbor's controller (unformed/mid-teardown/unloaded), crashing the
    server. Added the same null guard already used in CreativeParticleSourceBE.

  • Ion source capability regression on accelerator controllers
    PARTICLE_HANDLER_CAPABILITY returned LazyOptional.empty() on both Linear
    and Ring accelerator controllers instead of the real particle handler,
    and Ring's getCapability also hardcoded ITEM_HANDLER to empty and
    dropped to empty() instead of super.getCapability() - together these
    blocked ion sources from loading at all. Also fixed ion sources getting
    stuck after their first depletion tick: the depleted stack must be
    written back to the slot unconditionally, since ParticleSourceItem.use()
    returns empty on the exact tick that exhausts the source.

  • Target chamber fluid output not being produced
    AbstractRecipe.handleOutputs() only ever inserted item outputs for real
    processing; fluid outputs were computed but never inserted into the
    output tank. Also fixed an off-by-one in
    FluidCapabilityHandler.isValidForOutputSlot() that rejected an output
    exactly filling a tank to capacity, and a
    TargetChamberControllerBE.process() NPE when recipe desynced from
    recipeInfo().recipe() (e.g. after a reload).

  • Target chamber fluid tank GUI rendering
    TargetChamberControllerScreen was missing the addWidget() override every
    other screen in the package uses, so fluid tank widgets received events
    but never rendered. Both screens also had the wrong tank index/position
    /size for the two fluid tanks. Also fixed FluidTankRenderer.drawFluid()
    using Math.min(1, capacity) instead of Math.max(1, capacity), which
    clamped the fill level to a sliver for any tank above capacity 1.

  • Accelerator redstone port race preventing resume after reload
    Every accelerator_port independently called
    controller().setRedstoneByPort() every tick with no aggregation, so
    whichever port ticked first permanently overwrote the controller's
    redstone state - starving out ports actually wired to a lever. Switched
    from a push model (ports race to write) to a pull model: the controller
    now reads its own signal plus every connected INPUT-mode port's signal
    once per polling cycle and takes the max.

  • Same redstone port race in particle chamber ports
    TargetChamberPortBE had the identical flaw, and the write was dead code
    besides: ParticleChamberControllerBE.hasRedstoneSignal() reads a
    shadowed enabledByController field that port writes never touched, so
    port-driven redstone never worked for target/collision/decay chambers.
    hasRedstoneSignal() now also checks every connected INPUT-mode port's
    local signal.

  • Ring accelerator bounds resolution corrupted by nearby same-type rings
    resolveRingBounds() disambiguated a ring's outer wall by scanning up to
    64 blocks for any casing block, with no awareness of which physical
    multiblock it belonged to, so a second nearby ring (e.g. a WorldEdit
    copy) could corrupt the resolution and fail validation. Replaced the
    blind scan with an exact positional check once one axis resolves. Also
    fixed errorBlockPos never being set on these failure paths (was
    producing a misleading 0,0,0 in the error message).

  • Beam diverter never receiving particles from connected accelerators
    BeamDiverterControllerBE.getCapability() never exposed
    PARTICLE_HANDLER_CAPABILITY, unlike the Linear/Ring accelerator
    controllers, so particle handoff from an accelerator's beam port to the
    diverter silently never happened. Verified in-game: particle now
    correctly hands off ring -> diverter -> second ring.

  • StackOverflowError resolving foreign-mod fluid names by string
    IFluidStackIngredientCreator#from(String, int)'s fallback for names not
    in NC's own fluid maps treated any ":" as a full modid:path ID and
    re-entered the same default method via IngredientCreatorAccess.fluid(),
    recursing forever for any third-party fluid (e.g. one registered via
    registerParticleSourceFluid). Reused the existing TagUtil#getFluidByName
    path (already used for bare names) for all names instead.

Entity and others added 10 commits July 26, 2026 00:18
…ntroller

AcceleratorBeamPortBE.extractParticle and TargetChamberBeamPortBE.extractParticle
called targetPort.controller().getCapability(...) on a neighboring beam port
without checking whether that neighbor's controller() is null (e.g. its
multiblock is unformed, mid-teardown, or its controller's chunk is unloaded),
crashing the server. Guard with the same null check already used in
CreativeParticleSourceBE for this exact pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PARTICLE_HANDLER_CAPABILITY was returning LazyOptional.empty() on both
the Linear and Ring accelerator controllers instead of the real
particle handler, and Ring's getCapability also hardcoded ITEM_HANDLER
to empty and dropped through to empty() instead of super.getCapability()
- together these blocked ion sources from being loaded at all.

Also fixes ion sources getting stuck after their first depletion tick:
the depleted (now-empty) stack must be written back to the slot
unconditionally, since ParticleSourceItem.use() returns null/empty on
the exact tick that exhausts the source.
AbstractRecipe.handleOutputs() only ever inserted item outputs for
real (non-simulated) processing - fluid outputs were computed but
never actually inserted into the output tank, so recipes silently
produced no fluid despite completing.

FluidCapabilityHandler.isValidForOutputSlot() rejected an output that
would exactly fill a tank to capacity (off-by-one: > instead of >=).

TargetChamberControllerBE.process() could NPE when `recipe` desynced
from recipeInfo().recipe() (e.g. after a reload) - now re-fetches and
null-guards before using it.
TargetChamberControllerScreen was missing the addWidget() override that
every other screen in this package uses to register widgets against both
the click-handling list and the render list - without it, fluid tank
widgets received events but never rendered, making produced fluid
invisible in the GUI (and in tooltips) even though it was present in the
tank. Both screens also had the wrong tank index/position/size for the
two fluid tanks.

FluidTankRenderer.drawFluid() used Math.min(1, capacity) instead of
Math.max(1, capacity), so any tank capacity above 1 rendered the fill
level clamped to a sliver.
Every accelerator_port defaults to INPUT redstone mode, and each one
independently called controller().setRedstoneByPort() every tick,
guarded only by "skip if externalControlled is already true this
tick." setRedstoneByPort() is a hard overwrite with no aggregation,
so whichever port's tickServer() happened to run first in the block
entity tick order permanently won and stamped its own local redstone
reading (often 0, e.g. a coolant-only port with no lever) onto the
controller - starving out any port actually wired to a lever.

Multi-port structures (e.g. a ring with separate coolant and redstone
ports) could get stuck perpetually off regardless of lever state,
since only a manual block replacement happened to reshuffle tick order
in the right port's favor.

Switched from a push model (ports race to write) to a pull model: the
controller now reads its own redstone signal plus every connected
INPUT-mode port's signal once per polling cycle and takes the max, via
AbstractMultiblock.getPorts(). Ports no longer call setRedstoneByPort.

(ThoroidalAcceleratorMultiblock.validate() diagnostic logging used to
find this root cause has been removed now that the fix is confirmed.)
TargetChamberPortBE pushed to controller().setRedstoneByPort() with no
guard at all whenever it had a local signal, same architectural flaw as
the accelerator ports. Worse, the write was dead code: setRedstoneByPort
sets fields on the base MultiblockControllerBE, but
ParticleChamberControllerBE.hasRedstoneSignal() reads its own shadowed
enabledByController field, which nothing but toggleReactor() ever wrote
- so port-driven redstone never actually worked for target/collision/
decay chambers, only a lever directly on the controller block did.

hasRedstoneSignal() (shared by all ParticleChamberControllerBE subtypes)
now also checks every connected INPUT-mode ParticleChamberPortBE's local
signal alongside the controller's own, so a lever on any port works
correctly regardless of how many other ports (coolant, item, fluid) are
also present on the structure.
…rings

resolveRingBounds() disambiguated an off-center ring's true outer wall
by scanning up to 64 blocks past it for any casing-typed block, with
no awareness of which physical multiblock that block belonged to. A
second ring built within that range (e.g. a WorldEdit copy) would get
mistaken for the same ring's far wall, corrupting the disambiguation
and failing validation with "Cannot resolve X/Z bounds".

Replace the blind scan with an exact positional check: once one axis
resolves unambiguously, the far wall's coordinate on the other axis is
fully determined by arithmetic, so we can test that one precise spot
instead of searching a wide radius that a coincidental nearby
structure could satisfy.

Also fixes errorBlockPos never being set on these failure paths, which
caused the misleading "Wrong Casing at: 0, 0, 0" message instead of a
useful coordinate.
BeamDiverterControllerBE.getCapability() never exposed
PARTICLE_HANDLER_CAPABILITY, unlike RingAcceleratorControllerBE and
LinearAcceleratorControllerBE which both wire it to the same
particleHandler field on the shared AbstractAcceleratorControllerBE
parent. As a result, when an accelerator's output beam port raycasts
to the diverter's input port and requests its particle handler
capability, it always got an empty LazyOptional, so the handoff
silently never happened - no particle, no error, no log output, with
every other precondition (formed, powered, ports configured correctly)
satisfied.

Verified in-game: particle now correctly hands off ring -> diverter ->
second ring.
IFluidStackIngredientCreator#from(String, int)'s fallback branch for
names not already in NC's own fluid maps (NCFluids.NC_MATERIALS /
ALL_FLUID_ENTRIES) treated a ":" in the name as a full "modid:path" ID
and called IngredientCreatorAccess.fluid().from(name, amount) to
resolve it - but that just re-enters this same default method with
the identical arguments, since IngredientCreatorAccess.fluid() returns
another instance of this same interface. Any caller passing a
namespaced fluid name not already known to NC (e.g. a modpack
registering a third-party fluid as a particle source via
registerParticleSourceFluid) recursed forever and crashed with a
StackOverflowError.

The sibling !name.contains(":") branch already resolves names
correctly via TagUtil#getFluidByName, which loads the name through
FluidStack's NBT deserialization and so handles both bare names
(defaulting to the minecraft namespace) and full "modid:path" names
via ResourceLocation/ForgeRegistries.FLUIDS. Reuse that same call for
all names, removing the broken ":" special case entirely.
@igentuman

Copy link
Copy Markdown
Owner

Thanks a lot. I will accept the PR. But i disagree on some changes.

  • Accelerator/ParticleChamber Controller must not expose capabilities directly. Otherwise we can connect beam line directly to controllers. This also means ports must not call getCapability to controllers, they must pass handlers
  • Beam diverter never receiving particles from connected accelerators - this statement seems to be false, because i tested before
  • AbstractRecipe.handleOutputs() is a big change, so all multiblocks and processors do output fluids already. Adding this change might cause global issues, need to test and probably apply fix to target chamber specifically

@igentuman
igentuman merged commit 7e6c747 into igentuman:1.20 Jul 28, 2026
1 check passed
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.

2 participants