Skip to content

FS25_SlurryPipeSystem

Choose a tag to compare

@Willbobber Willbobber released this 31 Jul 10:49

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) * dmMult

With 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% at crustMix="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:
    • crustMix on <agitator> (vehicle) and on <fixedAgitator>
    • crustGrow on 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 (scaling settle down 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.
  • poolFillTypes declares 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 same Storage object
    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 from pEntry.crustInstances to per-store sourceEntry.crustInstances.
  • Persistence bug fixed. thicknesses.entry saves were keyed on placeable world position
    alone, so a multi-lagoon placeable's store 2 never saved or restored its solids/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 storeId attribute 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: applyStoreCrustState silently dropped its storeIndex parameter 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, a logStoreMPState("JIP-dump") call in sendFullStateToConnection, and
    an unconditional warning in getStoreSourceByIndex when 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 Storage method wrappers with no teardown on
    unregister — the closures survived the placeable.
  • Nine debug channels shipped as true. All reset to false. Channels are togglable at
    runtime via spsDebug <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#crustMix and fixedAgitator#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
  • crustGrow on the store root