Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ OPENROUTER_API_KEY=
# --- RIMAPI (RimWorld mod) ---
RIMAPI_URL=http://localhost:8765

# Run-summary pin (path + sha256 + fork commit). Workshop is not source of truth.
# AppSprout: point both at the compiled AppSprout-dev/RIMAPI checkout (rle-testing).
# Unset: probe $RIMAPI_FORK_PATH Assemblies, then ../RIMAPI next to the RLE git
# toplevel, then the Steam Workshop path as a last-resort OSS fallback.
# RIMAPI_DLL_PATH=/path/to/RIMAPI/1.6/Assemblies/RIMAPI.dll
# RIMAPI_FORK_PATH=/path/to/RIMAPI

# --- MCP HTTP host (coding-agent harnesses: grok-build, opencode, ...) ---
# Default: bind 127.0.0.1, ephemeral port, advertise http://127.0.0.1:<port>/mcp
# Container-reachable (agent in Docker, RimWorld/RLE on the host — NOT --docker):
Expand Down
18 changes: 16 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,25 @@ git checkout rle-testing
cd Source/RIMAPI
dotnet build RimApi.csproj -c Release-1.6

# Deploy DLL over Workshop install (close RimWorld first!)
# Deploy DLL over Workshop install (close RimWorld first!) so the *game* loads the fork.
cp ../../1.6/Assemblies/RIMAPI.dll \
"C:/Steam/steamapps/workshop/content/294100/3593423732/1.6/Assemblies/RIMAPI.dll"
```

The upstream Workshop DLL is backed up as `RIMAPI.dll.upstream-backup` in the same folder.

**Pin (source of truth):** AppSprout runs hash the compiled checkout, not Workshop.
Set both in `.env` after the build (Workshop remains a last-resort metadata fallback only):

```bash
RIMAPI_DLL_PATH=/path/to/RIMAPI/1.6/Assemblies/RIMAPI.dll
RIMAPI_FORK_PATH=/path/to/RIMAPI
```

`collect_metadata()` records `rimapi_dll_path`, `rimapi_dll_sha256`, and
`rimapi_fork_commit`. Probe order when env is unset: fork Assemblies (`1.6` then
`1.5`) → sibling `../RIMAPI` next to the RLE git toplevel → Workshop path.

### RIMAPI gotchas

- RIMAPI only starts serving **after the map loads** (not on the main menu)
Expand Down Expand Up @@ -81,6 +93,8 @@ The `.env` file controls which LLM provider is used. Key fields:
| `MODEL` | Model name as the provider expects it | `unsloth/nvidia-nemotron-3-nano-4b` |
| `PROVIDER_BASE_URL` | API base URL (required for LM Studio and OpenRouter) | `http://localhost:1234/v1` |
| `RIMAPI_URL` | RIMAPI mod URL | `http://localhost:8765` |
| `RIMAPI_DLL_PATH` | Compiled `RIMAPI.dll` to pin in run summaries (AppSprout SoT; not Workshop) | `/path/to/RIMAPI/1.6/Assemblies/RIMAPI.dll` |
| `RIMAPI_FORK_PATH` | AppSprout-dev/RIMAPI checkout root (`rle-testing`) for `rimapi_fork_commit` | `/path/to/RIMAPI` |
| `MCP_CONTAINER_REACHABLE` | Bind MCP on `0.0.0.0:8766`, advertise `http://host.docker.internal:8766/mcp` for Docker agents (host RimWorld). Not `--docker`. | `true` |
| `MCP_BIND_HOST` / `MCP_ADVERTISE_HOST` / `MCP_PORT` | Optional MCP listen overrides (also `--harness-opt mcp_*`) | `0.0.0.0` / `host.docker.internal` / `8766` |

Expand Down Expand Up @@ -466,7 +480,7 @@ docker/

We contribute upstream to IlyaChichkov/RIMAPI. PRs #52-54, #60, #63, #65 all merged.

The `rle-testing` branch tracks upstream develop. We always build from `rle-testing` and deploy the DLL to the Workshop folder — this is our active development workflow.
The `rle-testing` branch tracks upstream develop. We always **build** from `rle-testing`. Copying that DLL into the Workshop folder is how RimWorld loads the fork; **Workshop is not source of truth** for run metadata. Pin `RIMAPI_DLL_PATH` + `RIMAPI_FORK_PATH` at the compiled checkout so summaries record that path, sha256, and fork commit.

To restore the original Workshop DLL: rename `RIMAPI.dll.upstream-backup` back to `RIMAPI.dll` in `C:\Steam\steamapps\workshop\content\294100\3593423732\1.6\Assemblies\`.
```
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ python scripts/check_harness_boundary.py

`--smoke-test` always uses mock LLMs. To exercise a real model against the fake game state, run a real harness with `--ticks` small against a live RIMAPI instead (below).

### RIMAPI DLL pin (run metadata)

Live-run summaries record `rimapi_dll_path`, `rimapi_dll_sha256`, and
`rimapi_fork_commit`. **Steam Workshop is not the source of truth.** AppSprout
runs should pin the compiled [AppSprout-dev/RIMAPI](https://github.com/AppSprout-dev/RIMAPI)
checkout (`rle-testing`):

```bash
# .env — compiled Assemblies, not the Workshop copy
RIMAPI_DLL_PATH=/path/to/RIMAPI/1.6/Assemblies/RIMAPI.dll
RIMAPI_FORK_PATH=/path/to/RIMAPI
```

When those are unset, metadata probes `{RIMAPI_FORK_PATH}/1.6/Assemblies/RIMAPI.dll`
(then `1.5`), then `../RIMAPI` next to `git rev-parse --show-toplevel` of the RLE
checkout, and only then the conventional Workshop path as a last-resort OSS
fallback. Copying a build into the Workshop folder is still how RimWorld loads
the fork; it is not what summaries should treat as SoT.

### Live game test

```bash
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ You need four things set up:
3. **Python 3.14+** with [uv](https://docs.astral.sh/uv/)
4. **Save file** — `rle_crashlanded_v1` in RimWorld's save folder (the scenario auto-loads it)

> **RIMAPI note:** The Workshop version may not have our contributed endpoints yet. See [CLAUDE.md](CLAUDE.md) for instructions on building and deploying our fork DLL.
> **RIMAPI note:** The Workshop version may not have our contributed endpoints yet. AppSprout source of truth is the compiled [AppSprout-dev/RIMAPI](https://github.com/AppSprout-dev/RIMAPI) checkout (`rle-testing`), not Steam Workshop. Set `RIMAPI_DLL_PATH` (the built `1.6/Assemblies/RIMAPI.dll`) and `RIMAPI_FORK_PATH` (the fork repo root) so run summaries record path, sha256, and fork commit. Unset, RLE probes those compiled Assemblies (fork env, then `../RIMAPI` next to the RLE git toplevel) and only then a Workshop path as a last-resort OSS fallback. See [CLAUDE.md](CLAUDE.md) for build/deploy steps.

### Verify

Expand Down
135 changes: 109 additions & 26 deletions src/rle/tracking/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@
# baseline no longer banks free process points; weights redistributed.
SCORING_VERSION = "1.2"

# Conventional install path for the RIMAPI Workshop mod we deploy our fork DLL
# over. Best-effort — if Steam lives elsewhere set the RIMAPI_DLL_PATH env var.
_RIMAPI_DLL_DEFAULT_PATH = Path(
# Last-resort OSS fallback only. AppSprout source of truth is the compiled
# checkout (AppSprout-dev/RIMAPI, branch rle-testing), not Steam Workshop.
# Prefer $RIMAPI_DLL_PATH / $RIMAPI_FORK_PATH, then a sibling ../RIMAPI build.
_RIMAPI_DLL_WORKSHOP_FALLBACK = Path(
"C:/Steam/steamapps/workshop/content/294100/3593423732/1.6/Assemblies/RIMAPI.dll",
)
_RIMAPI_ASSEMBLY_RELATIVE = (
Path("1.6") / "Assemblies" / "RIMAPI.dll",
Path("1.5") / "Assemblies" / "RIMAPI.dll",
)


def collect_metadata(
Expand Down Expand Up @@ -83,36 +88,114 @@ def file_sha256(path: Path | None) -> str | None:
return None


def _env_path(name: str) -> Path | None:
raw = os.environ.get(name, "").strip()
return Path(raw).expanduser() if raw else None


def _git_show_toplevel(start: Path) -> Path | None:
try:
out = subprocess.check_output(
["git", "-C", str(start), "rev-parse", "--show-toplevel"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return None
return Path(out) if out else None


def _rle_git_toplevel() -> Path | None:
"""RLE checkout root via git, not ``Path(__file__).parents[3]``.

``__file__`` is under site-packages when RLE is installed into a venv, so
walking parents of this module does not find the repo. Prefer cwd, then
this file's directory (source checkouts), and ask git for the toplevel.
"""
for start in (Path.cwd(), Path(__file__).resolve().parent):
toplevel = _git_show_toplevel(start)
if toplevel is not None:
return toplevel
return None


def _rimapi_sibling_checkout() -> Path | None:
"""``../RIMAPI`` next to the RLE git toplevel, when that directory exists."""
toplevel = _rle_git_toplevel()
if toplevel is None:
return None
sibling = toplevel.parent / "RIMAPI"
return sibling if sibling.is_dir() else None


def _dlls_under_fork(fork_root: Path) -> tuple[Path, ...]:
return tuple(fork_root / relative for relative in _RIMAPI_ASSEMBLY_RELATIVE)


def _first_existing_file(*candidates: Path) -> Path | None:
for path in candidates:
if path.is_file():
return path.resolve()
return None


def _rimapi_dll_path() -> Path | None:
"""Resolve the deployed RIMAPI DLL path (env override → Workshop default)."""
override = os.environ.get("RIMAPI_DLL_PATH")
if override:
candidate = Path(override)
return candidate if candidate.is_file() else None
return (
_RIMAPI_DLL_DEFAULT_PATH if _RIMAPI_DLL_DEFAULT_PATH.is_file() else None
)
"""Resolve the RIMAPI DLL to hash for run metadata.

Probe order (first existing file wins):

1. ``$RIMAPI_DLL_PATH`` — explicit pin; if set but missing, return None
(do not silently fall through to Workshop)
2. ``$RIMAPI_FORK_PATH/{1.6,1.5}/Assemblies/RIMAPI.dll``
3. Sibling checkout ``{RLE git toplevel}/../RIMAPI/{1.6,1.5}/Assemblies/RIMAPI.dll``
4. Steam Workshop path — last-resort OSS fallback, not AppSprout SoT
"""
override = _env_path("RIMAPI_DLL_PATH")
if override is not None:
return override.resolve() if override.is_file() else None

candidates: list[Path] = []
fork_override = _env_path("RIMAPI_FORK_PATH")
if fork_override is not None:
candidates.extend(_dlls_under_fork(fork_override))

sibling = _rimapi_sibling_checkout()
if sibling is not None:
candidates.extend(_dlls_under_fork(sibling))

candidates.append(_RIMAPI_DLL_WORKSHOP_FALLBACK)
return _first_existing_file(*candidates)


def _rimapi_fork_candidates() -> list[Path]:
candidates: list[Path] = []
override = _env_path("RIMAPI_FORK_PATH")
if override is not None:
candidates.append(override)
sibling = _rimapi_sibling_checkout()
if sibling is not None and sibling not in candidates:
candidates.append(sibling)
return candidates


def _rimapi_fork_commit() -> str:
"""HEAD short SHA of the local RIMAPI fork checkout, if findable.

Honors $RIMAPI_FORK_PATH; otherwise checks the conventional sibling repo
location (../RIMAPI relative to this RLE checkout). Empty string when the
fork isn't reachable from the runtime environment.
Honors ``$RIMAPI_FORK_PATH``. Otherwise resolves a sibling ``../RIMAPI``
from the RLE git toplevel (``git rev-parse --show-toplevel``), not from
``Path(__file__).parents[3]``. Empty string when the fork isn't reachable.
"""
override = os.environ.get("RIMAPI_FORK_PATH")
candidates = [Path(override)] if override else []
candidates.append(Path(__file__).resolve().parents[3] / "RIMAPI")
for fork_path in candidates:
if (fork_path / ".git").exists():
try:
return subprocess.check_output(
["git", "-C", str(fork_path), "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL, text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return ""
for fork_path in _rimapi_fork_candidates():
if not (fork_path / ".git").exists():
continue
try:
return subprocess.check_output(
["git", "-C", str(fork_path), "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return ""
return ""


Expand Down
Loading
Loading