FeNigma/README.md
Dominik Roth 8bec273b3f Initial commit: IRON NEST Assist
GTK4/libadwaita desktop helper for IRON NEST: Heavy Turret Simulator.
Reads clipboard screenshots of the game's typewriter orders via Tesseract
OCR, parses absolute/relative entity positions, geometrically resolves
relative bearing/distance clues into map coordinates, and provides a
firing-commands sidebar with real ballistics (elevation/azimuth/powder
charge). Screen-reading only — no game files touched, no input injected.

- models.py: Board/Nest/Spotter/ReferencePoint/Target data model
- ocr.py: Tesseract preprocessing + typewriter-text parsing
- solver.py: bearing/distance geometric resolution (4 solvable shapes)
  plus position-based dedup for generic contacts later identified more
  specifically at the same resolved coord
- ballistics.py / shells.py: elevation/azimuth/charge math, shell types
- grid_widget.py: interactive map canvas (hidden entities and,
  optionally, dead targets are excluded from the map view entirely
  unless selected)
- firing_panel.py: drag-reorderable firing-command sidebar
- app.py: main window wiring it all together

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 20:01:50 +02:00

155 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ironnest-assist
Screen-reading helper for **IRON NEST: Heavy Turret Simulator**. Tracks the
Nest, Spotters, Reference Points, and Targets on a map matching the game's
grid, settable by hand, by a free-text description, or by OCR-ing a
clipboard screenshot of the typewriter order. A solver resolves relative
descriptions ("Bearing 293 from Alpha") into absolute coordinates once
their dependencies are known. Single-player QoL tool — no game files
touched, no input injected.
## Stack
- **GTK4 + libadwaita** (via PyGObject) — native GNOME look on Linux. GTK4
itself also runs on Windows/macOS (Win32/Quartz backends), just without
full Adwaita chrome there.
- **Pillow / numpy** — image preprocessing for OCR.
- **pytesseract + tesseract** — text OCR of the typewriter order.
## Run
```bash
./run.sh
```
GTK apps with this app ID are single-instance — if a run gets killed
uncleanly it can leave a zombie registered on D-Bus and silently no-op the
next launch. If `./run.sh` seems to do nothing, `pgrep -af ironnest_assist`
and kill any stragglers first.
## Coordinate system
Large grid: `X` in `A``T` (20 cols), `Y` in `1``10` (10 rows, row 1 at
the bottom). Sub-grid within a cell: `x`, `y` in `0``9`. **One large cell
is 1km × 1km** — that scale is what the solver's bearing/distance math
runs on. Quick keyboard entry in the coord dialog: type e.g. `C433`
(letter + 3 digits) to fill and submit in one go — `0` for the `Y` digit
means `10`.
### Location: coord and/or description
Every entity's `location` (`models.py`) independently holds:
- `coord` — a resolved absolute position.
- `desc_raw` + `clues` — a raw free-text description and the `Clue`s
parsed out of it (`reference`, `bearing_deg`, `distance_km`), each
naming another entity it's relative to.
- `potential_coords` — set instead of `coord` when the solver found the
position genuinely ambiguous (see below); shown on the map, never
chained into further resolution.
These aren't either/or — setting a coord never erases an existing
description (and vice versa), since a coord can arrive via OCR *after* a
description was already on file, or a description can be added as extra
context for an already-placed entity.
### Solver (`solver.py`)
Walks every RP/Target's clues, resolving whatever it can against
currently-known positions, repeating until nothing new resolves (handles
chains, e.g. `AmmoCache#3``AmmoCache#2``Alpha`/Spotters). Handles:
1. One clue with both bearing *and* distance from a resolved reference —
always unique.
2. Two bearing-only clues from different references — ray/ray
intersection, always unique (unless parallel).
3. A bearing-only + a distance-only clue from different references —
ray/circle intersection. A ray can cross a circle at 0, 1, or 2 points;
when there are 2, that's genuinely ambiguous — both candidates go into
`potential_coords` instead of picking one, and nothing depends on them
further.
Two distance-only clues (circle/circle, also up to 2 solutions) isn't
handled — even less to disambiguate with.
Runs automatically after every mutation (`MainWindow._refresh()` is the
single choke point), so newly-unblocked descriptions resolve immediately.
### Map overlays
Every entity row has an eye-icon (hide from map) and a star-icon (always
show its bearing/distance overlay, vs. only on hover). Hovering a marker,
or pinning it with the star, draws its clues: a yellow line for a
bearing-only clue, a white circle (radius = distance) for a distance-only
clue, a yellow arrow when a single clue has both. Ambiguous entities draw
both `potential_coords` as hollow dashed markers. Targets also have an
alive/destroyed toggle (checkmark icon) — dead ones show struck-through in
their list and dimmed on the map.
## OCR
Two sub-pipelines, planned: **text** (typewriter orders, implemented) and
**image** (map icons/markers, not started). Text OCR: screenshot →
grayscale → divide by a heavily-blurred copy of itself to flatten the
game's light-falloff vignette → threshold → `tesseract --psm 6` → parsing.
See `src/ironnest_assist/ocr.py`.
Two text formats parsed, both fuzzy/typo-tolerant (`difflib` keyword
matching, digit/letter OCR-mixup normalization `O`/`0` `I`/`l`/`1` `S`/`5`
`B`/`8` etc., and every separator — `#`, `:`, block-terminating `.`
treated as just as corruptible as any other character, never matched
literally):
1. **Absolute grid refs**`IRON NEST - C4 3:3`, `Spotter#1 - F7 7:1`
`nest_coord`, `spotters`.
2. **Field-intelligence blocks** — named entity header (`Target#5`,
`AmmoCache#1:`, `Reference Point Alpha:`) followed by one or more
`Bearing`/`Distance`/combined clue lines, terminated by a blank line or
lone `.``reference_points`, `targets`, each as (raw text, parsed
clues). The same clue grammar (`ocr.parse_clues_from_text`) also backs
the manual "Description" tab in the coord-entry dialog.
- Header clipboard button (or `Ctrl+P`): universal fetch — merges
everything recognized into the board.
- A category's "from screenshot" actions extract only that category;
toasts an error if the requested item isn't found in the screenshot.
- "Load all from screenshot" / a list item's screenshot action both use
the same merge: same name/id → update in place, new → add.
Known limitation: OCR on a *heavily skewed/rotated* screenshot degrades
badly — psm 6 assumes a roughly-upright text block, and deskewing isn't
implemented. A front-on-ish shot works well. Considered switching OCR
engines (vision-LLM structured extraction, PaddleOCR/EasyOCR) instead of
continuing to special-case Tesseract's misreads one at a time — staying
on Tesseract per your call for now.
## Ammunition (`shells.py`)
`Shell` enum from High Command's field reference — description, blast
radius in km (`None` where not yet measured), and whether it's a
"standard" (unlocked-by-default) type. Feeds the firing-solution
calculator later (e.g. AP is required for underground supply caches per
the typewriter note).
## Firing commands panel
Header button (top-right, `sidebar-show-right-symbolic`) slides in a
right-hand sidebar via `Adw.OverlaySplitView` — it shares the window's
width with the map (narrows it), rather than overlaying on top. One
placeholder card per board target (`firing_panel.py`): shell + quantity,
elevation/azimuth readout, confirm/cancel — all dummy values for now,
since the actual aiming math isn't known yet (see open questions).
## Status
Board data model, map view + overlays, exact/description coordinate
input, save/load to JSON, text OCR for both known typewriter formats, the
bearing/distance solver, and the firing-commands panel shell are all
working. Still open: image OCR sub-pipeline, deskewing, circle/circle
ambiguous case, and the actual firing-solution calculator (turret aiming
math still unknown — the panel currently just shows placeholder cards).
## Open questions
- What inputs the firing-solution math actually needs, beyond target
position and shell choice