Skip to content

Ess 0.3.3

Choose a tag to compare

@github-actions github-actions released this 25 Jul 03:56

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.command is 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 via Ess.On.death, no polling. The actual
    payoff is Ess.Followers.order(behavior, opts) — command the WHOLE current roster (any of Ess.AIOrders'
    11 behaviors) with no guid list to re-thread through your own script every call. Ess.Easy.Followers adds
    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 whatever order()'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): attack resumes Follow the instant
      its target dies; a non-looping move/patrol resumes once every follower finishes its route. Guard/
      hold/a looping patrol have no natural "done" and stay on that order until order("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, and Force=true alone doesn't
      reliably preempt it — order() now clears it first with Ai.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"/HiPri worked consistently, so order() now defaults every order's priority
      to "hi" (not changed in Ess.AIOrders.command itself, whose other callers never had a Role to preempt
      in the first place).
  • Ess.Loop.stats(id) / Ess.Loop.list() — introspection into the shared heartbeat registry: each
    loop's interval, ticks (count since last start()), lastDuration/avgDuration (real wall-clock
    tick cost, via Ess.Time.stamp()/.elapsed(), EMA-smoothed), and lastError. 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 handed Ai.Goal a "MoveToPos"/Location={x,y,z} table — confirmed
    LIVE to be rejected by the engine (Ai.Goal returns nil, no error, since it's pcall-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 disposable TinyGeometry
    at the destination and issuing "MoveTo" targeting THAT (the confirmed-working substitute — defend
    already did this exact trick for its own Ai.Anchor radius) instead of a raw coordinate.
  • Ess.Easy.AIOrders.attack's target silently attacked the PLAYER instead of the given guid.
    BEHAVIORS.attack only ever resolved o.target through the Ess.AIOrders.setGroup registry
    (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.target now 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 adding Force = 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 freshly Ess.Object.spawn'd vehicle
    confirmed live: Ai.Goal accepted the goal (truthy handle) once Vehicle.Usable(veh, true) was called on
    it first, matching a confirmed real-game sequence (oilcon002.lua). enter now 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=true gap "face" did — a unit coming off a prior
    "defend"/guard order (which leaves an Ai.Anchor lock 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-maintains MinDistance/MaxDistance on 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 hostile Ai.Feeling toward the target,
    disable the unit's ambient Ai.LivingWorld behaviour (it fights the Follow role for control otherwise),
    and set Ai.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.