Dafini-Husbandry icon

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_collider is family, 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, person or one. The count is what gave it away — it did not move when six Character members 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 now virtual returning false with Player overriding it to true, matching the assembly. The sibling project had a settable bool on Character and no override, so every Player in its harness read as not a player — worth fixing here before anything relied on it.
  • husbandry reports the sweep, and says NOT SWEPT rather 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.