Vigil
← All news

v1.3.0 — cactbot annotations Stage 1 (human-readable mechanic names + expected timings)

- **`vendor/cactbot/`** — 7 timeline files vendored from https://github.com/OverlayPlugin/cactbot (Apache 2.0, NOTICE included): `r9s.txt`, `r10s.txt`, `r11s.txt`, `r12s.txt` (cov…

Added — vendored cactbot timelines + annotation pipeline

  • vendor/cactbot/ — 7 timeline files vendored from https://github.com/OverlayPlugin/cactbot (Apache 2.0, NOTICE included): r9s.txt, r10s.txt, r11s.txt, r12s.txt (covers M12S + M12S-P2), futures_rewritten.txt, the_omega_protocol.txt, dragonsongs_reprise_ultimate.txt. Refresh by re-curling the upstream raw URLs.
  • ingest/cactbot.py — parser for cactbot's <time> "<label>" Ability { id: "<HEX>" } timeline grammar:
    • Extracts (abs_time_s, label, ability_ids[], phase_index, phase_label) per entry.
    • Phase detection from # Phase Two / # Phase 5 / # Adds Phase / # -- p2 -- comment markers (handles all formats cactbot uses).
    • Hex ability IDs converted to decimal so they join directly against our events.ability_game_id (PLAN Invariant 2).
    • Phase 0 anchors at the pull start (abs_time=0) so phase-relative times line up with our fight_model.relative_t_ms (which T-103 measures from fight start in P0).
    • Subsequent phases anchor at the first entry inside them (cactbot doesn't always emit an explicit phase-start abs_time).
  • annotate_fight_model_for_encounter(session, encounter_id, version=1) — for each fight_model row, find cactbot entries with matching ability_game_id, prefer same-phase candidates, pick the one closest in phase-relative time, persist cactbot_label / cactbot_phase_label / cactbot_expected_t_ms onto the row.
  • Schema migration 8ac127cddffe — three nullable columns on fight_model: cactbot_label, cactbot_phase_label, cactbot_expected_t_ms.
  • API: POST /api/encounters/{encounter_id}/fight-model/annotate-cactbot — runs the annotator for one encounter, returns {annotated, missed_no_match, missed_no_timeline} counts.
  • read_fight_model() extended to include the three new fields in its response payload — so every existing consumer (Fight Map, Compare, fight-model endpoint) sees the annotations.
  • React FightMap.jsx:
    • Phase label column shows cactbot_phase_label (e.g. "P2", "Adds") when present, falls back to P{N}.
    • Ability dot tooltip prepends cactbot_label (e.g. "Killer Voice") above the raw ability name + ID, plus shows expected-time + drift (e.g. "expected 10.4s (drift +0.7s)") for any annotated mechanic.
  • 18 new tests in tests/test_cactbot.py: hex parsing (3), phase marker detection (7 parametrized), timeline-extraction shape, phase-relative time computation, best-match phase preference, best-match fallback, real vendored file load (M9S + FRU), end-to-end annotate persistence. 326 tests passing (308 → 326).
  • 6-line autouse fixture in tests/test_fflogs_user_oauth.py (_clear_fflogs_user_auth) to wipe the real connected-user row at test start — those tests started failing once the dev DB had a real connected Gold user from the v1.2.0 live AC. Savepoint rollback restores production data on test exit.

Live AC — annotation rates across all 8 encounters

EncounterAnnotated / total%
M9S (101)16 / 4238%
M10S (102)32 / 6252%
M11S (103)52 / 8363%
M12S (104)28 / 5650%
M12S-P2 (105)45 / 7262%
FRU (1079)79 / 14455%
TOP (1068)63 / 13248%
DSR (1065)75 / 16246%

Missed-match rows are mostly cosmetic / sub-cast abilities cactbot collapses into a single label.

Sample of what users see now

  • M9S P1: "Killer Voice" (drift +0.7s), "Hardcore" (drift +0.7s), "Vamp Stomp" (drift +0.8s), "Sadistic Screech" (drift +1.1s) — real strat-doc names.
  • FRU P1: "Cyclonic Break", "Sinsmoke/Sinsmite", "Powder Mark Trail", "Burn Mark", "Burnout".
  • TOP P1: "Program Loop", "Storage Violation", "Blaster", "Pantokrator".

Stage 2 (next, separate ship)

Per-pull expected-vs-actual timeline diff: for each canonical mechanic in a pull, compute how off its actual time was from cactbot's expected phase-relative time. Surfaces "your phase 1 ran 4s long → everything in phase 2 shifted late" diagnostics directly. Deferred until user confirms Stage 1 reads well.