diff --git a/__pycache__/analyze.cpython-314.pyc b/__pycache__/analyze.cpython-314.pyc deleted file mode 100644 index a1a6d38..0000000 Binary files a/__pycache__/analyze.cpython-314.pyc and /dev/null differ diff --git a/analysis.jsonl b/analysis.jsonl deleted file mode 100644 index cc23f9d..0000000 --- a/analysis.jsonl +++ /dev/null @@ -1,46 +0,0 @@ -{"time": "21:22:12", "frame": 0, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 204, "action_points": 140}} -{"time": "21:22:13", "frame": 1, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 204, "action_points": 140}} -{"time": "21:22:14", "frame": 2, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 204, "action_points": 140}} -{"time": "21:22:16", "frame": 3, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 203, "action_points": 140}} -{"time": "21:22:17", "frame": 4, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 202, "action_points": 140}} -{"time": "21:22:18", "frame": 5, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 202, "action_points": 140}} -{"time": "21:22:20", "frame": 6, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 201, "action_points": 140}} -{"time": "21:22:21", "frame": 7, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 201, "action_points": 140}} -{"time": "21:22:22", "frame": 8, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 200, "action_points": 140}} -{"time": "21:22:23", "frame": 9, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 200, "action_points": 140}} -{"time": "21:22:24", "frame": 10, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 200, "action_points": 140}} -{"time": "21:22:26", "frame": 11, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 199, "action_points": 140}} -{"time": "21:22:27", "frame": 12, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 198, "action_points": 140}} -{"time": "21:22:28", "frame": 13, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 198, "action_points": 140}} -{"time": "21:22:29", "frame": 14, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1971, "action_points": 140}} -{"time": "21:22:30", "frame": 15, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1971, "action_points": 140}} -{"time": "21:22:32", "frame": 16, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 196, "action_points": 140}} -{"time": "21:22:33", "frame": 17, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 196, "action_points": 140}} -{"time": "21:22:34", "frame": 18, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 196, "action_points": 140}} -{"time": "21:22:36", "frame": 19, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 195, "action_points": 140}} -{"time": "21:22:37", "frame": 20, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 194, "action_points": 140}} -{"time": "21:22:38", "frame": 21, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 194, "action_points": 140}} -{"time": "21:22:40", "frame": 22, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 193, "action_points": 140}} -{"time": "21:22:41", "frame": 23, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 193, "action_points": 140}} -{"time": "21:22:43", "frame": 24, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1921, "action_points": 140}} -{"time": "21:22:44", "frame": 25, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1921, "action_points": 140}} -{"time": "21:22:46", "frame": 26, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1911, "action_points": 140}} -{"time": "21:22:47", "frame": 27, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 190, "action_points": 140}} -{"time": "21:22:49", "frame": 28, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 190, "action_points": 140}} -{"time": "21:22:50", "frame": 29, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 189, "action_points": 140}} -{"time": "21:22:52", "frame": 30, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 189, "action_points": 140}} -{"time": "21:22:53", "frame": 31, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 188, "action_points": 140}} -{"time": "21:22:55", "frame": 32, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 188, "action_points": 140}} -{"time": "21:22:56", "frame": 33, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1871, "action_points": 140}} -{"time": "21:22:58", "frame": 34, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 1871, "action_points": 140}} -{"time": "21:22:59", "frame": 35, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 186, "action_points": 140}} -{"time": "21:23:01", "frame": 36, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:02", "frame": 37, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:04", "frame": 38, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:05", "frame": 39, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:07", "frame": 40, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:08", "frame": 41, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:10", "frame": 42, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} -{"time": "21:23:12", "frame": 43, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 184, "action_points": 140}} -{"time": "21:23:13", "frame": 44, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 184, "action_points": 140}} -{"time": "21:23:15", "frame": 45, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 183, "action_points": 140}} diff --git a/analyze.py b/analyze.py deleted file mode 100644 index 5ac27df..0000000 --- a/analyze.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Lightweight image analysis for live capture feedback. - -All stats are computed on a small downsampled copy so the demo loop stays -fast at full 1080p capture. -""" - -from collections import Counter - -from PIL import Image - -ANALYSIS_SIZE = (320, 180) -_TOP_COLORS = 5 - -# Harvesting is detected from the single pixel at this window coordinate. -# Harvesting shows a light tan (~#f2c285, luma ~201); idle shows dark brown -# (~#664c33, luma ~81). -HARVEST_PIXEL = (1710, 1068) -HARVEST_LUMA_THRESHOLD = 140 - -# The small numeric readout just before each bar at the bottom of the panel. -# Regions are (x0, y0, x1, y1) in captured-window pixel coordinates. y0 starts -# below the panel's top border line (y=1032) which would confuse OCR. -NUMBER_REGIONS = { - "mana": (0, 1033, 62, 1048), - "food": (195, 1033, 226, 1048), - "health": (355, 1033, 389, 1048), - "inventory": (520, 1033, 552, 1048), - "action_points": (610, 1033, 714, 1048), -} - -_OCR_SCALE = 3 - -try: - import pytesseract -except ImportError: - pytesseract = None - - -def _luma(pixel): - r, g, b = pixel - return round(0.299 * r + 0.587 * g + 0.114 * b) - - -def load_image(path): - """Load a PNG as RGB; return None if the file is unreadable/corrupt.""" - try: - return Image.open(path).convert("RGB") - except OSError: - return None - - -def is_harvesting(image): - """Return whether the player is harvesting, based on the harvest pixel.""" - rgb = image.convert("RGB") - color = rgb.getpixel(HARVEST_PIXEL) - lum = _luma(color) - return { - "is_harvesting": lum >= HARVEST_LUMA_THRESHOLD, - "color": "#%02x%02x%02x" % color, - "luma": lum, - } - - -def _ocr_number(image, region): - """OCR a single number region; return int or None if it can't be read.""" - if pytesseract is None: - return None - crop = image.convert("L").crop(region) - crop = crop.resize((crop.width * _OCR_SCALE, crop.height * _OCR_SCALE)) - crop = crop.point(lambda p: 0 if p < 140 else 255) - try: - text = pytesseract.image_to_string( - crop, - config="--psm 7 -c tessedit_char_whitelist=0123456789", - ) - except Exception: - return None - digits = "".join(ch for ch in text if ch.isdigit()) - return int(digits) if digits else None - - -def read_bar_values(image): - """Return the numeric readout before each bar, or None if unreadable.""" - rgb = image.convert("RGB") - return { - name: _ocr_number(rgb, region) - for name, region in NUMBER_REGIONS.items() - } - - -def _pixels(image): - px = image.load() - w, h = image.size - return [px[x, y] for y in range(h) for x in range(w)] - - -def summarize(image): - """Return size, mean RGB, and dominant colors of a captured frame.""" - small = image.convert("RGB").resize(ANALYSIS_SIZE) - pixels = _pixels(small) - n = len(pixels) - total = [0, 0, 0] - for r, g, b in pixels: - total[0] += r - total[1] += g - total[2] += b - mean = tuple(round(c / n) for c in total) - top = Counter(pixels).most_common(_TOP_COLORS) - dominant = [ - {"hex": "#%02x%02x%02x" % color, "fraction": round(count / n, 3)} - for color, count in top - ] - return {"size": (image.width, image.height), "mean_rgb": mean, "dominant": dominant} - - -def difference(a, b): - """Sum of absolute channel differences between two same-size RGB images.""" - if a.size != b.size: - return None - pa, pb = a.load(), b.load() - w, h = a.size - diff = 0 - for y in range(h): - for x in range(w): - ca, cb = pa[x, y], pb[x, y] - diff += abs(ca[0] - cb[0]) + abs(ca[1] - cb[1]) + abs(ca[2] - cb[2]) - return diff diff --git a/analyze/AGENTS.md b/analyze/AGENTS.md new file mode 100644 index 0000000..1744b0a --- /dev/null +++ b/analyze/AGENTS.md @@ -0,0 +1,144 @@ +# AGENTS.md + +Guidance for AI agents working in this repo. Read before editing. The goal of +these rules is to keep the code clean, consistent, and easy to extend without +breaking the JSONL log contract or the test suite. + +## Project in one line + +Capture the Eternal Lands X11 window, detect harvesting + the 5 HUD bar +numbers, track the draggable Manufacturing/Inventory windows, OCR the craft +status message, and append one JSON object per frame to a JSONL log for a +future "observer" to consume. + +## Commands + +Run everything through the project virtualenv — the system Python is +externally managed (PEP 668) and lacks the deps: + +```bash +source .venv/bin/activate # then plain python/pytest work +# or, without activating: +.venv/bin/python -m pytest tests/ -v +.venv/bin/python main.py +.venv/bin/python main.py --replay tests/images/harvesting +``` + +- Always run the full test suite before finishing: `.venv/bin/python -m pytest tests/` +- If you add a Python dependency, add it to `requirements.txt` and install it + into `.venv`. + +## Architecture and responsibilities + +- `capture.py` — the ONLY module that touches X11. Returns PIL RGBA images. +- `analyze.py` — pure functions only (no I/O, no side effects). Every function + takes a PIL image (or path) and returns data. Includes window detection + (`verify_window`, `find_windows`), craft-message OCR + (`read_manufacturing_message`), and `classify_message`. +- `main.py` — CLI, capture loop, replay mode, JSON record construction, and + logging. Owns all file/stdout I/O. Also owns the window-position cache + (`WindowTracker`): verify the cached position each frame, rescan the full + frame only when a window moves. +- `tests/` — folder-driven pytest tests plus fixture images. + +Keep this split. New analysis logic goes in `analyze.py`. New ways to run the +loop go in `main.py`. Never put capture logic in `analyze.py` or analysis in +`capture.py`. + +## Code style + +- **No comments unless they explain WHY** (non-obvious decisions, measured + constants). Docstrings on public functions and modules are expected. Match + the existing tone: short, declarative docstrings; a brief comment only where + the reasoning is not obvious (e.g. "y0 starts at 1033 because row 1032 is + the panel border, which confuses OCR"). +- Standard library + the deps in `requirements.txt` only. Do not introduce + new heavy dependencies without a strong reason and a requirements.txt + update. +- Return plain dicts/lists of primitives (JSON-serializable) from `analyze.py` + functions — never objects or PIL types — so results drop straight into the + log. +- Function names are snake_case and descriptive (`is_harvesting`, + `read_bar_values`, `load_image`). +- Module-level constants are UPPER_SNAKE_CASE; private helpers/constants are + `_leading_underscore`. +- No type hints required, but if you add them keep them consistent across a + file. + +## Screen-coordinate calibration + +All pixel coordinates are measured values for a **1920x1080** window and live +as constants at the top of `analyze.py` (`HARVEST_PIXEL`, +`HARVEST_LUMA_THRESHOLD`, `NUMBER_REGIONS`). + +Window coordinates are **relative to the window title bar**, never fixed +screen positions — the player can drag the Manufacturing/Inventory windows. +Calibrated constants: `MESSAGE_SEARCH` (the craft message sits at +`(title_x - 161, title_y + 121)`, verified at 5 different window positions) +and the `TITLE_VERIFY_CROP`/`FIND_TITLE_OFFSETS` tuning. + +- When you change a coordinate or threshold, you MUST re-run the tests. If you + have a screenshot proving the new value, drop it in the right + `tests/images/` folder first so the tests verify your change. +- Never "adjust" a coordinate in a test to make it pass. Verify against the + actual images. +- If the game window resolution changes, the calibration must be redone from + real screenshots — do not guess coordinates. + +## Tests + +- Tests are **folder-driven**: + - `tests/images/harvesting/*.png` → `is_harvesting` must be True + - `tests/images/not_harvesting/*.png` → must be False + - `tests/images/corrupt/*.png` → `load_image` must return None, no crash + - `tests/images/manufacturing//*.png` → the craft message read at + the pinned title position must classify to `` + - `tests/images/inventory/*.png` → `verify_window` at the pinned position +- Adding an edge case = dropping a PNG into the right folder. Do that instead + of writing one-off test code, unless a new *expected value* must be pinned + (then update `tests/test_bars.py`, e.g. `INVENTORY_EXPECTED`, or + `tests/test_windows.py`, e.g. `MFG_POSITION`). +- Keep fixture images small in number and real (actual captured frames), not + synthetic. +- Pinned OCR values in `test_bars.py` are ground truth from real frames. If a + frame's expected value must change, verify the number by reading the image + — never just update the test to match a new wrong OCR result. + +## The JSONL log contract + +`main.py` appends one JSON object per line to `analysis.jsonl` (default). +Shape: + +```json +{"time": "…", "frame": 4, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 39, "health": 40, "inventory": 106, "action_points": 140}, "manufacturing_window": [1104, 327], "manufacturing_message": {"text": "you successfully created 1 potion of minor healing", "color": "green", "category": "success"}, "inventory_window": [1629, 686]} +``` + +- The future observer parses this log. **Do not change field names, types, or + add/remove fields casually.** If you must extend it, add new keys (never + repurpose existing ones) and update `design.md`. +- `bars.*` may be `null` when OCR fails — consumers must tolerate that; keep + it that way. +- `manufacturing_window` / `inventory_window` are `[x, y]` title-bar + coordinates, or `null` when the window is not currently verified. + `manufacturing_message` is `{text, color, category}` (`text`/`color` are + `null` when no message shows), or `null` when the window position is + unverified. Message categories: `working`, `success`, `stopped`, `failed`, + `failed_lost`, `hungry`, `overloaded`, `none`, `unknown`. Consumers must + tolerate all `null` cases. +- Keep the log append-only and line-per-record (JSONL). It is tail-friendly. + +## Error handling + +- Analysis functions must never crash the capture loop. `analyze.py` catches + per-item failures (e.g. OCR returns `None` on error) and returns `None` + rather than raising. +- Optional dependency pattern: `pytesseract` is imported inside + `try/except ImportError` and the module degrades gracefully + (`pytesseract = None`). Follow this pattern for any new optional dep. +- Corrupt images → `load_image` returns `None`; callers skip them. + +## Docs + +- `design.md` — the source of truth for capabilities, calibration, + architecture, and the log format. Keep it in sync when you change behavior. +- `AGENTS.md` — these rules. Update it if a convention changes. diff --git a/analyze/__pycache__/analyze.cpython-314.pyc b/analyze/__pycache__/analyze.cpython-314.pyc new file mode 100644 index 0000000..95ee390 Binary files /dev/null and b/analyze/__pycache__/analyze.cpython-314.pyc differ diff --git a/__pycache__/capture.cpython-314.pyc b/analyze/__pycache__/capture.cpython-314.pyc similarity index 100% rename from __pycache__/capture.cpython-314.pyc rename to analyze/__pycache__/capture.cpython-314.pyc diff --git a/analyze/__pycache__/main.cpython-314.pyc b/analyze/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..7a2a5a1 Binary files /dev/null and b/analyze/__pycache__/main.cpython-314.pyc differ diff --git a/analyze/analysis.jsonl b/analyze/analysis.jsonl new file mode 100644 index 0000000..70a0382 --- /dev/null +++ b/analyze/analysis.jsonl @@ -0,0 +1,256 @@ +{"time": "21:22:12", "frame": 0, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 204, "action_points": 140}} +{"time": "21:22:13", "frame": 1, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 204, "action_points": 140}} +{"time": "21:22:14", "frame": 2, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 204, "action_points": 140}} +{"time": "21:22:16", "frame": 3, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 203, "action_points": 140}} +{"time": "21:22:17", "frame": 4, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 202, "action_points": 140}} +{"time": "21:22:18", "frame": 5, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 202, "action_points": 140}} +{"time": "21:22:20", "frame": 6, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 201, "action_points": 140}} +{"time": "21:22:21", "frame": 7, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 201, "action_points": 140}} +{"time": "21:22:22", "frame": 8, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 200, "action_points": 140}} +{"time": "21:22:23", "frame": 9, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 200, "action_points": 140}} +{"time": "21:22:24", "frame": 10, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 200, "action_points": 140}} +{"time": "21:22:26", "frame": 11, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 199, "action_points": 140}} +{"time": "21:22:27", "frame": 12, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 198, "action_points": 140}} +{"time": "21:22:28", "frame": 13, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 198, "action_points": 140}} +{"time": "21:22:29", "frame": 14, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1971, "action_points": 140}} +{"time": "21:22:30", "frame": 15, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1971, "action_points": 140}} +{"time": "21:22:32", "frame": 16, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 196, "action_points": 140}} +{"time": "21:22:33", "frame": 17, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 196, "action_points": 140}} +{"time": "21:22:34", "frame": 18, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 196, "action_points": 140}} +{"time": "21:22:36", "frame": 19, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 195, "action_points": 140}} +{"time": "21:22:37", "frame": 20, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 194, "action_points": 140}} +{"time": "21:22:38", "frame": 21, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 194, "action_points": 140}} +{"time": "21:22:40", "frame": 22, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 193, "action_points": 140}} +{"time": "21:22:41", "frame": 23, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 193, "action_points": 140}} +{"time": "21:22:43", "frame": 24, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1921, "action_points": 140}} +{"time": "21:22:44", "frame": 25, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1921, "action_points": 140}} +{"time": "21:22:46", "frame": 26, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1911, "action_points": 140}} +{"time": "21:22:47", "frame": 27, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 190, "action_points": 140}} +{"time": "21:22:49", "frame": 28, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 190, "action_points": 140}} +{"time": "21:22:50", "frame": 29, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 189, "action_points": 140}} +{"time": "21:22:52", "frame": 30, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 189, "action_points": 140}} +{"time": "21:22:53", "frame": 31, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 188, "action_points": 140}} +{"time": "21:22:55", "frame": 32, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 188, "action_points": 140}} +{"time": "21:22:56", "frame": 33, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 28, "health": 40, "inventory": 1871, "action_points": 140}} +{"time": "21:22:58", "frame": 34, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 1871, "action_points": 140}} +{"time": "21:22:59", "frame": 35, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 186, "action_points": 140}} +{"time": "21:23:01", "frame": 36, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:02", "frame": 37, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:04", "frame": 38, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:05", "frame": 39, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:07", "frame": 40, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:08", "frame": 41, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:10", "frame": 42, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 185, "action_points": 140}} +{"time": "21:23:12", "frame": 43, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 184, "action_points": 140}} +{"time": "21:23:13", "frame": 44, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 184, "action_points": 140}} +{"time": "21:23:15", "frame": 45, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": null, "health": 40, "inventory": 183, "action_points": 140}} +{"time": "21:33:25", "frame": 0, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": null, "action_points": 140}} +{"time": "21:33:27", "frame": 1, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": null, "action_points": 140}} +{"time": "21:33:29", "frame": 2, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": null, "action_points": 140}} +{"time": "21:33:32", "frame": 3, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 40, "inventory": null, "action_points": 140}} +{"time": "21:33:34", "frame": 4, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 26, "health": 40, "inventory": 1, "action_points": 140}} +{"time": "21:33:36", "frame": 5, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 26, "health": 40, "inventory": 1, "action_points": 140}} +{"time": "21:33:39", "frame": 6, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 24, "health": 40, "inventory": 9, "action_points": 140}} +{"time": "21:33:42", "frame": 7, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 22, "health": 40, "inventory": 18, "action_points": 140}} +{"time": "21:33:45", "frame": 8, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 22, "health": 40, "inventory": 18, "action_points": 140}} +{"time": "21:33:48", "frame": 9, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 22, "health": 40, "inventory": 18, "action_points": 140}} +{"time": "21:33:51", "frame": 10, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 2, "health": 40, "inventory": 22, "action_points": 140}} +{"time": "21:33:54", "frame": 11, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 18, "health": 40, "inventory": 30, "action_points": 140}} +{"time": "21:33:58", "frame": 12, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 18, "health": 40, "inventory": 30, "action_points": 140}} +{"time": "21:34:01", "frame": 13, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 17, "health": 40, "inventory": 30, "action_points": 140}} +{"time": "21:34:04", "frame": 14, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 15, "health": 40, "inventory": 34, "action_points": 140}} +{"time": "21:34:07", "frame": 15, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 12, "health": 40, "inventory": 38, "action_points": 140}} +{"time": "21:34:11", "frame": 16, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 10, "health": 40, "inventory": 42, "action_points": 140}} +{"time": "12:59:47", "frame": 0, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 33, "health": 40, "inventory": 57, "action_points": 140}} +{"time": "12:59:49", "frame": 1, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 33, "health": 40, "inventory": 57, "action_points": 140}} +{"time": "12:59:51", "frame": 2, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 33, "health": 40, "inventory": 57, "action_points": 140}} +{"time": "12:59:53", "frame": 3, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 32, "health": 40, "inventory": 60, "action_points": 140}} +{"time": "12:59:55", "frame": 4, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 29, "health": 40, "inventory": 69, "action_points": 140}} +{"time": "12:59:57", "frame": 5, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 28, "health": 40, "inventory": 69, "action_points": 140}} +{"time": "12:59:59", "frame": 6, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 26, "health": 40, "inventory": 75, "action_points": 140}} +{"time": "13:00:02", "frame": 7, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 25, "health": 40, "inventory": 75, "action_points": 140}} +{"time": "13:00:04", "frame": 8, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 25, "health": 40, "inventory": 75, "action_points": 140}} +{"time": "13:00:06", "frame": 9, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 25, "health": 40, "inventory": 75, "action_points": 140}} +{"time": "13:00:08", "frame": 10, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 24, "health": 40, "inventory": 75, "action_points": 140}} +{"time": "13:00:10", "frame": 11, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 24, "health": 40, "inventory": 75, "action_points": 140}} +{"time": "13:00:12", "frame": 12, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 23, "health": 40, "inventory": 78, "action_points": 140}} +{"time": "13:00:15", "frame": 13, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 2, "health": 40, "inventory": 85, "action_points": 140}} +{"time": "13:00:17", "frame": 14, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 2, "health": 40, "inventory": 85, "action_points": 140}} +{"time": "13:00:20", "frame": 15, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 2, "health": 40, "inventory": 85, "action_points": 140}} +{"time": "13:00:22", "frame": 16, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 28, "food": 2, "health": 40, "inventory": 85, "action_points": 140}} +{"time": "13:00:25", "frame": 17, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 19, "health": 40, "inventory": 89, "action_points": 140}} +{"time": "13:00:27", "frame": 18, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 18, "health": 40, "inventory": 89, "action_points": 140}} +{"time": "13:00:30", "frame": 19, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 18, "health": 40, "inventory": 89, "action_points": 140}} +{"time": "13:00:33", "frame": 20, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 96, "action_points": 140}} +{"time": "13:00:35", "frame": 21, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 14, "health": 40, "inventory": 1021, "action_points": 140}} +{"time": "13:00:38", "frame": 22, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 12, "health": 40, "inventory": 105, "action_points": 140}} +{"time": "13:00:40", "frame": 23, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 105, "action_points": 140}} +{"time": "13:00:43", "frame": 24, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 10, "health": 40, "inventory": 108, "action_points": 140}} +{"time": "13:00:45", "frame": 25, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 7, "health": 40, "inventory": 1171, "action_points": 140}} +{"time": "13:00:48", "frame": 26, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 6, "health": 40, "inventory": 1171, "action_points": 140}} +{"time": "13:00:50", "frame": 27, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 5, "health": 40, "inventory": 120, "action_points": 140}} +{"time": "13:00:53", "frame": 28, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 4, "health": 40, "inventory": 124, "action_points": 140}} +{"time": "13:00:55", "frame": 29, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 3, "health": 40, "inventory": 1271, "action_points": 140}} +{"time": "13:00:58", "frame": 30, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": 1, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:01:00", "frame": 31, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:01:03", "frame": 32, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:01:05", "frame": 33, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:01:08", "frame": 34, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:01:10", "frame": 35, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:01:12", "frame": 36, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 29, "food": null, "health": 40, "inventory": 133, "action_points": 140}} +{"time": "13:48:18", "frame": 0, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 207, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:19", "frame": 1, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 205, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:20", "frame": 2, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 204, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:21", "frame": 3, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 204, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:22", "frame": 4, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 203, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:23", "frame": 5, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 203, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:24", "frame": 6, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 202, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:25", "frame": 7, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 202, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:27", "frame": 8, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 201, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:28", "frame": 9, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 201, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:29", "frame": 10, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 200, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:30", "frame": 11, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 200, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:31", "frame": 12, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 199, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:32", "frame": 13, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 199, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:33", "frame": 14, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 198, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:34", "frame": 15, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 198, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:35", "frame": 16, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 1971, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:36", "frame": 17, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 1971, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:37", "frame": 18, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 196, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:38", "frame": 19, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 196, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:39", "frame": 20, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 195, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:40", "frame": 21, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 195, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:41", "frame": 22, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 194, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:43", "frame": 23, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 194, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:44", "frame": 24, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 193, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:45", "frame": 25, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 1921, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:48:52", "frame": 26, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 1921, "action_points": 140}, "manufacturing_window": [1104, 337], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:04", "frame": 0, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 186, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:06", "frame": 1, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 183, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:07", "frame": 2, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 182, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:09", "frame": 3, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 1811, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:11", "frame": 4, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 42, "health": 45, "inventory": 180, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:13", "frame": 5, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 179, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:14", "frame": 6, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 178, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:16", "frame": 7, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 1771, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:18", "frame": 8, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 176, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:20", "frame": 9, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 175, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:23", "frame": 10, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 174, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:26", "frame": 11, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 173, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:29", "frame": 12, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 171, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:31", "frame": 13, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 170, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:35", "frame": 14, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 169, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:38", "frame": 15, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 1671, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:40", "frame": 16, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:42", "frame": 17, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 165, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:45", "frame": 18, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 163, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:47", "frame": 19, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 162, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:49", "frame": 20, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 1611, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:52", "frame": 21, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 160, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:55", "frame": 22, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 159, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:49:58", "frame": 23, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 1571, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:00", "frame": 24, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 156, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:06", "frame": 25, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 155, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:08", "frame": 26, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 152, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:10", "frame": 27, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 151, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:12", "frame": 28, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 41, "health": 45, "inventory": 150, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:15", "frame": 29, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 149, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:17", "frame": 30, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1471, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:19", "frame": 31, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 146, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:21", "frame": 32, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 145, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:23", "frame": 33, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 144, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:26", "frame": 34, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 143, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:27", "frame": 35, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1421, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:29", "frame": 36, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:31", "frame": 37, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:36", "frame": 38, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:37", "frame": 39, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:39", "frame": 40, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:41", "frame": 41, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:43", "frame": 42, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:44", "frame": 43, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:46", "frame": 44, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:48", "frame": 45, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you lack the knowledge of padded leather pants to create a leather pants", "color": "red", "category": "unknown"}, "inventory_window": null} +{"time": "13:50:49", "frame": 46, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 1411, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:50:51", "frame": 47, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 38, "health": 45, "inventory": 149, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you successfully created 1 thread", "color": "green", "category": "success"}, "inventory_window": null} +{"time": "13:50:53", "frame": 48, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 36, "health": 45, "inventory": 153, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:50:55", "frame": 49, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 36, "health": 45, "inventory": 153, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:50:57", "frame": 50, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 36, "health": 45, "inventory": 153, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:50:59", "frame": 51, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 35, "health": 45, "inventory": 158, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:51:01", "frame": 52, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 35, "health": 45, "inventory": 158, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:51:03", "frame": 53, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 35, "health": 45, "inventory": 158, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:51:07", "frame": 54, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 35, "health": 45, "inventory": 158, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:51:09", "frame": 55, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 34, "health": 45, "inventory": 158, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:10", "frame": 56, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 34, "health": 45, "inventory": 158, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:12", "frame": 57, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 33, "health": 45, "inventory": 162, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you successfully created 1 thread", "color": "green", "category": "success"}, "inventory_window": null} +{"time": "13:51:13", "frame": 58, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 30, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:15", "frame": 59, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 29, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:17", "frame": 60, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 29, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:19", "frame": 61, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 45], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:24", "frame": 62, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:26", "frame": 63, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:28", "frame": 64, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:29", "frame": 65, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:31", "frame": 66, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:33", "frame": 67, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 28, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:37", "frame": 68, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:39", "frame": 69, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 45, "inventory": 166, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:41", "frame": 70, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 25, "health": 45, "inventory": 170, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:42", "frame": 71, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 171, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:44", "frame": 72, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 40, "health": 45, "inventory": 171, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:46", "frame": 73, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 39, "health": 45, "inventory": 171, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:47", "frame": 74, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 39, "health": 45, "inventory": 171, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:49", "frame": 75, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 37, "health": 45, "inventory": 179, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you successfully created 1 thread", "color": "green", "category": "success"}, "inventory_window": null} +{"time": "13:51:51", "frame": 76, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 36, "health": 45, "inventory": 179, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:51:53", "frame": 77, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 35, "health": 45, "inventory": 184, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:51:55", "frame": 78, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 34, "health": 45, "inventory": 184, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:51:57", "frame": 79, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 33, "health": 45, "inventory": 188, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you successfully created 1 thread", "color": "green", "category": "success"}, "inventory_window": null} +{"time": "13:51:59", "frame": 80, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 32, "health": 45, "inventory": 1921, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you successfully created 1 thread", "color": "green", "category": "success"}, "inventory_window": null} +{"time": "13:52:01", "frame": 81, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 30, "health": 45, "inventory": 201, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:52:02", "frame": 82, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 30, "health": 45, "inventory": 201, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:52:15", "frame": 83, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 29, "health": 45, "inventory": 205, "action_points": 140}, "manufacturing_window": null, "manufacturing_message": null, "inventory_window": null} +{"time": "13:52:17", "frame": 84, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 45, "inventory": 210, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread, and lost the ingredients", "color": "red", "category": "failed_lost"}, "inventory_window": null} +{"time": "13:52:18", "frame": 85, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": null, "health": 45, "inventory": 210, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:52:20", "frame": 86, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 25, "health": 45, "inventory": 214, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:52:22", "frame": 87, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 25, "health": 45, "inventory": 214, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:52:23", "frame": 88, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 24, "health": 45, "inventory": 214, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:52:25", "frame": 89, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 23, "health": 45, "inventory": 214, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you started working...", "color": "green", "category": "working"}, "inventory_window": null} +{"time": "13:52:26", "frame": 90, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 22, "health": 45, "inventory": 214, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:52:28", "frame": 91, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 2, "health": 45, "inventory": 218, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you successfully created 1 thread", "color": "green", "category": "success"}, "inventory_window": null} +{"time": "13:52:30", "frame": 92, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 20, "health": 45, "inventory": 218, "action_points": 140}, "manufacturing_window": [201, 49], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:52:34", "frame": 93, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 20, "health": 45, "inventory": 218, "action_points": 140}, "manufacturing_window": [220, 111], "manufacturing_message": {"text": "you failed to create a[n] thread", "color": "red", "category": "failed"}, "inventory_window": null} +{"time": "13:52:36", "frame": 94, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 17, "health": 45, "inventory": 230, "action_points": 140}, "manufacturing_window": [220, 111], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:38", "frame": 95, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 17, "health": 45, "inventory": 230, "action_points": 140}, "manufacturing_window": [220, 111], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:39", "frame": 96, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 17, "health": 45, "inventory": 230, "action_points": 140}, "manufacturing_window": [220, 111], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:44", "frame": 97, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 17, "health": 45, "inventory": 230, "action_points": 140}, "manufacturing_window": [220, 111], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:46", "frame": 98, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 45, "inventory": 230, "action_points": 140}, "manufacturing_window": [220, 111], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:51", "frame": 99, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 45, "inventory": 229, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:53", "frame": 100, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 228, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:56", "frame": 101, "is_harvesting": false, "harvest_pixel": "#664c33", "harvest_luma": 81, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 228, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:52:58", "frame": 102, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 228, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:00", "frame": 103, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": null, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:02", "frame": 104, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 226, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:04", "frame": 105, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 225, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:06", "frame": 106, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 224, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:07", "frame": 107, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 223, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:09", "frame": 108, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 222, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:14", "frame": 109, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 32, "health": 44, "inventory": 22, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:16", "frame": 110, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 219, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:18", "frame": 111, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 218, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:19", "frame": 112, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": null, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:21", "frame": 113, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 216, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:23", "frame": 114, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 215, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:25", "frame": 115, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 215, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:26", "frame": 116, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 214, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:28", "frame": 117, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 213, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:30", "frame": 118, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 22, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:32", "frame": 119, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 2, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:34", "frame": 120, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 210, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:36", "frame": 121, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 209, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:38", "frame": 122, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 208, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:39", "frame": 123, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 207, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:41", "frame": 124, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 206, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:45", "frame": 125, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 206, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:47", "frame": 126, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 203, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:49", "frame": 127, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 203, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} +{"time": "13:53:51", "frame": 128, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 311, "health": 45, "inventory": 202, "action_points": 140}, "manufacturing_window": [224, 115], "manufacturing_message": {"text": "you stopped working.", "color": "red", "category": "stopped"}, "inventory_window": null} diff --git a/analyze/analyze.py b/analyze/analyze.py new file mode 100644 index 0000000..89eeef2 --- /dev/null +++ b/analyze/analyze.py @@ -0,0 +1,298 @@ +"""Lightweight image analysis for live capture feedback. + +All stats are computed on a small downsampled copy so the demo loop stays +fast at full 1080p capture. +""" + +from collections import Counter + +from PIL import Image + +ANALYSIS_SIZE = (320, 180) +_TOP_COLORS = 5 + +# Harvesting is detected from the single pixel at this window coordinate. +# Harvesting shows a light tan (~#f2c285, luma ~201); idle shows dark brown +# (~#664c33, luma ~81). +HARVEST_PIXEL = (1710, 1068) +HARVEST_LUMA_THRESHOLD = 140 + +# The small numeric readout just before each bar at the bottom of the panel. +# Regions are (x0, y0, x1, y1) in captured-window pixel coordinates. y0 starts +# below the panel's top border line (y=1032) which would confuse OCR. +NUMBER_REGIONS = { + "mana": (0, 1033, 62, 1048), + "food": (195, 1033, 226, 1048), + "health": (355, 1033, 389, 1048), + "inventory": (520, 1033, 552, 1048), + "action_points": (610, 1033, 714, 1048), +} + +_OCR_SCALE = 3 + +# --- Crafting windows ------------------------------------------------------- +# The Manufacturing and Inventory windows are semi-transparent panels the player +# can drag around, so nothing about them is fixed in screen space. Their title +# bars are located by OCR and everything else (status message, readouts) is +# offset from the title top-left. Calibrated from the frame sets in snapshots/. +TITLE_VERIFY_CROP = (14, 12, 150, 42) # (left, top, width, height) rel. to a candidate point +TITLE_VERIFY_THRESHOLD = 115 +TITLE_SUBSTRING = {"manufacturing": "anufact", "inventory": "nven"} + +# The craft status line sits at a fixed offset from the Manufacturing title, +# measured at 5 different window positions: always (title_x - 161, title_y + 121). +# A wider search region absorbs the few pixels of jitter from title detection. +MESSAGE_SEARCH = (175, 105, 600, 70) # (left, top, width, height) rel. to title +MESSAGE_COLOR_GREEN = (170, 120, 30) # (min green, min g-r, min g-b) for green text +MESSAGE_COLOR_RED = (170, 100, 60) # (min red, min r-g, min r-b) for red text +GREEN_OCR_THRESHOLD = 150 +RED_OCR_THRESHOLD = 140 +MESSAGE_OCR_PSM = 6 + +# Full-frame OCR is used to re-locate a window after it moves. Running it at two +# scales recovers some frames that fail at one scale on busy scenes. The word +# box OCR returns is usually the title itself (0..4px off) so the candidate +# title offsets only nudge that box. +FIND_OCR_SCALES = (1.0, 0.75) +# The OCR word box usually sits on the title text; -7 covers the Inventory +# window where OCR reads the glyph a few px below the verifying title bar. +FIND_TITLE_OFFSETS = ((0, 0), (0, 4), (0, -7)) + +try: + import pytesseract +except ImportError: + pytesseract = None + + +def _luma(pixel): + r, g, b = pixel + return round(0.299 * r + 0.587 * g + 0.114 * b) + + +def load_image(path): + """Load a PNG as RGB; return None if the file is unreadable/corrupt.""" + try: + return Image.open(path).convert("RGB") + except OSError: + return None + + +def is_harvesting(image): + """Return whether the player is harvesting, based on the harvest pixel.""" + rgb = image.convert("RGB") + color = rgb.getpixel(HARVEST_PIXEL) + lum = _luma(color) + return { + "is_harvesting": lum >= HARVEST_LUMA_THRESHOLD, + "color": "#%02x%02x%02x" % color, + "luma": lum, + } + + +def _ocr_number(image, region): + """OCR a single number region; return int or None if it can't be read.""" + if pytesseract is None: + return None + crop = image.convert("L").crop(region) + crop = crop.resize((crop.width * _OCR_SCALE, crop.height * _OCR_SCALE)) + crop = crop.point(lambda p: 0 if p < 140 else 255) + try: + text = pytesseract.image_to_string( + crop, + config="--psm 7 -c tessedit_char_whitelist=0123456789", + ) + except Exception: + return None + digits = "".join(ch for ch in text if ch.isdigit()) + return int(digits) if digits else None + + +def read_bar_values(image): + """Return the numeric readout before each bar, or None if unreadable.""" + rgb = image.convert("RGB") + return { + name: _ocr_number(rgb, region) + for name, region in NUMBER_REGIONS.items() + } + + +def verify_window(image, x, y, window): + """Return True if a window of the given kind has its title bar at (x, y).""" + if pytesseract is None: + return False + left, top, width, height = TITLE_VERIFY_CROP + crop = image.convert("L").crop((x - left, y - top, x - left + width, y - top + height)) + crop = crop.resize((crop.width * _OCR_SCALE, crop.height * _OCR_SCALE)) + crop = crop.point(lambda p: 0 if p < TITLE_VERIFY_THRESHOLD else 255) + try: + text = pytesseract.image_to_string(crop, config="--psm 7") + except Exception: + return False + return TITLE_SUBSTRING[window] in text.lower() + + +def _color_mask(crop, kind): + """Return a binary mask isolating green or red text pixels in a crop.""" + px = crop.load() + width, height = crop.size + if kind == "green": + lo, dr, db = MESSAGE_COLOR_GREEN + def hit(r, g, b): + return g > lo and g - r > dr and g - b > db + else: + lo, dg, db = MESSAGE_COLOR_RED + def hit(r, g, b): + return r > lo and r - g > dg and r - b > db + mask = Image.new("1", (width, height)) + mask.putdata([1 if hit(*px[x, y]) else 0 for y in range(height) for x in range(width)]) + return mask + + +def _ocr_message_channel(crop, box, kind): + """OCR the green or red channel of a crop; return lowercased text or None.""" + if pytesseract is None: + return None + channel = crop.split()[1] if kind == "green" else crop.split()[0] + channel = channel.crop(box) + threshold = GREEN_OCR_THRESHOLD if kind == "green" else RED_OCR_THRESHOLD + channel = channel.point(lambda p: 0 if p < threshold else 255) + channel = channel.resize((channel.width * _OCR_SCALE, channel.height * _OCR_SCALE)) + try: + text = pytesseract.image_to_string(channel, config=f"--psm {MESSAGE_OCR_PSM}") + except Exception: + return None + text = " ".join(text.split()).lower() + return text or None + + +def classify_message(text, color): + """Map an OCR'd craft status line to a stable category string.""" + if not text: + return "none" + if color == "green": + if "started working" in text: + return "working" + if "successfully created" in text: + return "success" + if "stopped working" in text: + return "stopped" + if "lost the ingredients" in text: + return "failed_lost" + if "failed to create" in text or "failed to craft" in text: + return "failed" + if "hungry" in text: + return "hungry" + if "overloaded" in text: + return "overloaded" + return "unknown" + + +def read_manufacturing_message(image, title_x, title_y): + """OCR the colored status line below a Manufacturing window title. + + Returns {"text", "color", "category"} or None if OCR is unavailable. + """ + if pytesseract is None: + return None + left, top, width, height = MESSAGE_SEARCH + region = image.convert("RGB").crop( + (title_x - left, title_y + top, title_x - left + width, title_y + top + height) + ) + for kind in ("green", "red"): + box = _color_mask(region, kind).getbbox() + if box is None: + continue + pad = 2 + box = (max(0, box[0] - pad), max(0, box[1] - pad), + min(region.width, box[2] + pad), min(region.height, box[3] + pad)) + text = _ocr_message_channel(region, box, kind) + if text: + return {"text": text, "color": kind, "category": classify_message(text, kind)} + return {"text": None, "color": None, "category": "none"} + + +def _find_title_words(image): + """Return candidate title word boxes from full-frame OCR. + + Each entry is a {"window": kind, "x": float, "y": float} in full-frame + pixel coordinates. Words containing the wrong substring (e.g. "manufacture" + inside the hungry status message) are filtered out by the caller's verify. + """ + if pytesseract is None: + return [] + words = [] + for scale in FIND_OCR_SCALES: + im = image.convert("L") + if scale != 1.0: + im = im.resize((int(im.width * scale), int(im.height * scale)), Image.BICUBIC) + try: + data = pytesseract.image_to_data( + im, config="--oem 1 --psm 11", output_type=pytesseract.Output.DICT + ) + except Exception: + continue + for i, word in enumerate(data["text"]): + flat = word.lower().strip().replace(" ", "") + for kind, needle in TITLE_SUBSTRING.items(): + if needle in flat: + words.append( + {"window": kind, "x": data["left"][i] / scale, "y": data["top"][i] / scale} + ) + return words + + +def find_windows(image): + """Locate Manufacturing and Inventory windows from a full frame scan. + + Returns {"manufacturing": (x, y) or None, "inventory": (x, y) or None}. + """ + found = {"manufacturing": None, "inventory": None} + for word in _find_title_words(image): + kind = word["window"] + if found[kind] is not None: + continue + for dx, dy in FIND_TITLE_OFFSETS: + x, y = round(word["x"] + dx), round(word["y"] + dy) + if verify_window(image, x, y, kind): + found[kind] = (x, y) + break + return found + + +def _pixels(image): + px = image.load() + w, h = image.size + return [px[x, y] for y in range(h) for x in range(w)] + + +def summarize(image): + """Return size, mean RGB, and dominant colors of a captured frame.""" + small = image.convert("RGB").resize(ANALYSIS_SIZE) + pixels = _pixels(small) + n = len(pixels) + total = [0, 0, 0] + for r, g, b in pixels: + total[0] += r + total[1] += g + total[2] += b + mean = tuple(round(c / n) for c in total) + top = Counter(pixels).most_common(_TOP_COLORS) + dominant = [ + {"hex": "#%02x%02x%02x" % color, "fraction": round(count / n, 3)} + for color, count in top + ] + return {"size": (image.width, image.height), "mean_rgb": mean, "dominant": dominant} + + +def difference(a, b): + """Sum of absolute channel differences between two same-size RGB images.""" + if a.size != b.size: + return None + pa, pb = a.load(), b.load() + w, h = a.size + diff = 0 + for y in range(h): + for x in range(w): + ca, cb = pa[x, y], pb[x, y] + diff += abs(ca[0] - cb[0]) + abs(ca[1] - cb[1]) + abs(ca[2] - cb[2]) + return diff diff --git a/capture.py b/analyze/capture.py similarity index 100% rename from capture.py rename to analyze/capture.py diff --git a/analyze/design.md b/analyze/design.md new file mode 100644 index 0000000..0c0febf --- /dev/null +++ b/analyze/design.md @@ -0,0 +1,213 @@ +# Design + +## Purpose + +This project watches a running instance of the game **Eternal Lands** (an X11 +window) and continuously analyzes screenshots of it. In its current form it +detects three kinds of state and records them to a JSONL log: + +1. **Harvesting** — whether the player is currently harvesting (mining, + cutting, etc.), detected from the color of a single pixel near the bottom + of the screen. +2. **HUD values** — the numeric readouts next to the five status bars at the + bottom of the screen (mana, food, health, inventory capacity, action + points), extracted with OCR. +3. **Crafting windows** — the player can drag the Manufacturing and Inventory + windows anywhere on screen, so they are located relative to their own title + bars (not fixed coordinates). The colored status line inside the + Manufacturing window is OCR'd and mapped to a category (`working`, + `success`, `failed`, `failed_lost`, `hungry`, `overloaded`, `stopped`, + `none`). + +The near-term goal is observation only: the log is produced so a future +**observer** component can read it and act — e.g., automatically resume +harvesting when the player stops, or react when health/food get low. Keeping +the log format stable and machine-parseable is therefore a first-class +requirement. + +## Capabilities + +- **Window capture** (`capture.py`) — finds a window by title (substring, + case-insensitive) on X11 and grabs its pixels even when unfocused or + occluded, via `XGetImage` with a root-window fallback. +- **Harvesting detection** (`analyze.is_harvesting`) — samples the pixel at + `(1710, 1068)`. Light tan (~`#f2c285`, luma ≈ 201) means harvesting; dark + brown (~`#664c33`, luma ≈ 81) means idle. Decision is `luma >= 140`. +- **Bar value extraction** (`analyze.read_bar_values`) — crops the small + number that precedes each bar, binarizes it, upscales 3x, and runs tesseract + with a digits-only whitelist (`--psm 7`). +- **Window verification** (`analyze.verify_window`) — checks a `150x42` crop + around a candidate point for the title substring `anufact` (Manufacturing) + or `nven` (Inventory). This is the per-frame cache check; it passes on all + 53 reference frames. +- **Window recovery** (`analyze.find_windows`) — full-frame OCR scan + (`--psm 11`, run at two scales) that collects candidate title words and + verifies each. Used only when a cached position stops verifying, i.e. after + the window moves. +- **Craft message OCR** (`analyze.read_manufacturing_message`) — crops a + window-relative region, isolates green or red text pixels, tight-bboxes + them, and OCRs the isolated color channel. +- **Message classification** (`analyze.classify_message`) — maps an OCR'd + message plus its color to a stable category string. +- **Window cache** (`main.WindowTracker`) — keeps last-known window positions, + re-verifies them each frame, and triggers a full-frame rescan only when a + cache miss happens. +- **JSONL logging** (`main.py`) — appends one JSON object per frame. +- **Replay mode** (`main.py --replay DIR`) — processes existing PNGs instead + of a live window, so analysis and OCR can be verified offline. +- **Test suite** (`tests/`) — folder-driven pytest tests; dropping a new + screenshot into a folder automatically covers it. + +## Architecture + +``` +capture.py X11 window capture -> PIL Image (RGBA) +analyze.py Pure functions: summarize, is_harvesting, read_bar_values, + verify_window, find_windows, read_manufacturing_message, + classify_message, load_image, difference. No side effects. +main.py CLI entry point. Live capture loop or replay mode; owns the + WindowTracker cache, builds the JSON record, appends to log. +tests/ Folder-driven pytest tests + test images. +``` + +- `capture.py` is the only module that talks to X11. +- `analyze.py` is pure image analysis — every function takes a PIL image (or + path) and returns data. This makes it directly testable. +- `main.py` wires them together and owns I/O (capturing, logging, printing). + +## HUD calibration + +All screen coordinates are hardcoded for a **1920x1080 captured window** and +live at the top of `analyze.py`. They were measured from real screenshots and +must move only if the game window resolution/layout changes. + +| Constant | Value | Meaning | +|---|---|---| +| `HARVEST_PIXEL` | `(1710, 1068)` | pixel that flips color while harvesting | +| `HARVEST_LUMA_THRESHOLD` | `140` | above = harvesting, below = idle | +| `NUMBER_REGIONS["mana"]` | `(0, 1033, 62, 1048)` | crop before the blue bar | +| `NUMBER_REGIONS["food"]` | `(195, 1033, 226, 1048)` | crop before the yellow bar | +| `NUMBER_REGIONS["health"]` | `(355, 1033, 389, 1048)` | crop before the red bar | +| `NUMBER_REGIONS["inventory"]` | `(520, 1033, 552, 1048)` | crop before the dark bar | +| `NUMBER_REGIONS["action_points"]` | `(610, 1033, 714, 1048)` | crop before the purple bar | + +Regions are `(x0, y0, x1, y1)`. `y0` starts at 1033 because row 1032 is the +panel's top border, which would confuse OCR. + +Observed values on the reference screenshots: mana `32`, food `39`, health +`40`, action points `140`; inventory capacity varies (`110` → `108` → `106`) +as items are collected. + +## Window calibration + +The Manufacturing and Inventory windows are semi-transparent panels the player +can drag anywhere, so their positions are **relative to the window title bar**, +never fixed screen coordinates. All offsets below are measured from real +captures and live at the top of `analyze.py`. + +| Constant | Value | Meaning | +|---|---|---| +| `TITLE_VERIFY_CROP` | `(14, 12, 150, 42)` | `(left, top, width, height)` crop around a candidate title point | +| `TITLE_VERIFY_THRESHOLD` | `115` | binarization threshold for the title crop | +| `TITLE_SUBSTRING` | `anufact` / `nven` | OCR substring that proves the title bar | +| `MESSAGE_SEARCH` | `(175, 105, 600, 70)` | craft message search region relative to the title | +| `MESSAGE_COLOR_GREEN` | `(170, 120, 30)` | green-text pixel filter `(min g, min g-r, min g-b)` | +| `MESSAGE_COLOR_RED` | `(170, 100, 60)` | red-text pixel filter `(min r, min r-g, min r-b)` | +| `FIND_OCR_SCALES` | `(1.0, 0.75)` | full-frame scan scales; some frames only recover at one | + +- The craft message sits at `(title_x - 161, title_y + 121)` — measured at 5 + different Manufacturing window positions, all the same offset. `MESSAGE_SEARCH` + is a wider region around it that absorbs title-detection jitter. +- The title bar is ~11px tall. The verify crop is deliberately generous + (`±14/±12` px) because the verify point is the title-bar center, which OCR + occasionally reports a few pixels from the glyphs. +- Full-frame recovery is approximate: OCR word boxes land within ~15px of the + true title. Positions are re-anchored by `verify_window`, so exactness only + matters for the message offset, and `MESSAGE_SEARCH` absorbs the jitter. +- Reference window positions across the capture sessions: Manufacturing at + `(488, 480)`, `(376, 262)`, `(1330, 194)`, `(1005, 565)`, `(281, 676)`, + and `(1104, 327)`/`(1104, 337)`; Inventory always `(1629, 686)`. + +## Log format + +One JSON object per line, appended to `analysis.jsonl` (configurable via +`--log`): + +```json +{"time": "20:31:02", "frame": 4, "is_harvesting": true, "harvest_pixel": "#f2c285", "harvest_luma": 201, "bars": {"mana": 32, "food": 39, "health": 40, "inventory": 106, "action_points": 140}, "manufacturing_window": [1104, 327], "manufacturing_message": {"text": "you successfully created 1 potion of minor healing", "color": "green", "category": "success"}, "inventory_window": [1629, 686]} +``` + +- `frame` is an integer in live mode, the file name in replay mode. +- `bars` values are integers, or `null` when OCR could not read them (e.g. + tesseract not installed). Consumers must tolerate `null`. +- `manufacturing_window` / `inventory_window` are `[x, y]` title-bar + coordinates of the (currently verified) window, or `null` when unverified. +- `manufacturing_message` is `{"text", "color", "category"}` or `null`. + `text`/`color` are `null` when no message is showing (category `none`); + `manufacturing_message` itself is `null` when the Manufacturing window + position is unverified. Colors: `green` (success/working), `red` + (failed/failed_lost/hungry/overloaded/stopped). +- Message categories: `working`, `success`, `stopped`, `failed`, + `failed_lost`, `hungry`, `overloaded`, `none`, `unknown`. +- This shape is the contract for the future observer — it should not change + without updating `tests/` and the observer accordingly. + +## Testing + +`tests/images/` holds the screenshots used as fixtures, grouped by expected +behavior: + +``` +tests/images/harvesting/ is_harvesting must be True +tests/images/not_harvesting/ is_harvesting must be False +tests/images/corrupt/ must be skipped gracefully (None) +tests/images/manufacturing/ craft message at the pinned title position must + /*.png classify to the folder's category +tests/images/inventory/ verify_window must pass at the pinned position +``` + +Tests are **folder-driven**: new edge cases are added by dropping a screenshot +into the appropriate folder; no test code changes needed unless a new expected +*value* must be pinned. `tests/test_bars.py` pins per-frame expectations (e.g. +inventory = 110/108/106 depending on frame). `tests/test_windows.py` pins the +Manufacturing title position per fixture frame (`MFG_POSITION`), the Inventory +position, and a representative subset of `find_windows` recovery cases. + +## Dependencies and setup + +System: + +- `tesseract-ocr` (OCR engine; the Python binding is `pytesseract`) +- X11 (for live capture) + +Python (installed in the project virtualenv `.venv/`, see `requirements.txt`): + +- `python-xlib`, `pillow`, `pytesseract`, `pytest` + +```bash +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +.venv/bin/python -m pytest tests/ +.venv/bin/python main.py # live capture +.venv/bin/python main.py --replay tests/images/harvesting +``` + +## Known limitations + +- Coordinates are resolution-specific (1920x1080). Resizing the game window + requires recalibration of `HARVEST_PIXEL` / `NUMBER_REGIONS`. +- OCR depends on the system `tesseract` binary; if missing, bar values log as + `null` (harvesting detection is unaffected). Window/message OCR degrades the + same way (`verify_window` returns False, messages read as `null`). +- The panel appears to be semi-transparent; some scene pixels show through. + So far this has not affected detection, but a very different background + could. +- Full-frame window recovery (`find_windows`) is not 100% reliable — it found + the Manufacturing window in all 53 reference frames but missed the Inventory + window in one harvesting frame. `WindowTracker` compensates by retrying on + every frame while a window stays stale, so a missed recovery only delays the + update by a frame or two. The cheap per-frame `verify_window` cache check is + the workhorse (53/53) and recovery only runs after a window moves. +- The steady-state loop adds roughly 0.5s/frame for the two window verifies + plus message OCR; a full-frame recovery scan costs ~5-9s but only happens + after a window moves. diff --git a/main.py b/analyze/main.py similarity index 62% rename from main.py rename to analyze/main.py index fc9dac7..d94feea 100644 --- a/main.py +++ b/analyze/main.py @@ -15,14 +15,59 @@ import analyze from capture import WindowCapture, WindowNotFound -def analyze_and_print(image, frame, prev, logf): +class WindowTracker: + """Cache last-known window positions and re-locate them when they move. + + Cached positions are re-verified cheaply every frame. The first failed + verify after a stable period triggers an immediate full-frame rescan; + afterwards rescans back off to SCAN_PERIOD so a closed or unfindable + window cannot pin the loop to full-frame OCR forever. + """ + + SCAN_PERIOD = 30.0 # seconds between recovery rescans while a window is missing + + def __init__(self): + self.pos = {"manufacturing": None, "inventory": None} + self._last_scan = {"manufacturing": 0.0, "inventory": 0.0} + self._verified = {"manufacturing": -1, "inventory": -1} + self._frame = 0 + + def update(self, image): + """Return currently verified window positions (None when unverified).""" + verified = {"manufacturing": None, "inventory": None} + for window in ("manufacturing", "inventory"): + cached = self.pos[window] + if cached is not None and analyze.verify_window(image, cached[0], cached[1], window): + verified[window] = cached + self._verified[window] = self._frame + continue + just_verified = cached is not None and self._verified[window] == self._frame - 1 + if just_verified or (cached is None and self._frame == 0) or ( + time.monotonic() - self._last_scan[window] >= self.SCAN_PERIOD + ): + self._last_scan[window] = time.monotonic() + pos = analyze.find_windows(image)[window] + if pos is not None: + self.pos[window] = pos + verified[window] = pos + self._verified[window] = self._frame + self._frame += 1 + return verified + + +def analyze_and_print(image, frame, prev, logf, tracker): global _last_t info = analyze.summarize(image) harvest = analyze.is_harvesting(image) bars = analyze.read_bar_values(image) + windows = tracker.update(image) fps = 1.0 / max(1e-6, time.monotonic() - _last_t) _last_t = time.monotonic() + mfg = windows["manufacturing"] + message = analyze.read_manufacturing_message(image, mfg[0], mfg[1]) if mfg else None + inv = windows["inventory"] + record = { "time": _now(), "frame": frame, @@ -30,6 +75,9 @@ def analyze_and_print(image, frame, prev, logf): "harvest_pixel": harvest["color"], "harvest_luma": harvest["luma"], "bars": bars, + "manufacturing_window": [mfg[0], mfg[1]] if mfg else None, + "manufacturing_message": message, + "inventory_window": [inv[0], inv[1]] if inv else None, } if logf is not None: logf.write(json.dumps(record) + "\n") @@ -41,11 +89,12 @@ def analyze_and_print(image, frame, prev, logf): top = ", ".join(f"{c['hex']} {c['fraction'] * 100:.0f}%" for c in info["dominant"][:3]) status = "harvesting" if harvest["is_harvesting"] else "not harvesting" frame_str = f"{frame:05d}" if isinstance(frame, int) else frame + msg = f"mfg={mfg} msg={message['category'] if message else None}" if mfg else "mfg=none" print( f"[{frame_str}] {status} luma={harvest['luma']} " f"size={info['size'][0]}x{info['size'][1]} " f"mean={info['mean_rgb']} top=[{top}] bars={bars} " - f"delta={delta} loop_fps={fps:.1f}", + f"delta={delta} loop_fps={fps:.1f} {msg}", flush=True, ) return small @@ -55,6 +104,7 @@ def run_replay(args): """Process PNGs from a directory instead of a live window.""" logf = open(args.log, "a") if args.log else None prev = None + tracker = WindowTracker() try: for name in sorted(os.listdir(args.replay)): if not name.lower().endswith(".png"): @@ -64,7 +114,7 @@ def run_replay(args): if image is None: print(f"[skipped] unreadable image: {path}", flush=True) continue - prev = analyze_and_print(image, name, prev, logf) + prev = analyze_and_print(image, name, prev, logf, tracker) finally: if logf: logf.close() @@ -78,6 +128,7 @@ def run_live(args): print(f"Capturing window matching {args.title!r} at {args.fps:.1f} fps (Ctrl-C to stop)", flush=True) logf = open(args.log, "a") if args.log else None prev = None + tracker = WindowTracker() frame = 0 try: while True: @@ -88,7 +139,7 @@ def run_live(args): print(f"[{_now()}] {exc}; retrying...", flush=True) time.sleep(1.0) continue - prev = analyze_and_print(image, frame, prev, logf) + prev = analyze_and_print(image, frame, prev, logf, tracker) if args.save_every and frame % args.save_every == 0: path = os.path.join(args.snapshots, f"frame_{frame:05d}.png") diff --git a/requirements.txt b/analyze/requirements.txt similarity index 100% rename from requirements.txt rename to analyze/requirements.txt diff --git a/analyze/snapshots/frame_00000.png b/analyze/snapshots/frame_00000.png new file mode 100644 index 0000000..6a1774f Binary files /dev/null and b/analyze/snapshots/frame_00000.png differ diff --git a/analyze/snapshots/frame_00001.png b/analyze/snapshots/frame_00001.png new file mode 100644 index 0000000..7097bc0 Binary files /dev/null and b/analyze/snapshots/frame_00001.png differ diff --git a/analyze/snapshots/frame_00002.png b/analyze/snapshots/frame_00002.png new file mode 100644 index 0000000..6c3a974 Binary files /dev/null and b/analyze/snapshots/frame_00002.png differ diff --git a/analyze/snapshots/frame_00003.png b/analyze/snapshots/frame_00003.png new file mode 100644 index 0000000..8e5ec1f Binary files /dev/null and b/analyze/snapshots/frame_00003.png differ diff --git a/analyze/snapshots/frame_00004.png b/analyze/snapshots/frame_00004.png new file mode 100644 index 0000000..726d520 Binary files /dev/null and b/analyze/snapshots/frame_00004.png differ diff --git a/analyze/snapshots/frame_00005.png b/analyze/snapshots/frame_00005.png new file mode 100644 index 0000000..a08c23c Binary files /dev/null and b/analyze/snapshots/frame_00005.png differ diff --git a/analyze/snapshots/frame_00006.png b/analyze/snapshots/frame_00006.png new file mode 100644 index 0000000..38e039c Binary files /dev/null and b/analyze/snapshots/frame_00006.png differ diff --git a/analyze/snapshots/frame_00007.png b/analyze/snapshots/frame_00007.png new file mode 100644 index 0000000..53b42de Binary files /dev/null and b/analyze/snapshots/frame_00007.png differ diff --git a/analyze/snapshots/frame_00008.png b/analyze/snapshots/frame_00008.png new file mode 100644 index 0000000..5c66925 Binary files /dev/null and b/analyze/snapshots/frame_00008.png differ diff --git a/analyze/snapshots/frame_00009.png b/analyze/snapshots/frame_00009.png new file mode 100644 index 0000000..b26f550 Binary files /dev/null and b/analyze/snapshots/frame_00009.png differ diff --git a/analyze/snapshots/frame_00010.png b/analyze/snapshots/frame_00010.png new file mode 100644 index 0000000..54f89fa Binary files /dev/null and b/analyze/snapshots/frame_00010.png differ diff --git a/analyze/snapshots/frame_00011.png b/analyze/snapshots/frame_00011.png new file mode 100644 index 0000000..4b1d736 Binary files /dev/null and b/analyze/snapshots/frame_00011.png differ diff --git a/analyze/snapshots/frame_00012.png b/analyze/snapshots/frame_00012.png new file mode 100644 index 0000000..276ad9f Binary files /dev/null and b/analyze/snapshots/frame_00012.png differ diff --git a/analyze/snapshots/frame_00013.png b/analyze/snapshots/frame_00013.png new file mode 100644 index 0000000..314f27e Binary files /dev/null and b/analyze/snapshots/frame_00013.png differ diff --git a/analyze/snapshots/frame_00014.png b/analyze/snapshots/frame_00014.png new file mode 100644 index 0000000..227d405 Binary files /dev/null and b/analyze/snapshots/frame_00014.png differ diff --git a/analyze/snapshots/frame_00015.png b/analyze/snapshots/frame_00015.png new file mode 100644 index 0000000..34e7834 Binary files /dev/null and b/analyze/snapshots/frame_00015.png differ diff --git a/analyze/snapshots/frame_00016.png b/analyze/snapshots/frame_00016.png new file mode 100644 index 0000000..0e86930 Binary files /dev/null and b/analyze/snapshots/frame_00016.png differ diff --git a/analyze/snapshots2/frame_00000.png b/analyze/snapshots2/frame_00000.png new file mode 100644 index 0000000..697d2fe Binary files /dev/null and b/analyze/snapshots2/frame_00000.png differ diff --git a/analyze/snapshots2/frame_00001.png b/analyze/snapshots2/frame_00001.png new file mode 100644 index 0000000..026d8ae Binary files /dev/null and b/analyze/snapshots2/frame_00001.png differ diff --git a/analyze/snapshots2/frame_00002.png b/analyze/snapshots2/frame_00002.png new file mode 100644 index 0000000..99eaeed Binary files /dev/null and b/analyze/snapshots2/frame_00002.png differ diff --git a/analyze/snapshots2/frame_00003.png b/analyze/snapshots2/frame_00003.png new file mode 100644 index 0000000..4c01000 Binary files /dev/null and b/analyze/snapshots2/frame_00003.png differ diff --git a/analyze/snapshots2/frame_00004.png b/analyze/snapshots2/frame_00004.png new file mode 100644 index 0000000..75f4a43 Binary files /dev/null and b/analyze/snapshots2/frame_00004.png differ diff --git a/analyze/snapshots2/frame_00005.png b/analyze/snapshots2/frame_00005.png new file mode 100644 index 0000000..defa53d Binary files /dev/null and b/analyze/snapshots2/frame_00005.png differ diff --git a/analyze/snapshots2/frame_00006.png b/analyze/snapshots2/frame_00006.png new file mode 100644 index 0000000..afa37d7 Binary files /dev/null and b/analyze/snapshots2/frame_00006.png differ diff --git a/analyze/snapshots2/frame_00007.png b/analyze/snapshots2/frame_00007.png new file mode 100644 index 0000000..e54912d Binary files /dev/null and b/analyze/snapshots2/frame_00007.png differ diff --git a/analyze/snapshots2/frame_00008.png b/analyze/snapshots2/frame_00008.png new file mode 100644 index 0000000..b820c4d Binary files /dev/null and b/analyze/snapshots2/frame_00008.png differ diff --git a/analyze/snapshots2/frame_00009.png b/analyze/snapshots2/frame_00009.png new file mode 100644 index 0000000..05b1d87 Binary files /dev/null and b/analyze/snapshots2/frame_00009.png differ diff --git a/analyze/snapshots2/frame_00010.png b/analyze/snapshots2/frame_00010.png new file mode 100644 index 0000000..673020f Binary files /dev/null and b/analyze/snapshots2/frame_00010.png differ diff --git a/analyze/snapshots2/frame_00011.png b/analyze/snapshots2/frame_00011.png new file mode 100644 index 0000000..43e24ab Binary files /dev/null and b/analyze/snapshots2/frame_00011.png differ diff --git a/analyze/snapshots2/frame_00012.png b/analyze/snapshots2/frame_00012.png new file mode 100644 index 0000000..618809b Binary files /dev/null and b/analyze/snapshots2/frame_00012.png differ diff --git a/analyze/snapshots2/frame_00013.png b/analyze/snapshots2/frame_00013.png new file mode 100644 index 0000000..2965e64 Binary files /dev/null and b/analyze/snapshots2/frame_00013.png differ diff --git a/analyze/snapshots2/frame_00014.png b/analyze/snapshots2/frame_00014.png new file mode 100644 index 0000000..ab88075 Binary files /dev/null and b/analyze/snapshots2/frame_00014.png differ diff --git a/analyze/snapshots2/frame_00015.png b/analyze/snapshots2/frame_00015.png new file mode 100644 index 0000000..2aa18cf Binary files /dev/null and b/analyze/snapshots2/frame_00015.png differ diff --git a/analyze/snapshots2/frame_00016.png b/analyze/snapshots2/frame_00016.png new file mode 100644 index 0000000..570d402 Binary files /dev/null and b/analyze/snapshots2/frame_00016.png differ diff --git a/analyze/snapshots2/frame_00017.png b/analyze/snapshots2/frame_00017.png new file mode 100644 index 0000000..471b146 Binary files /dev/null and b/analyze/snapshots2/frame_00017.png differ diff --git a/analyze/snapshots2/frame_00018.png b/analyze/snapshots2/frame_00018.png new file mode 100644 index 0000000..6d8e12f Binary files /dev/null and b/analyze/snapshots2/frame_00018.png differ diff --git a/analyze/snapshots2/frame_00019.png b/analyze/snapshots2/frame_00019.png new file mode 100644 index 0000000..da7b757 Binary files /dev/null and b/analyze/snapshots2/frame_00019.png differ diff --git a/analyze/snapshots2/frame_00020.png b/analyze/snapshots2/frame_00020.png new file mode 100644 index 0000000..a55674b Binary files /dev/null and b/analyze/snapshots2/frame_00020.png differ diff --git a/analyze/snapshots2/frame_00021.png b/analyze/snapshots2/frame_00021.png new file mode 100644 index 0000000..b560dfb Binary files /dev/null and b/analyze/snapshots2/frame_00021.png differ diff --git a/analyze/snapshots2/frame_00022.png b/analyze/snapshots2/frame_00022.png new file mode 100644 index 0000000..b147dfa Binary files /dev/null and b/analyze/snapshots2/frame_00022.png differ diff --git a/analyze/snapshots2/frame_00023.png b/analyze/snapshots2/frame_00023.png new file mode 100644 index 0000000..d03d613 Binary files /dev/null and b/analyze/snapshots2/frame_00023.png differ diff --git a/analyze/snapshots2/frame_00024.png b/analyze/snapshots2/frame_00024.png new file mode 100644 index 0000000..accb47e Binary files /dev/null and b/analyze/snapshots2/frame_00024.png differ diff --git a/analyze/snapshots2/frame_00025.png b/analyze/snapshots2/frame_00025.png new file mode 100644 index 0000000..5e259e9 Binary files /dev/null and b/analyze/snapshots2/frame_00025.png differ diff --git a/analyze/snapshots2/frame_00026.png b/analyze/snapshots2/frame_00026.png new file mode 100644 index 0000000..b4a6efa Binary files /dev/null and b/analyze/snapshots2/frame_00026.png differ diff --git a/analyze/snapshots2/frame_00027.png b/analyze/snapshots2/frame_00027.png new file mode 100644 index 0000000..082549f Binary files /dev/null and b/analyze/snapshots2/frame_00027.png differ diff --git a/analyze/snapshots2/frame_00028.png b/analyze/snapshots2/frame_00028.png new file mode 100644 index 0000000..9f8a10a Binary files /dev/null and b/analyze/snapshots2/frame_00028.png differ diff --git a/analyze/snapshots2/frame_00029.png b/analyze/snapshots2/frame_00029.png new file mode 100644 index 0000000..ff26db2 Binary files /dev/null and b/analyze/snapshots2/frame_00029.png differ diff --git a/analyze/snapshots2/frame_00030.png b/analyze/snapshots2/frame_00030.png new file mode 100644 index 0000000..ece919b Binary files /dev/null and b/analyze/snapshots2/frame_00030.png differ diff --git a/analyze/snapshots2/frame_00031.png b/analyze/snapshots2/frame_00031.png new file mode 100644 index 0000000..e9c1ba0 Binary files /dev/null and b/analyze/snapshots2/frame_00031.png differ diff --git a/analyze/snapshots2/frame_00032.png b/analyze/snapshots2/frame_00032.png new file mode 100644 index 0000000..a0130b5 Binary files /dev/null and b/analyze/snapshots2/frame_00032.png differ diff --git a/analyze/snapshots2/frame_00033.png b/analyze/snapshots2/frame_00033.png new file mode 100644 index 0000000..c42b31e Binary files /dev/null and b/analyze/snapshots2/frame_00033.png differ diff --git a/analyze/snapshots2/frame_00034.png b/analyze/snapshots2/frame_00034.png new file mode 100644 index 0000000..19853ac Binary files /dev/null and b/analyze/snapshots2/frame_00034.png differ diff --git a/analyze/snapshots2/frame_00035.png b/analyze/snapshots2/frame_00035.png new file mode 100644 index 0000000..6aceab3 Binary files /dev/null and b/analyze/snapshots2/frame_00035.png differ diff --git a/analyze/snapshots2/frame_00036.png b/analyze/snapshots2/frame_00036.png new file mode 100644 index 0000000..8ee4566 Binary files /dev/null and b/analyze/snapshots2/frame_00036.png differ diff --git a/tests/__pycache__/test_bars.cpython-314-pytest-9.1.1.pyc b/analyze/tests/__pycache__/test_bars.cpython-314-pytest-9.1.1.pyc similarity index 100% rename from tests/__pycache__/test_bars.cpython-314-pytest-9.1.1.pyc rename to analyze/tests/__pycache__/test_bars.cpython-314-pytest-9.1.1.pyc diff --git a/tests/__pycache__/test_corrupt.cpython-314-pytest-9.1.1.pyc b/analyze/tests/__pycache__/test_corrupt.cpython-314-pytest-9.1.1.pyc similarity index 100% rename from tests/__pycache__/test_corrupt.cpython-314-pytest-9.1.1.pyc rename to analyze/tests/__pycache__/test_corrupt.cpython-314-pytest-9.1.1.pyc diff --git a/tests/__pycache__/test_harvesting.cpython-314-pytest-9.1.1.pyc b/analyze/tests/__pycache__/test_harvesting.cpython-314-pytest-9.1.1.pyc similarity index 100% rename from tests/__pycache__/test_harvesting.cpython-314-pytest-9.1.1.pyc rename to analyze/tests/__pycache__/test_harvesting.cpython-314-pytest-9.1.1.pyc diff --git a/analyze/tests/__pycache__/test_windows.cpython-314-pytest-9.1.1.pyc b/analyze/tests/__pycache__/test_windows.cpython-314-pytest-9.1.1.pyc new file mode 100644 index 0000000..47b6722 Binary files /dev/null and b/analyze/tests/__pycache__/test_windows.cpython-314-pytest-9.1.1.pyc differ diff --git a/analyze/tests/__pycache__/test_windows.cpython-314.pyc b/analyze/tests/__pycache__/test_windows.cpython-314.pyc new file mode 100644 index 0000000..1a74dfc Binary files /dev/null and b/analyze/tests/__pycache__/test_windows.cpython-314.pyc differ diff --git a/tests/images/corrupt/frame_00014.png b/analyze/tests/images/corrupt/frame_00014.png similarity index 100% rename from tests/images/corrupt/frame_00014.png rename to analyze/tests/images/corrupt/frame_00014.png diff --git a/analyze/tests/images/corrupt/snapshots_frame_00016.png b/analyze/tests/images/corrupt/snapshots_frame_00016.png new file mode 100644 index 0000000..0e86930 Binary files /dev/null and b/analyze/tests/images/corrupt/snapshots_frame_00016.png differ diff --git a/tests/images/harvesting/frame_00000.png b/analyze/tests/images/harvesting/frame_00000.png similarity index 100% rename from tests/images/harvesting/frame_00000.png rename to analyze/tests/images/harvesting/frame_00000.png diff --git a/tests/images/harvesting/frame_00001.png b/analyze/tests/images/harvesting/frame_00001.png similarity index 100% rename from tests/images/harvesting/frame_00001.png rename to analyze/tests/images/harvesting/frame_00001.png diff --git a/tests/images/harvesting/frame_00002.png b/analyze/tests/images/harvesting/frame_00002.png similarity index 100% rename from tests/images/harvesting/frame_00002.png rename to analyze/tests/images/harvesting/frame_00002.png diff --git a/tests/images/harvesting/frame_00003.png b/analyze/tests/images/harvesting/frame_00003.png similarity index 100% rename from tests/images/harvesting/frame_00003.png rename to analyze/tests/images/harvesting/frame_00003.png diff --git a/tests/images/harvesting/frame_00004.png b/analyze/tests/images/harvesting/frame_00004.png similarity index 100% rename from tests/images/harvesting/frame_00004.png rename to analyze/tests/images/harvesting/frame_00004.png diff --git a/tests/images/harvesting/frame_00011.png b/analyze/tests/images/harvesting/frame_00011.png similarity index 100% rename from tests/images/harvesting/frame_00011.png rename to analyze/tests/images/harvesting/frame_00011.png diff --git a/tests/images/harvesting/frame_00012.png b/analyze/tests/images/harvesting/frame_00012.png similarity index 100% rename from tests/images/harvesting/frame_00012.png rename to analyze/tests/images/harvesting/frame_00012.png diff --git a/tests/images/harvesting/frame_00013.png b/analyze/tests/images/harvesting/frame_00013.png similarity index 100% rename from tests/images/harvesting/frame_00013.png rename to analyze/tests/images/harvesting/frame_00013.png diff --git a/analyze/tests/images/inventory/snapshots2_f00003.png b/analyze/tests/images/inventory/snapshots2_f00003.png new file mode 100644 index 0000000..4c01000 Binary files /dev/null and b/analyze/tests/images/inventory/snapshots2_f00003.png differ diff --git a/analyze/tests/images/inventory/snapshots2_f00033.png b/analyze/tests/images/inventory/snapshots2_f00033.png new file mode 100644 index 0000000..c42b31e Binary files /dev/null and b/analyze/tests/images/inventory/snapshots2_f00033.png differ diff --git a/analyze/tests/images/inventory/snapshots_f00000.png b/analyze/tests/images/inventory/snapshots_f00000.png new file mode 100644 index 0000000..6a1774f Binary files /dev/null and b/analyze/tests/images/inventory/snapshots_f00000.png differ diff --git a/analyze/tests/images/inventory/snapshots_f00006.png b/analyze/tests/images/inventory/snapshots_f00006.png new file mode 100644 index 0000000..38e039c Binary files /dev/null and b/analyze/tests/images/inventory/snapshots_f00006.png differ diff --git a/analyze/tests/images/manufacturing/failed/snapshots2_f00007.png b/analyze/tests/images/manufacturing/failed/snapshots2_f00007.png new file mode 100644 index 0000000..e54912d Binary files /dev/null and b/analyze/tests/images/manufacturing/failed/snapshots2_f00007.png differ diff --git a/analyze/tests/images/manufacturing/failed/snapshots_f00013.png b/analyze/tests/images/manufacturing/failed/snapshots_f00013.png new file mode 100644 index 0000000..314f27e Binary files /dev/null and b/analyze/tests/images/manufacturing/failed/snapshots_f00013.png differ diff --git a/analyze/tests/images/manufacturing/failed_lost/snapshots2_f00013.png b/analyze/tests/images/manufacturing/failed_lost/snapshots2_f00013.png new file mode 100644 index 0000000..2965e64 Binary files /dev/null and b/analyze/tests/images/manufacturing/failed_lost/snapshots2_f00013.png differ diff --git a/analyze/tests/images/manufacturing/failed_lost/snapshots_f00007.png b/analyze/tests/images/manufacturing/failed_lost/snapshots_f00007.png new file mode 100644 index 0000000..53b42de Binary files /dev/null and b/analyze/tests/images/manufacturing/failed_lost/snapshots_f00007.png differ diff --git a/analyze/tests/images/manufacturing/hungry/snapshots2_f00033.png b/analyze/tests/images/manufacturing/hungry/snapshots2_f00033.png new file mode 100644 index 0000000..c42b31e Binary files /dev/null and b/analyze/tests/images/manufacturing/hungry/snapshots2_f00033.png differ diff --git a/analyze/tests/images/manufacturing/none/snapshots2_f00000.png b/analyze/tests/images/manufacturing/none/snapshots2_f00000.png new file mode 100644 index 0000000..697d2fe Binary files /dev/null and b/analyze/tests/images/manufacturing/none/snapshots2_f00000.png differ diff --git a/analyze/tests/images/manufacturing/none/snapshots2_f00001.png b/analyze/tests/images/manufacturing/none/snapshots2_f00001.png new file mode 100644 index 0000000..026d8ae Binary files /dev/null and b/analyze/tests/images/manufacturing/none/snapshots2_f00001.png differ diff --git a/analyze/tests/images/manufacturing/overloaded/snapshots_f00003.png b/analyze/tests/images/manufacturing/overloaded/snapshots_f00003.png new file mode 100644 index 0000000..8e5ec1f Binary files /dev/null and b/analyze/tests/images/manufacturing/overloaded/snapshots_f00003.png differ diff --git a/analyze/tests/images/manufacturing/stopped/harvest_frame_00000.png b/analyze/tests/images/manufacturing/stopped/harvest_frame_00000.png new file mode 100644 index 0000000..1c6dc67 Binary files /dev/null and b/analyze/tests/images/manufacturing/stopped/harvest_frame_00000.png differ diff --git a/analyze/tests/images/manufacturing/success/snapshots2_f00003.png b/analyze/tests/images/manufacturing/success/snapshots2_f00003.png new file mode 100644 index 0000000..4c01000 Binary files /dev/null and b/analyze/tests/images/manufacturing/success/snapshots2_f00003.png differ diff --git a/analyze/tests/images/manufacturing/success/snapshots_f00000.png b/analyze/tests/images/manufacturing/success/snapshots_f00000.png new file mode 100644 index 0000000..6a1774f Binary files /dev/null and b/analyze/tests/images/manufacturing/success/snapshots_f00000.png differ diff --git a/analyze/tests/images/manufacturing/success/snapshots_f00006.png b/analyze/tests/images/manufacturing/success/snapshots_f00006.png new file mode 100644 index 0000000..38e039c Binary files /dev/null and b/analyze/tests/images/manufacturing/success/snapshots_f00006.png differ diff --git a/analyze/tests/images/manufacturing/success/snapshots_f00010.png b/analyze/tests/images/manufacturing/success/snapshots_f00010.png new file mode 100644 index 0000000..54f89fa Binary files /dev/null and b/analyze/tests/images/manufacturing/success/snapshots_f00010.png differ diff --git a/analyze/tests/images/manufacturing/success/snapshots_f00015.png b/analyze/tests/images/manufacturing/success/snapshots_f00015.png new file mode 100644 index 0000000..34e7834 Binary files /dev/null and b/analyze/tests/images/manufacturing/success/snapshots_f00015.png differ diff --git a/analyze/tests/images/manufacturing/working/snapshots2_f00002.png b/analyze/tests/images/manufacturing/working/snapshots2_f00002.png new file mode 100644 index 0000000..99eaeed Binary files /dev/null and b/analyze/tests/images/manufacturing/working/snapshots2_f00002.png differ diff --git a/analyze/tests/images/manufacturing/working/snapshots_f00002.png b/analyze/tests/images/manufacturing/working/snapshots_f00002.png new file mode 100644 index 0000000..6c3a974 Binary files /dev/null and b/analyze/tests/images/manufacturing/working/snapshots_f00002.png differ diff --git a/tests/images/not_harvesting/frame_00005.png b/analyze/tests/images/not_harvesting/frame_00005.png similarity index 100% rename from tests/images/not_harvesting/frame_00005.png rename to analyze/tests/images/not_harvesting/frame_00005.png diff --git a/tests/images/not_harvesting/frame_00006.png b/analyze/tests/images/not_harvesting/frame_00006.png similarity index 100% rename from tests/images/not_harvesting/frame_00006.png rename to analyze/tests/images/not_harvesting/frame_00006.png diff --git a/tests/images/not_harvesting/frame_00007.png b/analyze/tests/images/not_harvesting/frame_00007.png similarity index 100% rename from tests/images/not_harvesting/frame_00007.png rename to analyze/tests/images/not_harvesting/frame_00007.png diff --git a/tests/images/not_harvesting/frame_00008.png b/analyze/tests/images/not_harvesting/frame_00008.png similarity index 100% rename from tests/images/not_harvesting/frame_00008.png rename to analyze/tests/images/not_harvesting/frame_00008.png diff --git a/tests/images/not_harvesting/frame_00009.png b/analyze/tests/images/not_harvesting/frame_00009.png similarity index 100% rename from tests/images/not_harvesting/frame_00009.png rename to analyze/tests/images/not_harvesting/frame_00009.png diff --git a/tests/images/not_harvesting/frame_00010.png b/analyze/tests/images/not_harvesting/frame_00010.png similarity index 100% rename from tests/images/not_harvesting/frame_00010.png rename to analyze/tests/images/not_harvesting/frame_00010.png diff --git a/tests/test_bars.py b/analyze/tests/test_bars.py similarity index 100% rename from tests/test_bars.py rename to analyze/tests/test_bars.py diff --git a/tests/test_corrupt.py b/analyze/tests/test_corrupt.py similarity index 100% rename from tests/test_corrupt.py rename to analyze/tests/test_corrupt.py diff --git a/tests/test_harvesting.py b/analyze/tests/test_harvesting.py similarity index 100% rename from tests/test_harvesting.py rename to analyze/tests/test_harvesting.py diff --git a/analyze/tests/test_windows.py b/analyze/tests/test_windows.py new file mode 100644 index 0000000..6ce8ab9 --- /dev/null +++ b/analyze/tests/test_windows.py @@ -0,0 +1,139 @@ +import shutil +from pathlib import Path + +import pytest + +import analyze + +MFG_IMAGES = sorted(Path("tests/images/manufacturing").glob("*/*.png")) +INV_IMAGES = sorted(Path("tests/images/inventory").glob("*.png")) + +assert len(MFG_IMAGES) > 0, "no images found under tests/images/manufacturing" +assert len(INV_IMAGES) > 0, "no images found under tests/images/inventory" + +OCR_AVAILABLE = analyze.pytesseract is not None and shutil.which("tesseract") is not None + +pytestmark = pytest.mark.skipif( + not OCR_AVAILABLE, + reason="pytesseract or tesseract-ocr not installed", +) + +# Pinned Manufacturing title-bar positions per fixture frame, measured from the +# real captures (the window was dragged to 5 different places across sessions). +MFG_POSITION = { + "snapshots_f00000.png": (488, 480), + "snapshots_f00002.png": (488, 480), + "snapshots_f00003.png": (488, 480), + "snapshots_f00006.png": (376, 262), + "snapshots_f00007.png": (1330, 194), + "snapshots_f00010.png": (1005, 565), + "snapshots_f00013.png": (281, 676), + "snapshots_f00015.png": (281, 676), + "snapshots2_f00000.png": (1104, 327), + "snapshots2_f00001.png": (1104, 327), + "snapshots2_f00002.png": (1104, 327), + "snapshots2_f00003.png": (1104, 327), + "snapshots2_f00007.png": (1104, 327), + "snapshots2_f00013.png": (1104, 327), + "snapshots2_f00033.png": (1104, 337), + "harvest_frame_00000.png": (173, 771), +} + +# The Inventory window never moved across the capture sessions; every fixture +# frame pins the same title-bar position. +INV_POSITION = (1629, 686) + +# find_windows recovery is a full-frame OCR scan (~7s/frame) so it is only +# exercised on a representative subset. "inventory": None for the harvesting +# frame is a known miss: the full-frame scan does not read its Inventory title. +RECOVERY_CASES = { + "snapshots_f00000.png": ((488, 480), (1629, 686)), + "snapshots_f00013.png": ((281, 676), (1629, 686)), + "snapshots2_f00003.png": ((1104, 327), (1629, 686)), + "snapshots2_f00013.png": ((1104, 327), (1629, 686)), + "snapshots2_f00033.png": ((1104, 337), (1629, 686)), + "harvest_frame_00000.png": ((173, 771), None), +} + +_TOLERANCE = 15 + + +def _load(path): + image = analyze.load_image(path) + assert image is not None, f"cannot load {path}" + return image + + +def _find_fixture(name): + for root in list(Path("tests/images/manufacturing").glob("*")) + [Path("tests/images/inventory")]: + candidate = root / name + if candidate.exists(): + return candidate + raise AssertionError(f"no fixture file named {name}") + + +def _mfg_positions(): + assert set(MFG_POSITION) == {p.name for p in MFG_IMAGES}, "MFG_POSITION does not match fixture files" + for path in MFG_IMAGES: + yield path, MFG_POSITION[path.name] + + +def test_manufacturing_title_is_verified_at_pinned_position(): + for path, pos in _mfg_positions(): + image = _load(path) + assert analyze.verify_window(image, pos[0], pos[1], "manufacturing") is True, path + + +def test_inventory_title_is_verified_at_pinned_position(): + for path in INV_IMAGES: + image = _load(path) + assert analyze.verify_window(image, INV_POSITION[0], INV_POSITION[1], "inventory") is True, path + + +def test_manufacturing_message_category_matches_folder(): + for path in MFG_IMAGES: + image = _load(path) + x, y = MFG_POSITION[path.name] + message = analyze.read_manufacturing_message(image, x, y) + assert message is not None, path + assert message["category"] == path.parent.name, path + + +def test_find_windows_recovers_pinned_positions(): + for name, (exp_mfg, exp_inv) in RECOVERY_CASES.items(): + image = _load(_find_fixture(name)) + found = analyze.find_windows(image) + if exp_mfg is None: + assert found["manufacturing"] is None, name + else: + pos = found["manufacturing"] + assert pos is not None, f"manufacturing not recovered: {name}" + assert abs(pos[0] - exp_mfg[0]) <= _TOLERANCE, name + assert abs(pos[1] - exp_mfg[1]) <= _TOLERANCE, name + if exp_inv is None: + assert found["inventory"] is None, name + else: + pos = found["inventory"] + assert pos is not None, f"inventory not recovered: {name}" + assert abs(pos[0] - exp_inv[0]) <= _TOLERANCE, name + assert abs(pos[1] - exp_inv[1]) <= _TOLERANCE, name + + +def test_classify_message_maps_real_strings(): + # Ground-truth texts from the captured frames, one per category. + assert analyze.classify_message("you stopped working.", "red") == "stopped" + assert analyze.classify_message("you started working...", "green") == "working" + assert analyze.classify_message("you successfully created 1 thread", "green") == "success" + assert analyze.classify_message("you failed to create a[n] thread", "red") == "failed" + assert ( + analyze.classify_message( + "you failed to create a[n] potion of minor healing, and lost the ingredients", "red" + ) + == "failed_lost" + ) + assert analyze.classify_message( + "you are hungry, you can't manufacture anything. eat until your food level is over 0", "red" + ) == "hungry" + assert analyze.classify_message("you are overloaded, you can't carry 1 thread!", "red") == "overloaded" + assert analyze.classify_message("", "green") == "none" + assert analyze.classify_message("garbled text", "red") == "unknown"