Fixes For Accelerator, Diverter, and Target Chamber - #289
Merged
igentuman merged 10 commits intoJul 28, 2026
Merged
Conversation
…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.
Owner
|
Thanks a lot. I will accept the PR. But i disagree on some changes.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.