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).
- Extracts
annotate_fight_model_for_encounter(session, encounter_id, version=1)— for eachfight_modelrow, find cactbot entries with matchingability_game_id, prefer same-phase candidates, pick the one closest in phase-relative time, persistcactbot_label/cactbot_phase_label/cactbot_expected_t_msonto the row.- Schema migration
8ac127cddffe— three nullable columns onfight_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 toP{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.
- Phase label column shows
- 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
| Encounter | Annotated / total | % |
|---|---|---|
| M9S (101) | 16 / 42 | 38% |
| M10S (102) | 32 / 62 | 52% |
| M11S (103) | 52 / 83 | 63% |
| M12S (104) | 28 / 56 | 50% |
| M12S-P2 (105) | 45 / 72 | 62% |
| FRU (1079) | 79 / 144 | 55% |
| TOP (1068) | 63 / 132 | 48% |
| DSR (1065) | 75 / 162 | 46% |
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.