docs(vt): VT1 catalog 06 — VTank navigation engine and the .nav format
x.cs reader/writer (uTank2 NAV 1.2, route types, ten waypoint types incl. the live-position quirk of five of them), ca.cs cycle driver, fd.cs steering and creep band, bi.cs jump (2000 ms cap), b7.cs door/lockpick, priority interactions, 240 m/unit confirmed at four sites, two real routes decoded. MossTank gap ranked. Lead spot-check: header vs a real file, jump cap, conversion sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
289c6706ba
commit
1a35b83cf8
1 changed files with 580 additions and 0 deletions
580
docs/research/vtank-kb/06-navigation-and-nav.md
Normal file
580
docs/research/vtank-kb/06-navigation-and-nav.md
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
# VTank KB 06 — Navigation and `.nav`
|
||||
|
||||
Oracle: `refs/vtank/decompiled/` (ILSpy of `utank2-i.dll`, obfuscated identifiers,
|
||||
intact strings/settings). All citations are `file:line` against that tree
|
||||
unless marked otherwise. Decompiled source is never pasted verbatim; every
|
||||
value/behavior below was read directly from the cited lines. Real sample
|
||||
files were read from `C:\Games\VirindiPlugins\VirindiTank\*.nav` (read-only,
|
||||
1,593 files on this machine) to produce the worked decodes in §1.
|
||||
|
||||
Prior art consumed before writing this: `refs/vtank/notes/2026-09-06-idlepeace-fcm-trace.md`
|
||||
(pins the rule engine `cLogic.cs`, the `g8`/`fd` approach machinery, the FCM
|
||||
saga, and the 240 m/unit inference). This doc extends that trace into the
|
||||
`.nav` file format, waypoint execution, movement/steering, door/lockpick,
|
||||
follow, and priority wiring, then compares against the acdream port.
|
||||
|
||||
## 0. Class map (obfuscated name → role)
|
||||
|
||||
| Class | Role |
|
||||
|---|---|
|
||||
| `uTank2.x` (`x.cs`) | The nav **route** object: type (`eNavType`), waypoint list, filename I/O, mutators. Owns the `"uTank2 NAV 1.2"` format. |
|
||||
| `uTank2.eWaypointType` / `uTank2.eNavType` | The two public enums (waypoint kind, route kind). |
|
||||
| `bz` (`bz.cs`) + `g3` (`g3.cs`) | The waypoint contract: `bz` = position/distance/arrival/type/bonus/label/reset; `g3` = `TextReader`/`TextWriter` (de)serialization. Every waypoint class implements both. |
|
||||
| `at` (`at.cs`) | Abstract base for the five "action" waypoint kinds (Pause, ChatCommand, OpenVendor, Portal2, UseNPC). Provides the run-once arrival gate (`p()`) and reports the **player's live position** as the node's own coordinate. |
|
||||
| `fn`,`fq`,`a4`,`f4`,`e`,`v`,`e9`,`fa`,`gr`,`di` | The ten waypoint implementations (Point, Portal, Recall, Pause, ChatCommand, OpenVendor, Portal2, UseNPC, Checkpoint, Jump — same order as `eWaypointType`). |
|
||||
| `gl` (`gl.cs`) | The single "follow this object" node used by `eNavType.Target` routes (VTank's Follow/Object-PF feature). |
|
||||
| `ca` (`ca.cs`) | The **cycle-advance** driver: implements `bz` over the *whole route*, dispatches Circular/Linear/Once/Target semantics, and is itself handed to the mover as "the current thing to walk toward." |
|
||||
| `fd` (`fd.cs`) | The **close-in mover**: per-tick steering/turning/creep state machine, reused for nav routes, corpse approach, and combat approach. |
|
||||
| `g8` (`g8.cs`) | `ILogicRule` wrapper around one `fd` instance — the "Navigate" rule (also reused for corpse-approach and target-approach rules under different names/settings). |
|
||||
| `b7` (`b7.cs`) | `ILogicRule` for the "OpenDoor" rule — door identify/open/lockpick, fully separate from `g8`/`fd`. |
|
||||
| `eb` (`eb.cs`) | The `bz` used by the **combat** target-approach `g8` instance — same interface as nav waypoints, different backing data (current attack target). |
|
||||
| `da` (`da.cs`) | Per-character macro-profile container; owns the `x` route instance (`da.k`), the `.nav` filename (`da.n`), and save/load (`da.r()`/`da.o()`). |
|
||||
| `bi` (`bi.cs`) | The Jump waypoint's execution state machine (turn-to-heading → charge → release → wait-for-landing). |
|
||||
| `d` (`d.cs`) | A lightweight EW/NS/Z coordinate+distance helper, independent of `sCoord`, used by Portal2/UseNPC/Checkpoint for "compass" positions and distance math. |
|
||||
|
||||
## 1. `.nav` format, byte-for-byte
|
||||
|
||||
### 1.1 Header and route-level fields
|
||||
|
||||
Format constant and reader/writer: `x.cs:9` (`"uTank2 NAV 1.2"`), reader
|
||||
`x.cs:200-322` (`a(TextReader)`), writer `x.cs:324-360` (`a(TextWriter)`).
|
||||
Every field is one line (`TextWriter.WriteLine`/`TextReader.ReadLine`),
|
||||
`CultureInfo.InvariantCulture` throughout (`x.cs:209,227,241-243`), no
|
||||
delimiter escaping — a `ChatCommand` payload containing an embedded newline
|
||||
would desync the reader (not accounted for in the ground truth).
|
||||
|
||||
| Line(s) | Field | Values | Cite |
|
||||
|---|---|---|---|
|
||||
| 1 | Header | literal `uTank2 NAV 1.2` — mismatch → load refused, error to chat | `x.cs:9,204-208` |
|
||||
| 2 | Route type (int) | `1`=Circular, `2`=Linear, `3`=Target, `4`=Once | `x.cs:210-223` |
|
||||
| **If Target (3):** | | | |
|
||||
| 3 | Follow target name (string) | display-only, not used to re-find the object | `x.cs:226` |
|
||||
| 4 | Follow target object id (int) | the live game GUID to follow; `0` = no target added | `x.cs:227-231` |
|
||||
| **Else (Circular/Linear/Once):** | | | |
|
||||
| 3 | Waypoint count (int) | | `x.cs:235` |
|
||||
| 4..N | One record per waypoint (§1.2) | | `x.cs:236-320` |
|
||||
|
||||
Writer mirrors this exactly (`x.cs:326-359`); there is no version field beyond
|
||||
the header string itself — VTank has shipped exactly one `.nav` format
|
||||
revision (1.2) as far as this decompile shows.
|
||||
|
||||
### 1.2 Per-waypoint record
|
||||
|
||||
Every waypoint, regardless of type, starts with a **fixed 5-line header**
|
||||
(`x.cs:238-245` read / `x.cs:353-357` write):
|
||||
|
||||
| Line | Field | Notes |
|
||||
|---|---|---|
|
||||
| 1 | Waypoint-type int (0-9) | dispatches the `switch` at `x.cs:246-318`; matches `eWaypointType` numerically |
|
||||
| 2 | East/West (double) | |
|
||||
| 3 | North/South (double) | |
|
||||
| 4 | Elevation/Z (double) | |
|
||||
| 5 | **Discarded** — writer always emits literal `0.0` (`x.cs:357`); reader reads and throws the line away (`x.cs:245`) | Not `sWaypointDesc.bonus`; that field exists in the struct (`sWaypointDesc.cs:7`) but is never populated from this stream. Dead placeholder in the shipped format. |
|
||||
|
||||
**Critical quirk — the header x/y/z triple is meaningless for five of the ten
|
||||
waypoint types.** Point (`fn`), Portal (`fq`), Recall (`a4`), Checkpoint
|
||||
(`gr`), and Jump (`di`) store/report a real, load-bearing coordinate here.
|
||||
Pause (`f4`), ChatCommand (`e`), OpenVendor (`v`), Portal2 (`e9`), and UseNPC
|
||||
(`fa`) all extend `at`, whose position accessor is
|
||||
`f9.a(this.c.ay.CharacterFilter.Id, this.c.az.Actions)` — **the player's own
|
||||
live position** (`at.cs:19-23`) — not a stored waypoint location. Because the
|
||||
writer calls `item.e()` (`bz.e()`) uniformly for every node
|
||||
(`x.cs:354-356`), the header x/y/z written for those five types is simply
|
||||
wherever the character was standing at the moment "Save" was clicked, and is
|
||||
never read back into anything meaningful. Confirmed against three real
|
||||
files (§1.4): every Pause/ChatCommand node in a route shares the exact same
|
||||
x/y/z as its neighbors, because they were all saved from one stationary
|
||||
position.
|
||||
|
||||
Per-type payload (after the 5-line header), reader `x.cs:246-318`, writer
|
||||
`x.cs:353-358` (dispatches to `item.f(TextWriter)`):
|
||||
|
||||
| Type | Class | Extra fields (in order) | Cite (read / write) |
|
||||
|---|---|---|---|
|
||||
| 0 Point | `fn` | *(none)* | `fn.cs:35-53` (both no-ops) |
|
||||
| 1 Portal | `fq` | int: portal-use object id | `fq.cs:47-61` |
|
||||
| 2 Recall | `a4` | int: recall spell id | `a4.cs:48-62` |
|
||||
| 3 Pause | `f4` | int: duration, milliseconds | `f4.cs:24-38` |
|
||||
| 4 ChatCommand | `e` | string: the literal command line | `e.cs:23-37` |
|
||||
| 5 OpenVendor | `v` | int: vendor object id, then string: vendor name | `v.cs:28-44` |
|
||||
| 6 Portal2 (`PortalByName`) | `e9` | string: object name; int: `ObjectClass`; then a `d`-record: bool "valid" + double EW + double NS + double Elev | `e9.cs:169-187` (delegates the last 4 lines to `d.a(TextReader/TextWriter)`, `d.cs:185-200`) |
|
||||
| 7 UseNPC | `fa` | string: NPC name; int: `ObjectClass`; then the same `d`-record (bool+3 doubles) | `fa.cs:134-152` |
|
||||
| 8 Checkpoint | `gr` | *(none)* | `gr.cs:40-58` (both no-ops — the header x/y/z **is** the real, load-bearing checkpoint coordinate here, unlike types 3-5) |
|
||||
| 9 Jump | `di` | double: heading (degrees); bool: "Shift" (walk vs run); one combined line: `charge-ms formatted "0.0000" + direction digit` where digit ∈ {`3`=Forward,`4`=StrafeLeft,`5`=StrafeRight}, parsed by regex `^(?<mspart>[0-9]+\.[0-9]{4})(?<mvpart>3|4|5)$` (falls back to plain-double + Forward if the regex doesn't match, e.g. an old file predating the direction suffix) | `di.cs:116-172,180-183` |
|
||||
|
||||
Types 6/7's embedded `d`-record is the **real** target coordinate (matched
|
||||
against live world objects with a name+class+proximity search — §2); the
|
||||
outer header x/y/z for those two types is exactly as meaningless as it is
|
||||
for 3/4/5, since `e9`/`fa` also extend `at`.
|
||||
|
||||
### 1.3 String encoding, coordinate units
|
||||
|
||||
- Every string field is one raw line via `TextWriter.WriteLine`/`ReadLine` —
|
||||
effectively the platform default text encoding of a `StreamWriter`
|
||||
(`x.cs:189`, no explicit `Encoding` passed) with `\r\n` line endings on
|
||||
Windows. No escaping of embedded newlines.
|
||||
- Doubles are written with `Convert.ToString(double, InvariantCulture)`
|
||||
(round-trip "R"-ish default formatting) and parsed with
|
||||
`Convert.ToDouble(string, InvariantCulture)` — plain decimal, no scientific
|
||||
notation guard (though .NET will happily parse `E`-notation on read, and a
|
||||
small elevation value like `2.70833571751912E-05` was observed verbatim in
|
||||
a real file — see §1.4).
|
||||
- **Coordinate units are VTank's own EW/NS/Z "sCoord" units, not meters.**
|
||||
The conversion constant `240.0` (units-per-meter) is confirmed
|
||||
independently at **three** unrelated call sites, which upgrades this past
|
||||
a single-line inference:
|
||||
- `d.cs:46-47` — converts a raw landblock+offset position into EW/NS by
|
||||
dividing an accumulated offset by `240.0`.
|
||||
- `sCoord.cs:42` — converts a cell's height field with `hf2.k() / 240f`.
|
||||
- `dz.cs:239` (cited in the prior trace note) — divides `MyRangeMeters` by
|
||||
`240.0` to compare against an sCoord distance.
|
||||
- `ch.cs:807,848` — the user-facing expression functions
|
||||
`coordinatedistancewithz`/`coordinatedistanceflat` literally multiply
|
||||
the internal `d`-distance by `240.0` to report meters to scripts.
|
||||
|
||||
So: **1 sCoord unit = 240 meters is CONFIRMED, not inferred** (superseding
|
||||
the prior trace note's "one inference from one line" caveat). Worked
|
||||
examples: `NavCloseStopRange` default `0.00833333333333333` = 1/120 =
|
||||
**2 m**; `AttackDistance` default `0.0208333333333333` = 1/48 = **5 m**;
|
||||
`UsePortalDistance` default `0.0166666666666667` = 1/60 = **4 m**; the
|
||||
Portal2/UseNPC candidate-match radius constant `0.0104166` (`e9.cs:87`,
|
||||
`fa.cs:82`) = **2.5 m**.
|
||||
|
||||
### 1.4 Worked decodes of real files
|
||||
|
||||
**`bunny_stuck_jump.nav`** (384 bytes, a hand-authored anti-stuck macro —
|
||||
notable in its own right, see §3.2):
|
||||
|
||||
```
|
||||
uTank2 NAV 1.2 <- header
|
||||
4 <- route type 4 = Once
|
||||
5 <- 5 waypoints
|
||||
3 <- node 1: type 3 = Pause
|
||||
59.3058208465576 <- EW (meaningless: at-derived, = player pos at save)
|
||||
-28.5630541483561 <- NS
|
||||
0.0500250021616618 <- Elev
|
||||
0 <- discarded placeholder
|
||||
2000 <- Pause duration, ms
|
||||
4 <- node 2: type 4 = ChatCommand
|
||||
59.3058208465576 / -28.5630541483561 / 0.0500250021616618 / 0 <- same, meaningless
|
||||
/ub face 270 <- chat text (a third-party plugin's face-heading command)
|
||||
3 / (coords) / 0 / 2000 <- node 3: Pause 2000 ms
|
||||
4 / (coords) / 0 / /ub jumpw 250 <- node 4: ChatCommand "/ub jumpw 250"
|
||||
3 / (coords) / 0 / 2000 <- node 5: Pause 2000 ms
|
||||
```
|
||||
|
||||
Route: Pause 2 s → `/ub face 270` → Pause 2 s → `/ub jumpw 250` → Pause 2 s,
|
||||
Once (consumes itself). This is direct field evidence for the finding in
|
||||
§3.2: VTank's native waypoint types 3-9 give an author no built-in
|
||||
generic-stuck recovery, so real users route around it with ChatCommand
|
||||
nodes that invoke a *different* plugin's jump command, gated by Pause nodes
|
||||
for timing. Every waypoint's header coordinate is identical across the
|
||||
route (confirming the "at" position quirk from §1.2 — this was saved from
|
||||
one standing position).
|
||||
|
||||
**`deathnav.nav`** (284 bytes) — a Point-based example, Once route:
|
||||
|
||||
```
|
||||
uTank2 NAV 1.2
|
||||
4 <- Once
|
||||
4 <- 4 waypoints
|
||||
0 / 58.5531522115072 / 84.7372886339823 / 2.70833571751912E-05 / 0 <- Point 1
|
||||
0 / 58.5493430455526 / 84.7770081520081 / -0.00038958340883255 / 0 <- Point 2
|
||||
0 / 58.5451157569885 / 84.821087773641 / -0.00184791684150696 / 0 <- Point 3
|
||||
3 / 58.8850743492444 / 84.9831307411194 / -0.00372291654348373 / 0 / 10000 <- Pause 10 s
|
||||
```
|
||||
|
||||
Three real, load-bearing Point coordinates (a short walk, e.g. away from a
|
||||
corpse) followed by a 10-second Pause whose own header coordinate is simply
|
||||
wherever the character stood after the last Point — consistent with §1.2.
|
||||
|
||||
## 2. Route types and waypoint execution semantics
|
||||
|
||||
### 2.1 The cycle-advance driver (`ca`, wraps the whole route)
|
||||
|
||||
`ca` (`ca.cs`) itself implements `bz` over the **entire route** and is what
|
||||
`fd` (the mover) is actually given as its target (`cLogic.cs:507,569`:
|
||||
`new g8(0, "NavCloseStopRange", "NavFarStopRange", new ca(PluginCore.dz))`).
|
||||
`fd` never sees individual waypoints; each tick it asks `ca` for "current
|
||||
position" (`ca.i()`, `ca.cs:56-75`) and "current distance" (`ca.n()`,
|
||||
`ca.cs:84-346`), and `ca.n()` is where all route-type advancement logic
|
||||
lives.
|
||||
|
||||
`ca.n()` walks a **while loop**: as long as the current waypoint's own
|
||||
distance is below `NavCloseStopRange`, it advances the index (per the rules
|
||||
below), calls the new waypoint's `.i()` reset hook, and re-checks — so a
|
||||
tick can silently skip through several already-reached waypoints in one
|
||||
call (`ca.cs:196-343`). A "still executing" waypoint (its `bz.g()` returns
|
||||
true — mid-Pause, mid-portal-wait, etc.) is treated as an artificial huge
|
||||
distance (`999999.0`, `ca.cs:442-447`) so the loop never tries to advance
|
||||
past it.
|
||||
|
||||
| Mode | Enum | Index advance rule | Exhaustion behavior | Cite |
|
||||
|---|---|---|---|---|
|
||||
| Circular | `eNavType.Circular` (1) | `index++`, wraps to `0` at the end (or `index--` wrapping to `Count-1` if `o.m` "reverse" is set) | never exhausts | `ca.cs:122-146,196-250` |
|
||||
| Linear | `eNavType.Linear` (2) | `index++` until the last node, then flips `o.m=true` and starts decrementing back to `0`, flipping again at `0` — a ping-pong | never exhausts (bounces forever) | `ca.cs:147-177,252-306` |
|
||||
| Once | `eNavType.Once` (4) | always operates on index `0`; on arrival, `RemoveAt(0)` — **the route list is mutated/consumed** | when the list empties, distance reports `0.0` and `PluginCore.PC.m()` fires (route-complete notification) | `ca.cs:178-190,308-343` |
|
||||
| Target | `eNavType.Target` (3) | not index-based — single `gl` node tracks a live object id; see §2.3/§4 | position/distance become "invalid" if the target object or the follower's own single node disappear | `ca.cs:58-68,87-97` |
|
||||
|
||||
`x.a(bool)` (`x.cs:81-94`) is a route-edit-in-progress flag: while editing
|
||||
(`true`), waypoint mutations skip the "notify + reset index" side effects
|
||||
(`x.cs:96-129`); on commit (`false`), the route data (`m.d()`), the UI grid
|
||||
(`a0.l()`), and the current index (`o.k()`) are all refreshed at once. This
|
||||
is the batching seam a Route-tab "Apply"/multi-edit UI hangs off of.
|
||||
|
||||
The user-visible `NavPrio`/`LootPrio`/`NavLootPrio` hotkeys (`da.cs:803-805`)
|
||||
and the `NavPriorityBoost`/`LootPriorityBoost` settings are what move the nav
|
||||
rule earlier in `cLogic`'s list — see §5.
|
||||
|
||||
### 2.2 Per-waypoint-type execution semantics
|
||||
|
||||
Every waypoint's `bz.g()` (`m()`/`p()` depending on class) answers "is this
|
||||
node still busy" each tick; `bz.f()` answers "what is my distance"; `bz.e()`
|
||||
answers "what is my position." The five `at`-derived types share one
|
||||
run-once gate: `at.p()` (`at.cs:43-61`) calls the subclass's `e()` setup
|
||||
exactly once, then calls `f()` every tick until `f()` first returns `false`,
|
||||
at which point the node is **permanently done** for this visit (a later
|
||||
`o()`/`bz.i()` reset call — issued when the route revisits the node on a
|
||||
Circular/Linear lap — re-arms it, `at.cs:69-73`).
|
||||
|
||||
| Type | Arrival/completion condition | Timeout / retry | Notes |
|
||||
|---|---|---|---|
|
||||
| **Point** (`fn`) | `bz.g()` always `false` (never "busy") — distance is a plain live 3D calc via `f9.a()`; the cycle-advance loop treats it as arrived once within `NavCloseStopRange` | none | Simplest type — no other state. `fn.cs:79-89,115-119` |
|
||||
| **Portal** (`fq`) | `bz.g()` (arrival-flag `m_e`) is normally `true`; goes `false` for exactly one tick right after a `TryingPortal` lock expires without ever having entered portal space, so the route advances past a portal that silently failed to trigger | Inside `UsePortalDistance`, sends `UseItem` once (guarded by `ItemUse` lock, 3 s) then holds a 30 s `TryingPortal` lock while waiting for the client to actually enter portal space | `fq.cs:94-141` |
|
||||
| **Recall** (`a4`) | `bz.g()` (`m()`) is `true` ("busy") until the caster is confirmed stationary (moved `<0.01` sCoord ≈ 2.4 m since last check) **and** combat mode is forced to Magic via `ga.a(Magic,0,anyItem:true)` (the FCM path from the prior trace note), at which point the spell is cast and the node reports itself no-longer-busy on the *next* tick regardless of cast outcome | none — no cast-failure retry | Distance is a fixed `0.5` sCoord constant (not a real distance), so this node never blocks the cycle-advance "close enough" test on proximity, only on the busy flag. `a4.cs:82-123` |
|
||||
| **Pause** (`f4`) | `f()` returns `Now < deadline` (busy while the timer hasn't elapsed); `e()` arms the deadline as `Now + durationMs` | none | Pure timer. `f4.cs:70-78` |
|
||||
| **ChatCommand** (`e`) | `e()` arms a 200 ms guard timer; `f()` stays busy through that guard, then busy again while `Actions.BusyState != 0`, then sends the command via `f9.j(text)` and completes | none (no confirmation the command "worked") | `e.cs:69-87` |
|
||||
| **OpenVendor** (`v`) | **Does not wait for the vendor window to actually open.** `f()`: if `Actions.VendorId == target` already, stays busy (blocks the route while that exact vendor is open — this only matters on a *later* re-arrival, see below); otherwise resolves the object, and unconditionally returns `false` (done) after issuing one `UseItem` gated at ≥2 s since the last attempt. On a fresh node visit this fires-and-forgets a single open attempt and the cycle-advance loop treats the node as arrived (distance-when-not-busy is a constant `0.0`, `at.q()`) — **the route advances to the next waypoint whether or not the vendor actually opened.** | 2 s internal retry gate, but only relevant while the node stays busy from a *prior* successful open | `v.cs:76-104`, `at.cs:31-41` |
|
||||
| **Portal2 / PortalByName** (`e9`) | Two-state machine: state **a** — waits out the `ItemUse` lock, re-finds the target by name+`ObjectClass`+nearest-of-the-`d`-filter match (radius `0.0104166` ≈ 2.5 m, `item.c()==0` gate of undetermined meaning — see §7), sends `UseItem`; state **b** — waits `PluginCore.@do >= startFrame + 2` (a **rendered-frame counter**, not a timer!) then checks the client is out of portal space and ≥`0.0625` sCoord (=15 m) from the pre-portal origin; if too close, warns "came out of portal space too close to the origin point" and restarts state a | none beyond the 2-frame + 15 m re-check loop | `e9.cs:94-143` |
|
||||
| **UseNPC** (`fa`) | Re-finds the NPC each tick by the same name+class+2.5 m nearest-match; sends one `UseItem`; completes when a `ChatTextInterceptEventArgs` handler observes color-3 text starting with `"{name} tells you, "` **or** color-0 text starting with `"{name} gives you"` | none (waits forever for the chat line) | `fa.cs:65-107,160-177` |
|
||||
| **Checkpoint** (`gr`) | Blocks (`p()` returns `true`) while within `NavCloseStopRange` of the stored coordinate by the mover's own tracking (`f9.a`) **but** the object-table's independently-tracked position (`fu.z`, via `PluginCore.dz.q.f(PluginCore.dn).z`) disagrees (still `>= NavCloseStopRange` from the target). This is the **only general-purpose stuck/anti-wedge mechanism in the whole nav engine** — see §3.2. | If the two trackers keep disagreeing for **15 s**, presses the forward key (`br.ai`) once, releases it 100 ms later, and bumps the global casting-suspend counter (`ga.h()`/`ga.e()`, the same counter used to pause all other logic rules mid-cast) around that single nudge | `gr.cs:94-196` |
|
||||
| **Jump** (`di`) | Delegates entirely to the `bi` state machine (§3.3); `e()`/`f()` just start/poll it | none beyond `bi`'s own internal turn/charge/land phases | `di.cs:60-73` |
|
||||
|
||||
### 2.3 Target (Follow) route (`gl`)
|
||||
|
||||
`eNavType.Target` routes hold exactly one `gl` node tracking a live object
|
||||
id (`x.a(int,string)`, `x.cs:47-54`). `gl` is the ONLY waypoint type with two
|
||||
distinct behavior modes gated by the `FollowAroundCorners` setting:
|
||||
|
||||
- **Off**: position is simply the target's live position each tick
|
||||
(`gl.cs:130`), distance is a straight 3D calc (`gl.cs:151`).
|
||||
- **On**: `gl` records a breadcrumb trail of the target's positions
|
||||
(deduping points closer than `0.0004` sCoord ≈ 0.1 m apart, `gl.cs:103-106`),
|
||||
and walks that trail backward from the follower's current position,
|
||||
discarding any breadcrumb segment already passed within `0.01` sCoord
|
||||
(≈2.4 m) of the segment **and** within `1.0` sCoord (=240 m) of the target
|
||||
overall (`gl.cs:98-116`) — i.e. it hands the mover the *nearest un-passed
|
||||
breadcrumb*, not the target's raw live position, so the follower cuts
|
||||
corners around the same path the target actually walked rather than
|
||||
beelining through walls. This runs on a 70 ms-budgeted timer tick
|
||||
(`gl.cs:67-79`, `d8.a("...", 70.0)`), independent of the main logic-tick
|
||||
cadence.
|
||||
|
||||
## 3. Movement
|
||||
|
||||
### 3.1 Autorun vs. held keys, turning, stop ranges (`fd`)
|
||||
|
||||
`fd` is a single per-tick steering function (`fd.a()`, `fd.cs:311-386`)
|
||||
reused by three different `g8` instances (§5): the main nav route
|
||||
(`NavCloseStopRange`/`NavFarStopRange`), a corpse-approach rule
|
||||
(`CorpseApproachRange-Min/Max`), and the combat target-approach rule
|
||||
(`AttackDistance`/`ApproachDistance`, backed by `eb` rather than a waypoint).
|
||||
Every tick:
|
||||
|
||||
1. Compute `num` = target's own reported distance (`bz.f()`), `num2` =
|
||||
bearing to target (`f9.b(sCoord,sCoord)`), `value` = signed-magnitude
|
||||
heading delta (`f9.b(double,double)`, always the *smaller* of the two
|
||||
possible arcs, 0-180°).
|
||||
2. **If the target node itself reports "busy"** (`bz.g()`, e.g. a Pause
|
||||
counting down, a portal-use in flight, an unconfirmed vendor-open): every
|
||||
held key and autorun is released and the tick returns immediately — the
|
||||
character stands still while the waypoint's own action executes
|
||||
(`fd.cs:322-328`).
|
||||
3. **Else if `f9.j()`** (wraps `ad.a(PluginCore.dz.az)` — the exact predicate
|
||||
is not independently confirmed, but its usage pattern strongly suggests
|
||||
"chat input box has focus," since held movement keys would otherwise type
|
||||
into an open chat box — see §7): releases strafe keys; if the heading
|
||||
delta exceeds **4°**, stops all movement and re-issues an absolute
|
||||
`Actions.FaceHeading()` server command at most once per 0.7 s (a snap-turn,
|
||||
not a held key); if aligned, engages the **creep** state (§3.1.1)
|
||||
(`fd.cs:329-347`).
|
||||
4. **Else** (normal open-world steering): picks one of two strafe keys via
|
||||
`f9.c(heading, bearing)` (a "nudge and see which way shrinks the error"
|
||||
test, `f9.cs:1061-1068`) and holds it to gently curve the character while
|
||||
walking forward, rather than snap-turning; then gates forward movement on
|
||||
a **two-tier** heading tolerance keyed off distance:
|
||||
`num > 0.0125` (≈3 m): allow forward unless `|value| > 45°`;
|
||||
`num ≤ 0.0125`: allow forward unless `|value| > 15°`
|
||||
(`fd.cs:348-379`). MossTank's `Steer()` reproduces this exact 3 m /
|
||||
45° / 15° structure.
|
||||
5. If none of the above (aligned within 4°, any distance): stop strafing,
|
||||
move forward (`fd.cs:380-385`).
|
||||
|
||||
#### 3.1.1 The creep sub-band (SetAutorun vs. held Shift+Forward)
|
||||
|
||||
`fd.a(bool,double)` (`fd.cs:112-177`) is the single mover-state toggle,
|
||||
distinguishing two DIFFERENT movement styles by whether the reported
|
||||
distance is inside the constant `fd.a = 1.0/160.0` (≈1.5 m):
|
||||
|
||||
- **Inside 1/160 ("creep")**: holds the Shift key (`br.c8`) together with
|
||||
Forward (`br.ai`) — i.e. a *walking*, not running, forward hold. If the
|
||||
character is in Peace mode at this point, the mover force-switches combat
|
||||
mode to Magic via `ga.a(Magic,0,anyItem:true)` (the same FCM saga as
|
||||
Recall) and warns *"Idle peace selected with low waypoint minimum
|
||||
distance. Will switch to magic mode."* if `IdlePeaceMode` is set
|
||||
(`fd.cs:118-139`).
|
||||
- **Outside 1/160 ("run")**: `SetAutorun(true)` (`fd.cs:154-158`).
|
||||
|
||||
Both styles **re-assert themselves every 4 seconds** while active
|
||||
(`fd.a = TimeSpan.FromSeconds(4.0)` set in the constructor, re-armed at
|
||||
`fd.cs:151-152,156-157,160-174`) — a periodic keepalive re-press of the held
|
||||
key or re-call of `SetAutorun(true)`, the closest thing to an anti-desync
|
||||
mechanism the general mover has (it is not a stuck *detector*, just an
|
||||
unconditional re-assert). There is no equivalent of VTank's own 1/160 creep
|
||||
sub-band or the 0.7 s `FaceHeading` snap-turn in MossTank's `Steer()` — see
|
||||
§6.
|
||||
|
||||
### 3.2 Stuck detection / recovery — there is exactly one mechanism
|
||||
|
||||
There is **no string "Stuck" or "Unstick" anywhere in the decompiled tree**,
|
||||
and no generic "player hasn't moved in N seconds, do something" watchdog.
|
||||
The *only* built-in anti-wedge behavior is the Checkpoint waypoint's 15 s
|
||||
dual-position-disagreement nudge (§2.2). A **Point** waypoint that the
|
||||
player physically cannot reach (geometry-wedged, stuck on a rock) has **no**
|
||||
native recovery at all — the mover will simply hold its steering forever.
|
||||
|
||||
This is corroborated by real user data: `bunny_stuck_jump.nav` (§1.4) is a
|
||||
hand-authored Once route that exists *purely* to unstick the bot, and it
|
||||
does so by chaining `ChatCommand` nodes into a **different, external**
|
||||
plugin's jump command (`/ub jumpw 250` — not VTank's own `Jump` waypoint
|
||||
type), bracketed by `Pause` nodes for timing. In practice, route authors
|
||||
route around VTank's missing general stuck-recovery by hand, using
|
||||
ChatCommand + a third-party utility, rather than relying on anything native.
|
||||
|
||||
### 3.3 Jump execution (`bi`)
|
||||
|
||||
`di.a(TextReader)`/`bi.a(double heading, bool shiftKey, double chargeMs,
|
||||
bi.d direction)` (`bi.cs:496-516`) drives a small state machine
|
||||
(`bi.cs:389-545`, states enumerated `a`-`i`):
|
||||
|
||||
1. **Turn-to-heading** (if start heading differs ≥3° from the target — note
|
||||
this is a *separate*, tighter constant than `fd`'s general 4° tolerance):
|
||||
repeatedly calls `Actions.FaceHeading()` every 2000 ms until aligned
|
||||
(`bi.cs:74-93`).
|
||||
2. **Charge**: holds Forward (or the configured Strafe-Left/Strafe-Right key,
|
||||
`bi.a(bi.d)` → `MovementForward`/`MovementStrafeLeft`/`MovementStrafeRight`,
|
||||
`bi.cs:431-441`) plus optional Shift, plus the Jump key, for the
|
||||
configured duration **clamped to at most 2000 ms**
|
||||
(`bi.cs:502-504,524-526`) — a real, verifiable ceiling MossTank does not
|
||||
visibly reproduce (see §6). Aborts mid-charge if `Actions.ChatState`
|
||||
becomes true (`bi.cs:162-166`).
|
||||
3. **Release**: releases held keys in order (jump, strafe/forward, shift)
|
||||
with the state machine tracking which keys are actually down
|
||||
(`bi.j.a/b/c` flags, `bi.cs:167-218`).
|
||||
4. **Wait for landing**: polls up to a **15 s** timeout comparing the
|
||||
captured pre-release position (`d`-record) against the live position
|
||||
with a `1/120` sCoord (2 m) settle threshold (`bi.cs:358-362`);
|
||||
`PluginCore.dz.aw.d()` (the `bi.d()` "is busy" accessor) reports
|
||||
completion.
|
||||
|
||||
## 4. Follow/stick and target approach
|
||||
|
||||
VTank reuses **one** mover class (`fd`) and **one** waypoint contract
|
||||
(`bz`) for three conceptually different "walk toward a moving/fixed thing"
|
||||
situations, each wired through its own `g8` instance in `cLogic.cs`:
|
||||
|
||||
| Situation | `g8` settings (min/max range) | `bz` backing | Cite |
|
||||
|---|---|---|---|
|
||||
| Nav route (Point et al.) | `NavCloseStopRange`/`NavFarStopRange` | `ca` (the whole route) | `cLogic.cs:507,569` |
|
||||
| Nav route, **Target** mode (Follow) | same range settings | `gl` (single object-follow node, §2.3) | `x.cs:47-54`, `gl.cs` |
|
||||
| Corpse approach (loot) | `CorpseApproachRange-Min/Max` | `fg` (not read in this pass — a corpse-specific advancer) | `cLogic.cs:492,535` |
|
||||
| Combat target approach | `AttackDistance`/`ApproachDistance` | `eb` (tracks `PluginCore.dz.p.a.b`, the active combat target's live position) | `cLogic.cs:559`, `eb.cs:55-79` |
|
||||
|
||||
`eb` is deliberately simple relative to `gl` — a straight 2D/3D distance and
|
||||
bearing to the current combat target's live position, no breadcrumb/corner
|
||||
logic, no busy flag (`m()` is always `false`) — because in combat the
|
||||
target is expected to be visible with a mostly-open line of movement, unlike
|
||||
a nav Follow target that may be walking around a building. Both share `fd`
|
||||
for the actual steering, which is the real "shared with combat" seam: **the
|
||||
turning/creep/autorun machinery in §3.1 is identical code for nav-following
|
||||
and combat-approaching** — only the position/distance provider differs.
|
||||
|
||||
## 5. Priority and interaction with other rules
|
||||
|
||||
### 5.1 Full `cLogic` rule order (`cLogic.cs:459-577`)
|
||||
|
||||
```
|
||||
START
|
||||
SpellCompMin-Critical
|
||||
(critical spell components)
|
||||
Recharge-Norm-*
|
||||
BuffSelf (a0)
|
||||
fz: normal rebuff-timer buff
|
||||
SpellCompMin-Normal
|
||||
POSTBUFF
|
||||
DispelSelf (c8) / UseDispelItem (cx)
|
||||
Recharge-Helper-* (fb, gu)
|
||||
DispelAllies (af)
|
||||
CraftFood (a9)
|
||||
RefillPetCharges (dq, Normal)
|
||||
POSTHELPER
|
||||
Autofellow (g5)
|
||||
POSTAUTOFELLOW
|
||||
OpenDoor (b7) <-- door/lockpick, BEFORE all loot/nav/attack
|
||||
PREPRIORITYLOOTACTIONS
|
||||
priority loot actions (er/aj/ar, gated EnableLooting+LootPriorityBoost)
|
||||
POSTPRIORITYLOOTACTIONS
|
||||
PREPRIORITYLOOT
|
||||
priority corpse-approach (g8, CorpseApproachRange, gated LootPriorityBoost)
|
||||
priority salvage/loot (bj/d0/a1)
|
||||
POSTPRIORITYLOOT
|
||||
PREPRIORITYNAV
|
||||
priority nav route (g8 "mr", NavCloseStopRange/NavFarStopRange,
|
||||
gated NavPriorityBoost) <-- toggled by /vt nav priority equivalent
|
||||
POSTPRIORITYNAV
|
||||
PREATTACK
|
||||
Attack (b4)
|
||||
POSTATTACK
|
||||
PREIDLESTATUS
|
||||
idle spell comps / craft / pet refill (idle variants)
|
||||
PREIDLELOOTACTIONS
|
||||
idle loot actions (+ cm(0) IdlePeace pre-chain)
|
||||
POSTIDLELOOTACTIONS
|
||||
PREIDLELOOT
|
||||
idle corpse approach (g8, CorpseApproachRange, + cm(0) pre-chain gated
|
||||
on the SAME proximity-band custom delegate as the
|
||||
mover itself)
|
||||
idle salvage/loot (+ cm(0))
|
||||
POSTIDLELOOT
|
||||
PREIDLEBUFF
|
||||
idle buff top-off (fz, gated IdleBuffTopoff)
|
||||
POSTIDLEBUFF
|
||||
PRETARGETAPPROACH
|
||||
target approach (g8 "g11", AttackDistance/ApproachDistance, + cm(0) pre-chain)
|
||||
POSTTARGETAPPROACH
|
||||
PREIDLERECHARGE
|
||||
Recharge-NoTarg-*
|
||||
POSTIDLERECHARGE
|
||||
PRENAVROUTE
|
||||
main nav route (g8 "g12", NavCloseStopRange/NavFarStopRange, + cm(0)
|
||||
pre-chain gated on the mover's own proximity-band delegate)
|
||||
POSTNAVROUTE
|
||||
ba (unidentified, low priority)
|
||||
END
|
||||
IdlePeace standalone (cm) <-- last resort: drop to peace if nothing else claimed it
|
||||
```
|
||||
|
||||
Rule dispatch is **first-match-wins in list order** (`cLogic.cs:222-235`) —
|
||||
"Priority" on `ILogicRule` is purely an index, not a numeric comparison.
|
||||
This means the ONLY way navigation runs earlier than loot/attack is the
|
||||
duplicate, `NavPriorityBoost`-gated `g8` instance placed physically earlier
|
||||
in the list (`PREPRIORITYNAV`) — there is no dynamic re-sorting.
|
||||
|
||||
### 5.2 IdlePeace pre-chain and interruption
|
||||
|
||||
`cm` (IdlePeace) is registered **six times**: once as a `LogicRulePreChain`
|
||||
pre-action on ReadScroll/StackCram/Salvage idle rules, idle corpse approach,
|
||||
idle loot, target approach, and the main nav route, plus once standalone at
|
||||
the very end (`cLogic.cs:530-577`, corroborated by the prior trace note).
|
||||
Pre-chain semantics: when the *main* rule (e.g. main nav route) is picked as
|
||||
the tick's winner, its pre-action (`cm`) runs **instead**, for that one
|
||||
tick, if `cm` itself is currently valid; the following tick, `cm` is no
|
||||
longer valid (already at peace) so the main rule finally runs — i.e. "go to
|
||||
peace first, then act," spread over two ticks minimum. `cm`'s own gate is
|
||||
just `IdlePeaceMode` setting + `CombatMode != Peace`
|
||||
(prior trace note, `cm.cs:66,70`); everything else suppressing it is
|
||||
**structural** — it only runs where explicitly chained ahead of an idle rule.
|
||||
|
||||
Combat/casting suspends the *entire* rule loop globally
|
||||
(`ga.u` counter, `cLogic.cs:214`, incremented/decremented at `gj.cs:211/226`
|
||||
and `gj.cs:198` respectively) — this is the same counter Checkpoint's 15 s
|
||||
stuck-nudge bumps around its single key-press (§2.2), meaning a Checkpoint
|
||||
nudge briefly pauses buffing/looting/attacking too.
|
||||
|
||||
Loot and combat interrupt/resume nav implicitly through list order and
|
||||
locks, not through any nav-specific pause flag: `OpenDoor` (b7) takes
|
||||
`ActionLockType.Navigation`/`ItemUse`/`DoorOpening` locks while opening a
|
||||
door (`b7.cs:106,113-128,213-224`), and `g8.b()` (the Navigate rule's
|
||||
`ValidNow`) explicitly refuses to run while `Navigation`,
|
||||
`SpreadLockTargetRequested`, or `DoorOpening` locks are held
|
||||
(`g8.cs:91-103`) — so door-opening always wins over the mover for as long
|
||||
as its locks are held, and a higher-priority loot/attack rule simply never
|
||||
lets control reach the nav-route rule that tick (first-match-wins).
|
||||
|
||||
## 6. MossTank gap analysis
|
||||
|
||||
Files reviewed: `src/AcDream.Plugins.MossTank/Navigation.cs`,
|
||||
`VtankNavRouteSerializer.cs`, `MossTankRouteProfileStore.cs`, and their
|
||||
tests (`tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs`,
|
||||
`VtankNavRouteSerializerTests.cs`).
|
||||
|
||||
**Can a real `.nav` file load today? Yes.** `VtankNavRouteSerializer.TryLoad`
|
||||
implements the exact header/route-type/waypoint-record grammar in §1,
|
||||
including the correct discard of the placeholder line, the correct
|
||||
per-type payload for all ten waypoint types, and — notably — correctly
|
||||
special-cases Portal2/UseNPC to overwrite the (meaningless) outer header
|
||||
coordinate with the embedded `d`-record's own coordinate (`VtankNavRouteSerializer.cs:151-157`).
|
||||
It is wired to actual import via `MossTankRouteProfileStore.TryImportLegacy`,
|
||||
which reads a `.nav` text file from plugin storage by filename. Manual
|
||||
verification against `bunny_stuck_jump.nav` and `deathnav.nav` (§1.4) round-trips
|
||||
cleanly against the documented grammar.
|
||||
|
||||
Ranked by impact (highest first):
|
||||
|
||||
| # | Gap | Ground truth | acdream (`Navigation.cs`) | Impact |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **OpenVendor does not wait for confirmation in VTank, but MossTank does.** | `v.f()` fires one `UseItem` and reports itself done on the very first tick regardless of outcome (§2.2) — the route advances immediately, vendor-open success or not. | `TickUse`'s OpenVendor path waits for `ActiveVendorObjectId == waypoint.ObjectId`, retrying every 2 s up to a 30 s timeout (`Navigation.cs:730-736,805-826`). | **High** — behaviorally the single biggest divergence found: real VTank routes with an OpenVendor node effectively "fire and forget," while acdream's port will block the whole route for up to 30 s if the vendor never opens (e.g. NPC out of range, wrong id). Likely a deliberate improvement, but it is not the same routine and should be a conscious, documented choice per this project's "no workarounds/redesigns without flagging the tradeoff" rule. |
|
||||
| 2 | **Lockpick selection strategy differs.** | `b7.a()` (`b7.cs:52-81`) only considers items the user has explicitly tagged in the "AssistItems" list as `fs.k`, and picks the tagged item with the **lowest** remaining uses (`bc.aa`) — i.e. a manually-curated list, consumed lowest-charge-first. | `SelectLockpick` (`Navigation.cs:515-549`) scans **all** owned items carrying a `LockpickPublicFlag` bit (auto-detected by item data, no user list) and picks the **highest**-bonus item (`LockpickModifierProperty`). | **Medium-high** — opposite selection heuristic (auto-detect + best-tool vs curated-list + use-up-worst-first) means a different physical item gets consumed first, and acdream needs no manual "tag this as a lockpick" step VTank required. Not a bug per se, but a real, evidenced behavioral difference worth a deliberate call-out. |
|
||||
| 3 | **No creep sub-band or discrete snap-turn.** | Inside 1/160 sCoord (≈1.5 m), `fd` switches from held-key strafing to a walk-speed Shift+Forward creep, with periodic (0.7 s) absolute `FaceHeading()` snap-turns when misaligned, and force-switches combat mode to Magic if in Peace at this range (§3.1.1). | `NavigationController.Steer()` (`Navigation.cs:611-635`) has exactly one steering mode: continuous held-key `TurnLeft`/`TurnRight`/`Forward` with `Run: true` always set, for every distance band. No creep, no FaceHeading, no combat-mode interaction. | **Medium** — likely an intentional simplification (acdream's movement primitive may not need VTank's turning workaround), but it means acdream never walks (vs. runs) on final approach and never forces Magic mode near a tight waypoint, which was one of VTank's real, user-visible quirks (the "Idle peace selected with low waypoint minimum distance" warning has no acdream analogue at all). |
|
||||
| 4 | **Recall's "must be stationary" + forced Magic-mode gate is not visibly reproduced.** | `a4.m()` refuses to cast until the caster has been stationary (movement `<0.01` sCoord ≈2.4 m) since the last check, and force-switches combat mode to Magic via the FCM sequence before casting (§2.2). | `TickRecall`/`SubmitRecall` (`Navigation.cs:850-914`) call `Automation.Magic.Cast(...)` directly with no visible stationary check or explicit combat-mode sequencing in this file. | **Medium** — could not determine whether `Automation.Magic.Cast` internally handles combat-mode sequencing at a lower plugin-abstraction layer (out of scope for this file); if it does not, casting Recall while still moving, or from Melee/Missile mode, would diverge from ground truth. |
|
||||
| 5 | **Jump-charge duration is not clamped to VTank's 2000 ms ceiling.** | `bi.a(...)` clamps any requested charge duration to at most 2000 ms (`bi.cs:502-504,524-526`) — this appears to be a deliberate ceiling on how long the jump key can be held. | `RouteWaypoint.JumpChargeMilliseconds` is clamped to `[0, 10_000]` on load (`MossTankRouteProfileStore.cs:428-431`) and not further bounded in `TickJump`. | **Low-medium** — a route (VTank-authored or hand-edited) requesting >2000 ms would charge a jump far longer in acdream than real VTank/retail ever would; low likelihood in practice since real `.nav` files were themselves produced under the 2000 ms VTank ceiling, but a hand-edited or generated route could exceed it. |
|
||||
| 6 | **Portal2/UseNPC candidate filter omits the ground truth's `item.c()==0` gate.** | `e9.g()`/`fa.g()` only consider candidates where `item.c() == 0` (§7 — exact meaning undetermined, plausibly "not dead"/"visible") in addition to name+class+proximity. | `TryFindObject` is opaque from this file (defined elsewhere in the plugin abstraction); could not confirm whether an equivalent filter exists. | **Low** — flagged for follow-up rather than asserted as missing. |
|
||||
| 7 | **Chat-color gate on UseNPC's "got a response" detection is dropped.** | `fa.a(ChatTextInterceptEventArgs)` only accepts color-3 "tells you" or color-0 "gives you" lines (`fa.cs:160-177`). | `HasNpcResponse` (`Navigation.cs:829-848`) matches on text content and sender name only, with no color/channel check, plus an extra `Sender.Equals(npcName)` branch not present in ground truth. | **Low** — small false-positive risk (any channel's text matching the phrase would complete the node), unlikely to matter in practice given the fairly specific phrase match. |
|
||||
| 8 | **Door frame-count debounce vs. time-based retry.** | Portal2's post-use verification waits `PluginCore.@do >= startFrame + 2` — at least two *rendered frames*, not a duration — before checking arrival (`e9.cs:126-139`). | acdream's equivalents are all elapsed-seconds based (`UseRetrySeconds`, etc., `Navigation.cs:181` and throughout). | **Low** — a frame-based debounce doesn't map cleanly onto acdream's tick model in the first place; noted for completeness, not actionable. |
|
||||
|
||||
Correctly and precisely ported (confirmed, not a gap — listed since they
|
||||
were non-obvious and worth recording as verified rather than re-litigated):
|
||||
the exact 3 m/45°/15° steering tolerance tiers (§3.1 point 4); the
|
||||
Checkpoint 15 s stuck-nudge threshold (`CheckpointRetrySeconds = 15d`
|
||||
exactly matches `gr.cs`'s 15-second gate, using an analogous
|
||||
live-vs-server-confirmed dual-position-source design); the Portal2/UseNPC
|
||||
2.5 m reacquire radius (exactly `0.0104166 × 240`); the Portal-exit "too
|
||||
close to origin" 15 m threshold (exactly `0.0625 × 240`); the door
|
||||
identify/open ranges and lockpick excess-threshold *default numbers*
|
||||
(20 m / 4 m / `-50`, byte-for-byte matches of `DoorIDRange`/`DoorOpenRange`/
|
||||
`DoorLockpickDiffExcessThreshold` in `uTank2.Resources.defaultsettings.usd`);
|
||||
and the Circular/Linear/Once index-advance semantics of §2.1.
|
||||
|
||||
## 7. Could not determine
|
||||
|
||||
- **`f9.j()`'s exact predicate.** Used by `fd` to switch from held-key
|
||||
strafing to FaceHeading-snap-turn + creep (§3.1 point 3). Wraps
|
||||
`ad.a(PluginCore.dz.az)`; the naming and the fact that `bi.cs` separately
|
||||
and directly checks `Actions.ChatState` for its own, unrelated jump-abort
|
||||
logic makes "chat box has input focus" a strong hypothesis (holding
|
||||
movement keys while a chat box is focused would type into it instead of
|
||||
moving), but the `ad` class itself was not read to confirm.
|
||||
- **`item.c() == 0` filter in `e9.g()`/`fa.g()`** (Portal2/UseNPC candidate
|
||||
matching, `e9.cs:77-78`, `fa.cs:72-73`). Plausibly a "not dead"/"is
|
||||
visible" flag on the world-object wrapper `fu`, not confirmed.
|
||||
- **Exact default for `ApproachDistance`.** The `defaultsettings.usd` text
|
||||
format's field grouping (`d`/value/`s`/description/`i`/category) was
|
||||
reverse-engineered from context around `NavCloseStopRange` et al., but the
|
||||
apparent default read for `ApproachDistance` (`0`) would make the
|
||||
combat-approach `g8` instance's "in band" test degenerate (`fd.f()` would
|
||||
treat almost any positive distance as "outside band"), which doesn't match
|
||||
the feature clearly being functional in practice. Either the pairing was
|
||||
misread for this one key, or `ApproachDistance` is normally set by the
|
||||
user/character-file rather than left at its shipped default. Not resolved.
|
||||
- **`PluginCore.PC.m()`** — fired when an `Once` route empties itself
|
||||
(`ca.cs:340`). Presumably a "route complete"/UI-refresh notification; not
|
||||
traced to its definition.
|
||||
- **`ba` (`cLogic.cs:575`)** — a rule registered at the very end of the
|
||||
main list, after `POSTNAVROUTE`, before the closing `END` sentinel and the
|
||||
standalone `IdlePeace`. Not opened in this pass; low priority since it
|
||||
sits after every rule this doc concerns itself with.
|
||||
- **Exact left/right assignment of the `br.ah`/`br.aj` strafe-key pair**
|
||||
(`fd.cs:350-359`) — confirmed *that* one of the pair is held based on
|
||||
`f9.c()`'s sign, not confirmed *which enum member maps to which physical
|
||||
strafe direction*, since `br` (`br.cs`) is a plain sequential int enum
|
||||
with no name-to-action lookup table in the files read for this pass.
|
||||
Loading…
Add table
Add a link
Reference in a new issue