Ess 0.3.3
The Ess.Followers / Ess.AIOrders live-verification pass. Every fix and addition below was tested
against the running game (cross-checked against the decompiled game script corpus where the live behavior
alone didn't explain it), not just read-reviewed — see the entries themselves for what was actually
confirmed.
Added
Ess.Followers— a lifecycle-aware "who's currently assigned to me" roster, built entirely on
Ess.AIOrders/Ess.On.death/Ess.Mark(no new native calls).Ess.AIOrders.commandis stateless — every
call re-passes an explicit guid list, and nothing remembers who you've already recruited, or reverts the
Ai.Feeling/Ai.LivingWorld/Ai.SetState("Vip")state"follow"sets when following ends.
Ess.Followers.recruit(guid, opts)runs that sequence AND remembers the guid;.dismiss(guid)reverts it
AND forgets it; a dead follower prunes itself automatically viaEss.On.death, no polling. The actual
payoff isEss.Followers.order(behavior, opts)— command the WHOLE current roster (any ofEss.AIOrders'
11 behaviors) with no guid list to re-thread through your own script every call.Ess.Easy.Followersadds
recruit(guid)/orderAttack(target)/orderPatrol(points)/orderGuard(at)one-liners.- Markers, ON by default: a floating world-space icon over every follower's head (each in its own
color, stepped by the golden angle so any number of followers stay evenly spread with no fixed palette
to exhaust), plus a temporary marker at whateverorder()'s current destination/target is — cleared the
moment a new order supersedes it or the current one naturally completes.setMarkersEnabled(bool)/
markersEnabled()toggle it. - Auto-resume-follow, on natural completion only (confirmed live):
attackresumes Follow the instant
its target dies; a non-loopingmove/patrolresumes once every follower finishes its route. Guard/
hold/a looping patrol have no natural "done" and stay on that order untilorder("follow", ...)is
called again. - Two more confirmed-live fixes specific to ordering an ALREADY-following unit onto something else: a
follower can still be mid-goal from a PRIOR order when a new one comes in, andForce=truealone doesn't
reliably preempt it —order()now clears it first withAi.RemoveGoal({Handle=0})(the confirmed
"whatever's current" wildcard). And the actual root cause of an intermittent "order does nothing" during
testing turned out to be priority, not timing:Ess.AIOrders' own per-behavior defaults (e.g. attack's
"med") are not reliably high enough to override a just-released Follow Role's leftover state, even with
Force=true— only"hi"/HiPriworked consistently, soorder()now defaults every order's priority
to"hi"(not changed inEss.AIOrders.commanditself, whose other callers never had a Role to preempt
in the first place).
- Markers, ON by default: a floating world-space icon over every follower's head (each in its own
Ess.Loop.stats(id)/Ess.Loop.list()— introspection into the shared heartbeat registry: each
loop'sinterval,ticks(count since laststart()),lastDuration/avgDuration(real wall-clock
tick cost, viaEss.Time.stamp()/.elapsed(), EMA-smoothed), andlastError. Lets a monitor catch a
loop whose tick is expensive relative to its own interval — the actual, measurable version of "this
poller feels heavy" instead of guessing from framerate. Purely additive:start()/stop()/isRunning()
are unchanged, and every existing call site (20+ files) only ever used that public surface, never
Ess.Loop._reg's internal shape directly, so extending it is backwards-compatible by construction —
confirmed by grep before making the change, not assumed.
Fixed
Ess.AIOrders:move/defend/patrol/flee/attack's position-fallback all silently no-op'd on an
on-foot human. Every one of them handedAi.Goala"MoveToPos"/Location={x,y,z}table — confirmed
LIVE to be rejected by the engine (Ai.Goalreturnsnil, no error, since it'spcall-wrapped) for ANY
raw-coordinate move on a walking human, regardless of distance, while the identical unit accepts"Idle"
fine. Cross-checked against the full decompiled game script corpus:"MoveToPos"appears in exactly one
file, and only ever targets a VEHICLE DRIVER, never a human. Fixed by spawning a disposableTinyGeometry
at the destination and issuing"MoveTo"targeting THAT (the confirmed-working substitute —defend
already did this exact trick for its ownAi.Anchorradius) instead of a raw coordinate.Ess.Easy.AIOrders.attack'stargetsilently attacked the PLAYER instead of the given guid.
BEHAVIORS.attackonly ever resolvedo.targetthrough theEss.AIOrders.setGroupregistry
(Ess.AIOrders.group(o.target)[1]) — passing a raw guid (a reticle target, say) missed that lookup
(group()returns{}for an unregistered name, per its own contract) and fell all the way through to
nearestHero().o.targetnow accepts EITHER a registered group name OR a raw uGuid directly.Ess.AIOrders.command(..., "face", ...)silently no-op'd on a unit already holding an
Ai.Anchor(AnchorRadius=0)lock (i.e. after a"hold"order) — confirmed live: the goal was accepted
(no error) but never visibly turned the unit. Fixed by addingForce = true, matching every other
movement-ish behavior in this file; no separate "release the anchor" step is needed.Ess.AIOrders.command(..., "enter", ...)silently no-op'd on a freshlyEss.Object.spawn'd vehicle —
confirmed live:Ai.Goalaccepted the goal (truthy handle) onceVehicle.Usable(veh, true)was called on
it first, matching a confirmed real-game sequence (oilcon002.lua).enternow calls this once before
issuing the goal — a harmless no-op on a vehicle that's already usable (every placed-in-level vehicle
already is)."attack"and"hold"had the same missing-Force=truegap"face"did — a unit coming off a prior
"defend"/guard order (which leaves anAi.Anchorlock active) silently ignored a follow-up"attack"
goal with no error; confirmed live and fixed the same way, proactively applied to"hold"too once the
pattern was clear.
Changed
Ess.AIOrders.command(..., "follow", ...)now uses Mercenaries 2's own real "recruit" mechanic
(Ai.Role({Role="Follow", ...}), confirmed live against the decompiled game script corpus's own
resident/mrxfollow.lua) instead of re-issuing a plain"MoveTo"goal on a dumb timer. The native Follow
role auto-maintainsMinDistance/MaxDistanceon its own and follows the target into/out of vehicles for
free — neither of which the old timer-based approach did at all. Three prerequisites, confirmed live, are
now applied in order before the role is assigned: neutralize a hostileAi.Feelingtoward the target,
disable the unit's ambientAi.LivingWorldbehaviour (it fights the Follow role for control otherwise),
and setAi.SetState(..., "Vip", true)— confirmed to be the one MISSING piece: without it,Ai.Role
still returns a truthy handle but the unit never actually follows.