Working harvesting detection, working manufacturing detection.

This commit is contained in:
2026-08-12 13:54:40 -06:00
parent 2181655801
commit d7755df2e8
112 changed files with 1105 additions and 177 deletions
Binary file not shown.
-46
View File
@@ -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}}
-127
View File
@@ -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
+144
View File
@@ -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/<category>/*.png` → the craft message read at
the pinned title position must classify to `<category>`
- `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.
Binary file not shown.
Binary file not shown.
+256
View File
@@ -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}
+298
View File
@@ -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
View File
+213
View File
@@ -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
<category>/*.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.
+55 -4
View File
@@ -15,14 +15,59 @@ import analyze
from capture import WindowCapture, WindowNotFound 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 global _last_t
info = analyze.summarize(image) info = analyze.summarize(image)
harvest = analyze.is_harvesting(image) harvest = analyze.is_harvesting(image)
bars = analyze.read_bar_values(image) bars = analyze.read_bar_values(image)
windows = tracker.update(image)
fps = 1.0 / max(1e-6, time.monotonic() - _last_t) fps = 1.0 / max(1e-6, time.monotonic() - _last_t)
_last_t = time.monotonic() _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 = { record = {
"time": _now(), "time": _now(),
"frame": frame, "frame": frame,
@@ -30,6 +75,9 @@ def analyze_and_print(image, frame, prev, logf):
"harvest_pixel": harvest["color"], "harvest_pixel": harvest["color"],
"harvest_luma": harvest["luma"], "harvest_luma": harvest["luma"],
"bars": bars, "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: if logf is not None:
logf.write(json.dumps(record) + "\n") 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]) top = ", ".join(f"{c['hex']} {c['fraction'] * 100:.0f}%" for c in info["dominant"][:3])
status = "harvesting" if harvest["is_harvesting"] else "not harvesting" status = "harvesting" if harvest["is_harvesting"] else "not harvesting"
frame_str = f"{frame:05d}" if isinstance(frame, int) else frame 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( print(
f"[{frame_str}] {status} luma={harvest['luma']} " f"[{frame_str}] {status} luma={harvest['luma']} "
f"size={info['size'][0]}x{info['size'][1]} " f"size={info['size'][0]}x{info['size'][1]} "
f"mean={info['mean_rgb']} top=[{top}] bars={bars} " 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, flush=True,
) )
return small return small
@@ -55,6 +104,7 @@ def run_replay(args):
"""Process PNGs from a directory instead of a live window.""" """Process PNGs from a directory instead of a live window."""
logf = open(args.log, "a") if args.log else None logf = open(args.log, "a") if args.log else None
prev = None prev = None
tracker = WindowTracker()
try: try:
for name in sorted(os.listdir(args.replay)): for name in sorted(os.listdir(args.replay)):
if not name.lower().endswith(".png"): if not name.lower().endswith(".png"):
@@ -64,7 +114,7 @@ def run_replay(args):
if image is None: if image is None:
print(f"[skipped] unreadable image: {path}", flush=True) print(f"[skipped] unreadable image: {path}", flush=True)
continue continue
prev = analyze_and_print(image, name, prev, logf) prev = analyze_and_print(image, name, prev, logf, tracker)
finally: finally:
if logf: if logf:
logf.close() 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) 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 logf = open(args.log, "a") if args.log else None
prev = None prev = None
tracker = WindowTracker()
frame = 0 frame = 0
try: try:
while True: while True:
@@ -88,7 +139,7 @@ def run_live(args):
print(f"[{_now()}] {exc}; retrying...", flush=True) print(f"[{_now()}] {exc}; retrying...", flush=True)
time.sleep(1.0) time.sleep(1.0)
continue 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: if args.save_every and frame % args.save_every == 0:
path = os.path.join(args.snapshots, f"frame_{frame:05d}.png") path = os.path.join(args.snapshots, f"frame_{frame:05d}.png")
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Some files were not shown because too many files have changed in this diff Show More