# panel-empyrion-bridge

A small .NET 8 ASP.NET Core sidecar that proxies Empyrion's native Mod API
(TCP/protobuf-net) into a JSON REST surface for the panel dashboard.

## Why this exists

Empyrion's only first-class admin tool is a Windows-only desktop app
(Empyrion Admin Helper / EAH) speaking protobuf-net to an in-process mod
called EPM. Re-implementing protobuf-net contract decoding in Go is a
losing battle — every Empyrion update could shift `[ProtoMember]` tags.

Instead, this bridge:

- References Eleon's `Mif.dll` directly (the official mod-API SDK).
- Re-uses `EPMConnector` (TCP framing + protobuf-net dispatch) by
  compiling its source files in.
- Exposes a thin REST API the panel calls.

## Build

```
docker build -t panel-empyrion-bridge:latest modules/empyrion-bridge
```

## Run

The bridge needs network reach to wherever EPM is listening on each
Empyrion instance. On a single-host setup with `network_mode: host`
empyrion containers, hostname `127.0.0.1` works:

```
docker run -d --name panel-bridge --network host \
  -e BRIDGE_URLS=http://127.0.0.1:8090 \
  panel-empyrion-bridge:latest
```

For multi-host setups, pass each Empyrion's LAN IP + EAH_API_PORT in the
query string per request.

## API (Phase 2 — read-only)

All endpoints take `?host=…&port=…` query parameters identifying the
Empyrion EPM instance.

| Method | Path                                       | Returns                                          |
|--------|--------------------------------------------|--------------------------------------------------|
| GET    | `/healthz`                                 | bridge health + version                          |
| GET    | `/api/v1/factions`                         | `[{ origin, factionId, name, abbrev }]`          |
| GET    | `/api/v1/players`                          | `{ playerIds: [int], count }`                    |
| GET    | `/api/v1/players/{id}`                     | full `PlayerInfo` (raw protobuf-net contract)    |
| GET    | `/api/v1/playfields`                       | `{ playfields: [name] }`                         |
| GET    | `/api/v1/playfields/{name}/entities`       | `{ entities: [...] }`                            |
| POST   | `/api/v1/console`                          | fire-and-forget; body is the console line        |

`POST /api/v1/console` is currently EXPERIMENTAL — see "Known issues" below.

## Known issues / Phase 3+ work

- **Console command cmd-byte truncation suspected.** Empyrion's `CmdId`
  enum has grown past 255 entries; the legacy `EPMConnector.ModProtocol`
  writes `cmd` as a single byte, which means commands with id > 255 alias
  to other commands. Symptom seen: sending `Request_ConsoleCommand` shows
  up on the EPM side as `Event_ChatMessageEx`. Fix: extend the protocol
  encoding to use a UInt16 cmd OR build a tested mapping table from
  Mif.dll's CmdId enum values. Tracked for Phase 3.
- **Authentication.** EPM has none. The bridge currently has none either.
  Before exposing any port to the WAN, add a shared-secret bearer token
  matching the panel's existing service-API pattern.
- **Streaming events.** Right now unsolicited Event_* packages (chat,
  player join, etc) are dropped. A `GET /api/v1/stream` SSE endpoint will
  fan these out to subscribers in Phase 4.
- **Connection persistence.** The pool reaps idle connections after 5
  minutes. If Empyrion restarts, we don't yet auto-reconnect; the next
  request opens a fresh connection but in-flight requests fail. Phase 5.

## Source layout

```
empyrion-bridge/
  bridge/                  ASP.NET Core minimal API project
    Bridge.csproj
    Program.cs             endpoint definitions
    BridgePool.cs          per-(host,port) connection cache
    BridgeConnection.cs    request/response correlation by (seq, expectedCmd)
    EPMConnector/          source files compiled in (from MichaelGoulding/sample-empyrion-mod, MIT)
  lib/
    Mif.dll                Eleon Modding SDK reference
  Dockerfile               multi-stage build, alpine runtime
```

## Verified end-to-end on 2026-04-30

```
curl 'http://<agent-host>:8090/api/v1/factions?host=127.0.0.1&port=7028'
→ {"factions":[{"origin":1,"factionId":1,"name":"Human","abbrev":"Hum"}]}
```
