Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Husbandry
Big herds without the frame cost. Idle tames think less often, the breeding system stops asking the same question sixty times, and animals in zones nobody has loaded heal and slowly get on with it.
CHANGELOG
Changelog
1.1.0 — forty wolves and one doorway
A requested feature, and it belongs here rather than in a mod of its own: it is the same complaint the rest of Husbandry exists for, seen from the other end. One wolf is a companion. Forty wolves are a moving, shoving wall that puts you in the water, through a doorway you were not aiming at, and off the edge of your own roof.
Your own tames no longer push you. Off by default, including on birth, on taming and on streaming in from an unloaded zone.
Hold E on any tame to make that one solid again, and hold E again to undo it. The choice lives on the animal's ZDO, so it survives a relog and every player on the server sees the same wolf the same way.
Nothing here touches a hostile creature
Being able to walk through a wolf that wants to eat you is not a convenience, it is a
cheat, and it would quietly remove one of the few things that makes a pack dangerous.
Bumping.Phased refuses on tamed before it looks at anything else, and the sweep does
not even enter an untamed creature in its table — which is what makes a creature that
loses its tame status go solid again without anything having to notice it changed.
Why hold-E, when ALT+E was the obvious key
ALT+E is taken. From the shipped assembly, Tameable.Interact(user, hold, alt) opens:
if (!m_nview.IsValid()) return false;
if (hold) return false; // nothing at all
if (alt) { SetName(); return true; } // the rename dialog
So ALT+E renames your wolf — a feature people use constantly and a miserable thing to take away. Hold returns false outright and does nothing in vanilla, which makes it the one genuinely free press on a tame. Skipping the original in a prefix is unusually safe here for the same reason: the very next thing vanilla does with a held press is return false, so there is nothing downstream to lose.
The default is the whole feature
The ZDO flag records solid, not phased. So an animal nobody has ever touched — just
born, just tamed, just streamed in — reads false and is already out of your way. A flag
that had to be set to get the good behaviour would mean every newborn in the world
arrived solid until somebody found it and pressed a key.
Why it is swept rather than set once
Physics.IgnoreCollision — the same call Player.AttachStart uses to stop you colliding
with a chair you sat in — is a runtime fact about a pair of colliders. It is not on the
prefab, does not survive the object being rebuilt, and knows nothing about anybody who was
not in the world when it was called. A wolf born, a player joining, a wolf tamed, a key
pressed: each makes a pair stale.
Four hooks that each have to be right forever, or one sweep that is self-correcting. It runs twice a second and only speaks when a pair has actually changed — without that table the counters would be meaningless, since "phased 40,000 pairs" tells nobody anything where "phased 40" is the herd.
And it is set in both directions. Turning the feature off in the config puts every pair back rather than merely stopping, because a wolf left intangible after the setting was switched off is a setting that looks broken rather than off.
Two things the tooling caught
Character.m_colliderisfamily, not public. The first stub declared it public, which compiles offline and fails on Windows.GetCollider()is the public way in and is what the code uses. That check has now paid for itself in three mods.- The API linter checked 53 references and none of them were the new ones. It matches
receivers by variable name and had never heard of
beast,personorone. The count is what gave it away — it did not move when sixCharactermembers were added. Third release running that this has happened, in two different projects, and printing the count has caught it every time.
Also
Character.IsPlayer()in the stubs is nowvirtualreturning false withPlayeroverriding it to true, matching the assembly. The sibling project had a settable bool onCharacterand no override, so everyPlayerin its harness read as not a player — worth fixing here before anything relied on it.husbandryreports the sweep, and saysNOT SWEPTrather than four zeroes when nothing has looked, because "swept and found nothing" and "never ran" are indistinguishable from inside the game and only one of them is a working mod.- 145 assertions, up from 117. 66 API references checked clean.
1.0.0 — first release
Two reports, from opposite directions: a large pack of tames is expensive, and it is expensive while you are standing near them. The second half is what made it findable.
Idle tames think less often
MonoUpdatersExtra.UpdateAI walks BaseAI.Instances every FixedUpdate and ticks every AI in
it. No stagger, no level of detail, no distance cull. At 50Hz, fifty tames is two and a half
thousand AI ticks a second to stand still.
A tame that is tamed, unalerted, has nothing targeted, is following nobody and is past
NearRange now thinks one tick in four, or one in ten further out. Nothing hostile, nothing in
a fight and nothing at your heel is ever touched.
It does not slow animals down. UpdateAI decides; Character moves — so a throttled animal
walks, falls and stands on the ground at full rate and only changes its mind less often. And
the time a skipped tick would have had is banked and handed to the next one, so every
timer -= dt inside the AI lands where it would have.
One prefix covers every AI in the game, because both subclasses open with
if (!base.UpdateAI(dt)) return false; — so BaseAI.UpdateAI returning false already means
"this tick is not yours" throughout. The signal is the game's own.
Breeding stops asking the same question sixty times
SpawnSystem.GetNrOfInstances walks every AI in the world comparing gameObject.name to a
string, with a GetComponent per match. Procreation.Procreate calls it up to four times, on
an InvokeRepeating, per animal. That is O(herd²) — the only cost in a tame that grows
with the size of the herd rather than the number of animals, and the reason a big pen hurts so
much more than twice a small one.
One shared census replaces the n private ones, bucketed by prefab hash. Most queries touch nothing at all: if the world holds fewer of a species than the cap, no radius can hold enough, and one dictionary lookup settles it.
At the default SoftCap of 0 the effective cap is the prefab's own m_maxCreatures, so
breeding behaves exactly as before — a full pen just stops paying to rediscover that it is
full. Nothing is ever culled; the cap stops births, never causes a death.
A pregnant animal is never skipped, because Procreate is also what gives birth.
Offline healing and slow breeding
Tames in zones nobody has loaded heal slowly, and pairs court slowly.
The breeding does not spawn anything. It sets s_pregnant — which is all MakePregnant does,
one line — and the game itself gives birth, with the right level, the right tamed flag and its
own cap check, on the first frame somebody loads the zone. Conception is gated by the cap here;
birth is gated again by vanilla's. Twelve pairs left overnight cannot become twelve
simultaneous births.
Worth knowing and not something this mod adds: unloaded tames already get hungry in vanilla, because the feeding timer is a world-time stamp rather than a loaded timer.
Runs only where the whole ZDO table lives — dedicated server, host, or single-player — and only
on animals no connected machine is simulating. That test is ownership rather than a radius:
ZDO.GetOwner() is exact and already stored, where the active area is a Chebyshev box around a
zone centre times a multiplier that moves with the server's settings.
Two traps the API linter caught
SpawnSystem.GetNrOfInstances has two overloads, so naming it in a HarmonyPatch attribute
would have thrown AmbiguousMatchException out of PatchAll and taken the whole plugin down
at startup. The cap went on Procreation.Procreate instead.
Procreation.IsPregnant is private. The call compiled fine against the stub and would not
have existed on anybody's machine. The stub was also more permissive than the assembly, which
is the one thing a stub must never be; both are fixed, and the pregnancy test is now one shared
definition read off the ZDO.
Testing
117 assertions, no Valheim required. Eleven deliberate re-breaks, each failing exactly its own
assertions — except one, which passed, and turned out to be a guard in Mending that could
never fire. It was removed and the assertion re-pointed at the line that actually protects the
case. See DESIGN.md.