FS25_SlurryPipeSystem
Version 1.0.1.1
FS25_SlurryPipeSystem — detailed changelog
Covers 25–31 July 2026 · V1.0.0.10 → 1.0.1.1
Each entry states what was wrong, why it was wrong, and what changed. Line references are
against the V1.0.1.1 tag unless noted.
1. Spreader blockages — reworked
1.1 Crust had no effect on outlets
SlurryPipeManager.BLOCKAGE_OUTLET_CHANCE shipped as 0.0. The outlet chance was computed as:
outletChance = (BLOCKAGE_BASE_CHANCE + BLOCKAGE_OUTLET_CHANCE * crustScale) * dmMultWith the slope at zero, crustScale cancelled out entirely and every outlet was left with
nothing but the flat BLOCKAGE_BASE_CHANCE foreign-object roll. Carried crust — the whole
point of the agitation system — could not touch the outlets. Only the macerator responded to
it, and only via a 0.005 slope.
Measured against a five-minute tankful (50 rolls at the 6-second interval) on a 12-outlet
bar, the old numbers gave:
| Carried crust | Macerator jams | Outlets blocked (avg) |
|---|---|---|
| 0.00 — fully mixed | 7% | 0.9 of 12 |
| 0.80 — month unstirred | 27% | 1.1 of 12 |
The outlet row is indistinguishable to a player. Agitating before filling changed almost
nothing at the bar.
1.2 The new banded model
Blockages are caused by lumps, i.e. the crust the tanker carries (inherited from the
store's settle at fill time). Dry matter's role is slowing the pump via
getFlowRateMultiplier, not clogging the bar, so it is now only a mild amplifier.
A macerator is a lump smasher with a finite capacity. The two rows therefore now have
separate crust thresholds:
| Band | Carried crust | Behaviour |
|---|---|---|
| Smooth | 0 – 0.25 | Macerator idles. Nothing lump-driven happens on either row. |
| Coping | 0.25 – 0.60 | Lumps present. The macerator takes the punishment; outlets stay clear. Macerator risk climbs. |
| Overwhelmed | 0.60 – 1.00 | Past the macerator's capacity. Lumps reach the bar. Outlet risk climbs hard. |
A bar with no macerator node gets no protection and its outlets are exposed from
BLOCKAGE_OUTLET_CRUST_MIN_RAW (0.25) upward. That is the reason to fit one.
1.3 Blockage map
Chance of a stoppage across one full tank (≈5 minutes spreading = 50 rolls), dry matter at
the gauge floor:
With a macerator fitted
| Carried crust | Macerator per roll | Macerator per load | Outlet per roll | Outlet per load |
|---|---|---|---|---|
| 0.00 | 0.0010 | 5% | 0.0010 | 5% |
| 0.25 | 0.0010 | 5% | 0.0010 | 5% |
| 0.40 | 0.0064 | 27% | 0.0010 | 5% |
| 0.50 | 0.0100 | 39% | 0.0010 | 5% |
| 0.60 | 0.0136 | 50% | 0.0010 | 5% |
| 0.70 | 0.0172 | 58% | 0.0085 | 35% |
| 0.80 | 0.0208 | 65% | 0.0160 | 55% |
| 0.90 | 0.0244 | 71% | 0.0235 | 70% |
| 1.00 | 0.0280 | 76% | 0.0310 | 79% |
No macerator fitted (outlet threshold drops to 0.25)
| Carried crust | Outlet per load |
|---|---|
| 0.00 – 0.25 | 5% |
| 0.50 | 42% |
| 0.75 | 65% |
| 1.00 | 79% |
Thick slurry multiplies both rows by up to ×1.25 at the dry-matter jam point.
1.4 Constants
| Constant | Was | Now | Note |
|---|---|---|---|
BLOCKAGE_BASE_CHANCE |
0.0015 | 0.001 | now per bar, not per outlet |
BLOCKAGE_MACERATOR_CRUST_MIN |
— | 0.25 | new |
BLOCKAGE_MACERATOR_CHANCE |
0.005 | 0.027 | |
BLOCKAGE_OUTLET_CRUST_MIN |
— | 0.60 | new — macerator capacity ceiling |
BLOCKAGE_OUTLET_CRUST_MIN_RAW |
— | 0.25 | new — no macerator fitted |
BLOCKAGE_OUTLET_CHANCE |
0.0 | 0.030 | |
BLOCKAGE_DM_BONUS |
0.5 | 0.25 | |
BLOCKAGE_CRUST_MIN |
0.3 | 0.25 | retained under its original name — SPSSpreaderHUD.lua:146 reads it for the risk band |
BLOCKAGE_ROLL_INTERVAL |
6000 | 6000 | unchanged |
BLOCKAGE_CLEAR_RADIUS |
0.75 | 0.75 | unchanged |
1.5 Three structural fixes
Base chance moved from per-outlet to per-bar. Every outlet previously rolled its own
0.0015. On a 12-outlet bar that is 600 effective rolls per tankful — a 59% chance of a
nuisance foreign-object clog on perfectly clean, freshly mixed slurry, and a 24-outlet bar
was twice as cursed as a 12-outlet one purely for having more nodes. The bar now takes one
roll and a random still-open outlet receives it.
Macerator and outlet rolls are now independent. The outlet loop previously sat in the
else branch of the macerator roll, so outlets were only rolled on ticks where the macerator
did not block — meaning the worse the slurry got, the fewer chances the outlets were given.
Both rows now roll on every tick (the whole block still skips while the macerator is already
blocked, since nothing is flowing then).
Macerator protection now exists. There was previously no concept of the macerator
shielding the outlets at all; the distinction was purely in the slope values.
1.6 Per-vehicle XML tuning
All seven values are now overridable per vehicle as attributes on the <blockageNodes>
container (not on individual nodes — they describe the bar as a whole). Read into
entry.blockageTuning, clamped on read, nil falls back to the global constant, so every
existing third-party config loads unchanged.
<blockageNodes baseChance="0.001" maceratorCrustMin="0.25" maceratorChance="0.027"
outletCrustMin="0.60" outletCrustMinRaw="0.25"
outletChance="0.030" dmBonus="0.25">1.7 Shipped configs updated
Eight configs now carry explicit tuning:
| Config | Outlets | Macerator | Tuning |
|---|---|---|---|
| bomech multi4XL | 9 | 4 nodes | standard |
| bomech multiProfi | 7 | 2 nodes | standard |
| bomech tracPack | 9 | 2 nodes | standard |
| samsonAgro sbh4_36 | 9 | 1 node | standard |
| samsonAgro sd700 | 2 | 1 node | standard |
| zunhammer vibro | 2 | 1 node | standard |
| farmtech supercis800 | 1 splash plate | none | splash-plate |
| FS25_Joskin_Cobra | 1 per config | none | splash-plate |
The two splash-plate machines needed different numbers. Each has a single wide-bore outlet
and no macerator, so under the unprotected-bar default they would be exposed from 0.25 crust
and blocking that one plate stops the machine outright — 42% of loads at half crust. A splash
plate is a wide aperture and should not clog like a fine trailing-shoe orifice. Both are set
to outletCrustMinRaw="0.85", outletChance="0.010", baseChance="0.0005": about 2.5% per
load on clean slurry, 17% at 0.9 crust, 41% only at fully crusted.
Note that multi4XL declares four macerator nodes and multiProfi/tracPack two each. These are
one logical macerator with several animation points — a successful roll blocks them together
and stops the bar. The node count is not four separate risks.
1.8 Compatibility
No entries are added to, removed from or reordered within entry.blockageEntries, so
SPSBlockageEvent index addressing, the JIP join dump (SlurryPipeManager:4394) and
SPSSpreaderHUD's index-to-cell mapping are all unaffected. ManureBarrelOverride is
untouched: outlets still gate their work-area band via workArea._spsBlockageEntry.blocked,
and a blocked macerator still kills discharge.
Known, unchanged: getBlockageFlowFraction (SPSM_Spreader.lua:112) remains dead code —
defined and called from nowhere. Blocked outlets stop their section spreading rather than
throttling flow, which is the better behaviour; the function was left in place.
2. Crust timing — rebuilt on game hours
2.1 The bug
Crust growth and agitation both scaled by environment.timeAdjustment. That value is not
a timescale multiplier — it is a season-length normaliser equal to 1 / daysPerPeriod. Two
consequences:
- Crust processes ran at a fraction of real time on any save with a season length above 1.
- The player's timescale setting was ignored entirely.
A separate bug report also claimed g_slurryPipeManager.agitationEnabled was being read on a
non-existent field. That was verified as already resolved in the v1.0.0.9 baseline — the
check had been replaced with self:isFeatureEnabled(), and the one remaining reference is
deliberate legacy savegame-compatibility code.
2.2 The fix
The clock was replaced wholesale with a shared monotonic game-hour system built on
Environment:getMonotonicHour(), which is timescale-correct and survives sleeping,
fast-forward and save/load without drift. Timing is now absolute:
CRUST_GROW_HOURS = 720.0— game hours of no mixing for 0 → 100% crust (30 in-game days)CRUST_MIX_HOURS = 2.0— game hours of continuous stirring for 100% → 0% atcrustMix="1"
Both mean the same thing on every save regardless of daysPerPeriod. AGITATION_RATE_MULT
was retired into the new system and SETTLE_YEARS_TO_FULL deprecated; both are retained as
no-op fields so third-party scripts reading them do not index a nil.
2.3 Additional changes in the same pass
- Per-agitator hour stamps are now explicitly released on every non-mixing path: agitator
lifted out, PTO disengaged, tractor detached, below the level node, realism disabled.
Without this an agitator that stopped mid-cycle would bank elapsed hours and dump them into
the store the moment it resumed. - New XML multipliers, both defaulting to 1.0 and clamped 0.1–10.0 on read so a typo
cannot make a store uncrustable or a mixer instantaneous:crustMixon<agitator>(vehicle) and on<fixedAgitator>crustGrowon the store placeable's<slurryPipeSystem>root
- Multiple agitators stack additively on the same store — each calls in with its own
elapsed hours and multiplier. - The separator is deliberately not a caller. It lowers crust by physically removing dry
matter (scalingsettledown in proportion to the shrinking solids pool), a different
mechanism from re-suspension that must not be double-counted against the mixing clock. It
continues to run on real seconds; the clock divergence is a noted follow-up design
decision, not a bug. - Multi-lagoon vegetation refresh fixed — the identity test against
pEntry.sourceEntry
only ever matched the primary store, leaving secondary lagoons visually stale after mixing.
3. Multi-store placeables and BGA support
3.1 The blocker
A modder could not add a fill plane to a biogas plant. The gate at SlurryPipeManager.lua
2304–2337 accepted only spec_silo / spec_husbandry / spec_siloExtension. A production
point has no engine-driven fill plane — nothing in vanilla links its Storage levels to a
plane node — so even past the gate, a plane authored via the legacy <fillPlane> block would
sit frozen at its authored height and report a constant surface Y to arm detection.
3.2 The solution
New module SPSM_Stores.lua introducing <slurryStores>, with coordinated edits to
SlurryPipeManager.lua, SPSM_Consistency.lua, SPSCrustVegetation.lua, SPSEvents.lua,
modDesc.xml and fillPoints.xml.
Design decisions:
- Production points must use
<slurryStores>, not<fillPlane>. SPS drives the plane
itself every tick on both server and client:
Y = minY + (level / capacity) * (maxY - minY). A silo already has an engine-driven plane,
so SPS leaves that alone rather than fighting it. - The two blocks are alternatives, not companions. Declaring both was previously
unguarded and left one crust mesh bound to a node that a second owner also moved.
<slurryStores>now wins as the more specific declaration and the singular block is
ignored, announced with an unconditional warning so a config author sees it in a player log
with no debug channels enabled. - Store 1 becomes the primary
sourceEntry, so every existing lookup, save path, coupler
binding and HUD readout works unchanged against it; the remaining stores ride alongside in
pEntry.storeSources. - Child node resolution is scoped to each store's subtree, so duplicating a lagoon only
requires renaming the root node. poolFillTypesdeclares a set of fill types treated as one body of liquid sharing a
plane and crust — a BGA's LIQUIDMANURE and DIGESTATE live in the sameStorageobject
with separate per-type capacities.
3.3 _getStoreTotalFill scoping
The function summed every fill type the storage tracked. On a BGA that meant 102,000 L of
silage was being read as slurry, wrecking the dry-matter fraction. It is now restricted to
declared pool fill types, and excludes straw, silage and water — water piped into a
slurry store converts to the held type on entry and never persists as water, so counting it
would double-count.
3.4 Per-store correctness
- Crust, foliage thickness and agitator support implemented across all stores; plant
instances moved frompEntry.crustInstancesto per-storesourceEntry.crustInstances. - Persistence bug fixed.
thicknesses.entrysaves were keyed on placeable world position
alone, so a multi-lagoon placeable's store 2 never saved or restored itssolids/settle.
Now one entry per store tagged with#storeId, restored by matching position and id;
legacy entries (no#storeId, reading as 0) are treated as primary-store entries for
backward compatibility. - Fixed agitators gained a
storeIdattribute naming which<slurryStore>they stir.
Without it a fixed agitator on a multi-lagoon placeable always mixed store 1 regardless of
which lagoon its shaft stood in. Omitted means the primary store, so existing configs are
unchanged. An id that does not exist is warned about once. - MP:
applyStoreCrustStatesilently dropped itsstoreIndexparameter despite two call
sites passing it — every replicated crust event landed on store 1. The pending crust queue
is now keyed per store rather than per placeable. Throttled plane-driver traces tagged
server/client, alogStoreMPState("JIP-dump")call insendFullStateToConnection, and
an unconditional warning ingetStoreSourceByIndexwhen an incoming store index exceeds
the local store count.
4. Crust foliage tracking
Plants were being buried as a tank filled. The crust mesh itself was fine — it is genuinely
parented to the fill plane and tracks it correctly. The plants are world-parented clones
whose height was applied manually with only the crust rise added, a maximum of 0.16 m,
while the fill plane itself travels 0.763 m. The gap was the missing 0.6 m.
Fix: each plant's local XZ is stored on the attach node and re-projected through it live each
frame. A parent-walk at init determines whether the attach node already inherits the crust
rise, preventing a double-count. A surface-driven refresh was added alongside
updateStorePlanes so foliage repositions on fill-level changes, not only on crust changes.
Expected node hierarchy: slurryPit_fillPlane (engine-driven) → crust_fillPlane (child,
local Y = settle × maxRise) → crustFoliage (static child that plants follow).
5. Effects
5.1 Fill arm stream played with the pump off
getDirectionalPressureFlowScalar returns two values; the second was being discarded at the
call site, so on exempt endpoints (non-pressure-model vehicles) the pipe stream effect played
whenever the arm was connected regardless of pump state.
5.2 Effect attributes were authored but never read
Eight <effects> attributes were present in shipped configs and silently ignored by the
code, which used hardcoded values throughout. All are now read per-config through a shared
readEffectParams helper, with defaults matching the previous hardcoded values so nothing
changes visually until a config author sets one.
| Attribute | Stream default | Smoke default |
|---|---|---|
maxBending |
0.8 | — |
extraDistance |
0.1 m | — |
fadeInTime |
1.0 s | 1.0 s |
fadeOutTime |
1.0 s | 1.0 s |
planeFadeTime |
1.0 s | 1.0 s |
startDelay |
0.0 s | 0.1 s |
stopDelay |
0.0 s | 0.1 s |
alignToWorldY |
— | true |
Negative or absent values fall back to the default. A defaultEffectParams() fallback set
covers build sites with no <effects> block (the legacy single effect resolved by node name).
6. Other fixes
- Fertiliser store adoption loop advanced its cursor past couplers that rejected a fill
type, making those couplers unavailable to every subsequent type in the loop. - Dynamic store gate installed three
Storagemethod wrappers with no teardown on
unregister — the closures survived the placeable. - Nine debug channels shipped as
true. All reset tofalse. Channels are togglable at
runtime viaspsDebug <ChannelName> on.
7. Config manifest fixes
7.1 precisionFarming entries never loaded
The manifest resolves a config as configRoot .. configFolder .. "/fillPoints.xml" where
configRoot is <mod>/configs/. Four entries declared configFolder without the
mods/ prefix that every other mod entry uses:
FS25_precisionFarming/vehicles/wienhoff/ta25ProfiLine(×2 — base and bundle)- the two johnDeere precisionFarming entries
fileExists failed, the entries were never added to vehicleConfigMap,
findVehicleConfigForVehicle returned nil, and nothing registered — no [SPS] lines in the
log and no in-game behaviour. The demountable-barrel nature of the Wienhoff was not a factor;
SPS binds at vehicle load via configFileName, independent of attach state.
7.2 Witcombe fertiliser stores never loaded
liquidFertLarge and liquidFertSmall were authored as <placeable> children inside
<sprayerSourceVehicleConfigs>, whose loader iterates only .vehicle(%d) and therefore
silently skipped them. The two cow barns from the same pack worked because they sat correctly
under <sprayerPlaceableConfigs>, whose loader iterates .placeable(%d). Both fert entries
moved.
8. Documentation
README.md, SPS_README_01_Tanker_and_Spreader.md and SPS_README_03_Placeables.md updated:
- Crust described as an absolute 720-game-hour timescale rather than "over the season"
- Full
<blockageNodes>container attribute table plus a "How a blockage is decided" section agitator#crustMixandfixedAgitator#crustMix/#storeId- New §3A documenting
<slurryStores>in full — previously undocumented - New §6A documenting the fixed PTO agitator — previously undocumented anywhere
<fillPlane>/<slurryStores>mutual-exclusion warning- Effect timing and shaping attribute table
crustGrowon the store root