Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to BOT-MMORPG-AI will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- Recordings no longer disappear from the Train tab: the bundled
`versions/0.01/1-collect_data.py` now honors the `--out` argument the
Tauri shell passes, so data lands in `datasets/<game>/<name>/` where
the dataset scanner looks (issues #57, #60, #63, #65).
- Training no longer crashes with
`Target size (... 39) must be the same as input size (... 29)` when
mouse recording is enabled. `2-train_model.py` now auto-detects the
output-head size from the dataset's action vector (29 without mouse,
39 with), and `--num-actions` defaults to `0` (auto) (issue #64).

## [1.0.0] - 2026-02-09

### Added
Expand Down
32 changes: 32 additions & 0 deletions docs/installer/04-bug-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,38 @@ The PowerShell driver script computes the wrong driver path under PROD.
not `$INSTDIR\resources\drivers\`).
- **See:** `05-case-studies.md` Bug #5 (`9d8a16f`).

### "Recording saved" but no dataset appears in the Train tab

The Rust shell spawns `1-collect_data.py` with
`--out <data_root>/datasets/<gid>/<name>` so the recording lands where
`modelhub/tauri.py::_scan_datasets_fs` looks. The **legacy**
`versions/0.01/1-collect_data.py` had no argument parser, so it ignored
`--out` entirely and wrote to a bare `datasets/` folder (driven only by
`BOTMMO_OUTPUT_DIR`). Result: recording "succeeds" but the file lands
outside `datasets/<gid>/` and never shows up in the UI.

- **File:** `versions/0.01/1-collect_data.py` (`_resolve_output_dir`)
- **Fix:** parse `--out` (with `parse_known_args` so future flags don't
crash the recorder) and use it as the output dir, falling back to
`BOTMMO_OUTPUT_DIR` then the default. Keeps the writer, the session
bookkeeping, and `_scan_datasets_fs` all pointed at the same path.
- **Issues:** #57, #60, #63, #65.

### Training crash: `Target size (... 39) must be the same as input size (... 29)`

`BCEWithLogitsLoss` failed in `2-train_model.py` because the dataset's
action vector was 39 wide (29 keyboard+gamepad **plus** 10 mouse values
when mouse recording is on) while the model's output head was hard-sized
to 29 via the `--num-actions` default.

- **File:** `versions/0.01/2-train_model.py` (`GameplayDataset.num_actions`,
`main()` head-size resolution)
- **Fix:** auto-detect the head size from the dataset
(`self.actions.shape[1]`) and default `--num-actions` to `0` (auto). A
positive `--num-actions` still overrides for advanced use. The model
head now always matches the recorded action width (29 or 39).
- **Issue:** #64.

## UI-layer symptoms (frontend can't reach the backend)

### Notification "Dismiss" / "×" / "Later" buttons appear inert
Expand Down
36 changes: 32 additions & 4 deletions versions/0.01/1-collect_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import time
import signal
import argparse
from pathlib import Path

import cv2
Expand Down Expand Up @@ -188,8 +189,35 @@ def save_chunk(path: Path, data: list) -> None:
print(f"[SAVED] {path} ({len(data)} samples)")


def main():
out_path, idx = next_available_file(OUTPUT_DIR, FILE_PREFIX)
def _resolve_output_dir(argv=None) -> Path:
"""Decide where to write the .npy chunks.

Priority (highest first):
1. ``--out PATH`` on the command line. The Tauri shell spawns us
with ``--out <data_root>/datasets/<gid>/<name>`` so the
recording lands exactly where the Train tab's scanner
(``modelhub/tauri.py::_scan_datasets_fs``) looks. Before this
was honored, the flag was silently ignored and recordings
landed in a bare ``datasets/`` folder that never showed up in
the UI -- the "dataset not appearing after recording" bug
(issues #57, #60, #63, #65).
2. ``BOTMMO_OUTPUT_DIR`` environment variable (legacy launcher).
3. The default ``datasets/`` relative to the working directory.

Unknown args are tolerated (parse_known_args) so a future caller can
pass extra flags without crashing the recorder mid-launch.
"""
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--out", "--output", dest="out", default=None)
args, _unknown = parser.parse_known_args(argv)
if args.out:
return Path(args.out)
return OUTPUT_DIR


def main(argv=None):
output_dir = _resolve_output_dir(argv)
out_path, idx = next_available_file(output_dir, FILE_PREFIX)

# Mouse recording support (issues #21, #33, #36)
mouse_enabled = os.getenv("BOTMMO_CAPTURE_MOUSE", "").lower() == "true"
Expand All @@ -202,7 +230,7 @@ def main():
print(f"[Collector] CaptureRegion: {CAPTURE_REGION}")
print(f"[Collector] Resolution : {RESOLUTION}")
print(f"[Collector] Mouse Record : {'ENABLED' if mouse_enabled else 'DISABLED'}")
print(f"[Collector] Output Dir : {OUTPUT_DIR.resolve()}")
print(f"[Collector] Output Dir : {output_dir.resolve()}")
print(f"[Collector] Output File : {out_path.name}")
print("=================================================")
if not mouse_enabled:
Expand Down Expand Up @@ -346,7 +374,7 @@ def _handle_sigint(signum, frame):
save_chunk(out_path, training_data)
training_data = []
idx += 1
out_path = OUTPUT_DIR / f"{FILE_PREFIX}-{idx}.npy"
out_path = output_dir / f"{FILE_PREFIX}-{idx}.npy"

finally:
# Save remainder so stopping from launcher doesn't lose data
Expand Down
32 changes: 30 additions & 2 deletions versions/0.01/2-train_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,23 @@ def _load_data(self):
self.frames = np.array(all_frames)
self.actions = np.array(all_actions, dtype=np.float32)

# Auto-detected width of the action vector. This is the single
# source of truth for sizing the model's output head. When mouse
# recording is enabled, collect_data appends 10 extra values
# (x, y, dx, dy, vx, vy, lmb, rmb, mmb, scroll) so the vector is
# 39 wide instead of 29 (keyboard=9 + gamepad=20). The trainer
# must size the head to match or BCEWithLogitsLoss raises
# "Target size ... must be the same as input size" (issue #64).
self.num_actions = (
self.actions.shape[1] if self.actions.ndim == 2 else len(self.actions[0])
)

dt = time.time() - t0
LOG.info(f"[Dataset] Loaded {len(self.frames)} sample(s) in {dt:.1f}s")
LOG.info(f"[Dataset] Skipped files: {skipped_files}, skipped items: {skipped_items}")
LOG.info(f"[Dataset] Frame shape: {self.frames[0].shape}")
LOG.info(f"[Dataset] Action shape: {self.actions[0].shape}")
LOG.info(f"[Dataset] Auto-detected num_actions: {self.num_actions}")

def __len__(self) -> int:
return max(0, len(self.frames) - self.seq_len + 1)
Expand Down Expand Up @@ -430,7 +442,12 @@ def main(argv=None) -> int:
)
parser.add_argument("--lr", type=float, default=1e-4, help="Learning rate")
parser.add_argument("--seq-len", type=int, default=4, help="Sequence length for temporal models")
parser.add_argument("--num-actions", type=int, default=29, help="Number of output actions")
parser.add_argument(
"--num-actions",
type=int,
default=0,
help="Number of output actions (0 = auto-detect from data; issue #64)",
)
parser.add_argument("--val-split", type=float, default=0.1, help="Validation split ratio")
parser.add_argument("--no-pretrained", action="store_true", help="Don't use pretrained weights")
parser.add_argument("--cpu", action="store_true", help="Force CPU training")
Expand Down Expand Up @@ -537,11 +554,22 @@ def main(argv=None) -> int:
if val_size > 0 else None
)

# Resolve the output-head size. Prefer the value auto-detected from
# the dataset so the model head always matches the recorded action
# vector (29 keyboard+gamepad, or 39 when mouse recording is on).
# A positive --num-actions overrides it for advanced use. This fixes
# the "Target size ... must be the same as input size" crash (#64).
num_actions = args.num_actions if args.num_actions > 0 else dataset.num_actions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist the auto-detected action width

When this auto-detects 39 actions for a mouse-recorded dataset, training can now finish but the saved checkpoints still do not record that width; models_pytorch.load_model() defaults num_actions back to 29, and 3-test_model.py calls it without an override, so loading the resulting 39-output state dict fails with size mismatches before inference can run. Please save num_actions as checkpoint metadata and have the loader use it, otherwise every mouse-enabled model produced by this path is unusable by the bundled tester.

Useful? React with 👍 / 👎.

LOG.info(f"Action vector size: {num_actions}")
if num_actions > 29:
mouse_extra = num_actions - 29
LOG.info(f" (keyboard=9 + gamepad=20 + mouse={mouse_extra})")

# Create model
LOG.info(f"Creating model: {args.model}")
model = get_model(
args.model,
num_actions=args.num_actions,
num_actions=num_actions,
temporal_frames=seq_len,
pretrained=not args.no_pretrained,
)
Expand Down
Loading