# The Cycle: Frontier — serveur privé (émulateur Prospect)

Émulateur des services en ligne de **The Cycle: Frontier** (jeu retiré de Steam en
septembre 2022), permettant de rejouer en solo sur un serveur auto-hébergé.
Fork de [`deiteris/Prospect`](https://github.com/deiteris/Prospect), figé sur le
**Build 8 / client Saison 2**, buildé depuis les sources et déployé en conteneur via
une CI Gitea.

> **Ce n'est pas du multijoueur.** Le raid tourne **côté client** (station solo) : chacun
> joue sa propre instance. Le serveur partage la progression, les comptes et les boutiques,
> pas la partie. Le vrai multi (squad en raid, voix de proximité) suppose un serveur de jeu
> Unreal dédié — voir [Hors-périmètre & R&D](#hors-périmètre--rd).

---

## Sommaire

- [Comment ça marche](#comment-ça-marche)
- [Structure du dépôt](#structure-du-dépôt)
- [Build & lancement du serveur](#build--lancement-du-serveur)
- [Certificat TLS](#certificat-tls)
- [CI/CD](#cicd)
- [Config du client (switch de serveur + certificat)](#config-du-client-switch-de-serveur--certificat)
- [État des fonctionnalités](#état-des-fonctionnalités)
- [Hors-périmètre & R&D](#hors-périmètre--rd)
- [Crédits & licence](#crédits--licence)

---

## Comment ça marche

Le client officiel parle à PlayFab. On l'intercepte côté client et on le redirige vers
notre serveur, qui réimplémente juste ce qu'il faut de PlayFab (auth Steam, CloudScript,
UserData/TitleData, matchmaking solo).

```mermaid
flowchart LR
  subgraph client [Poste de jeu]
    L[Prospect.Client.Loader<br/>injecte l'agent] --> A[Prospect.Agent<br/>hooke l'URL PlayFab]
    A -- lit --> B[backend.txt]
    A --> G[Jeu &#40;TCF&#41;]
  end
  G -- HTTPS / SignalR --> API[Prospect.Server.Api<br/>émulateur PlayFab]
  API --> M[(MongoDB)]
```

- **`Prospect.Client.Loader`** lance le jeu en injectant l'agent.
- **`Prospect.Agent`** hooke l'URL de l'API PlayFab et la remplace par le contenu de
  **`backend.txt`** (placé dans `Prospect/Binaries/Win64`). Absent → fallback
  `https://127.0.0.1:8443`.
- **`Prospect.Server.Api`** émule PlayFab (auth Steam → JWT, CloudScript, données joueur)
  et pousse le temps-réel via **SignalR**. Les données vivent dans **MongoDB**.

Toute la redirection du client tient donc dans **une seule valeur** (`backend.txt`) — gérée
par l'outil [`Prospect.Client.Config`](#config-du-client-switch-de-serveur--certificat).

---

## Structure du dépôt

| Projet | Rôle |
|---|---|
| **`Prospect.Server.Api`** | Cœur : émulateur PlayFab (ASP.NET 8). Controllers Client/CloudScript/Multiplayer, services Auth/UserData/TitleData/Database(Mongo)/Qos, hub SignalR. |
| **`Prospect.Steam`** | Validation du ticket Steam (auth). |
| **`Prospect.Client.Loader`** | Loader C++ : lance le jeu et injecte l'agent. |
| **`Prospect.Agent`** | Agent C++ injecté : hooke l'URL PlayFab → `backend.txt`. |
| **`Prospect.Client.Config`** | Utilitaire multi-OS : écrit `backend.txt`, importe le certificat, lance le jeu. Voir son [README](src/Prospect.Client.Config/README.md). |
| **`Prospect.Server.Game`** | Serveur de jeu dédié (squelette, **R&D**). |
| **`Prospect.Unreal[.Generator/.Tests]`** | Réimplémentation C# du netcode Unreal (**R&D** serveur dédié). |
| `utils/` | `generate_ssl.py` — génération du certificat auto-signé. |

Build config **`Season 2 Release`** obligatoire pour l'API (le code sélectionne la saison
via `#if SEASON_2_RELEASE` / `SEASON_3_RELEASE` → sinon `#error Unsupported build type`).

---

## Build & lancement du serveur

Le serveur tourne en conteneur. L'image est buildée depuis les sources par le
[`Dockerfile`](Dockerfile) (multi-stage SDK .NET 8 → runtime aspnet 8, publish en
`Season 2 Release`).

### Build de l'image

```bash
docker build -t the-cycle .
```

### Lancement

Il faut une **instance MongoDB** joignable et un **certificat TLS** monté (voir section
suivante). Variables d'environnement :

| Variable | Rôle |
|---|---|
| `DatabaseSettings__ConnectionString` | URI de connexion MongoDB. |
| `DatabaseSettings__DatabaseName` | Base à utiliser (ex. `ProspectDb`). |
| `AuthTokenSettings__Secret` | Secret de signature des JWT émis par le serveur. |
| `PlayFabSettings__SignalRURL` | URL SignalR **telle que le client doit l'atteindre** (voir gotcha ci-dessous). |
| `Kestrel__Certificates__Default__Path` | Chemin du `.pfx` dans le conteneur. |
| `Kestrel__Endpoints__Https__Url` | ex. `https://0.0.0.0:8443`. |
| `SteamWebApiKey` | *(optionnel)* clé Steam Web API pour récupérer les pseudos ; inerte si absente. |

```bash
docker run -d --name the-cycle-api \
  -e DatabaseSettings__ConnectionString="mongodb://user:pass@HOST:27017/?authSource=ProspectDb" \
  -e DatabaseSettings__DatabaseName="ProspectDb" \
  -e AuthTokenSettings__Secret="<secret>" \
  -e PlayFabSettings__SignalRURL="https://<host-public>:8443/signalr/?hub=pubsub" \
  -e Kestrel__Endpoints__Https__Url="https://0.0.0.0:8443" \
  -e Kestrel__Certificates__Default__Path="/certs/certificate.pfx" \
  -v "$PWD/certs:/certs:ro" \
  -p 8443:8443 \
  the-cycle
```

> ⚠️ **`PlayFabSettings__SignalRURL` = l'URL que le CLIENT doit joindre**, pas `127.0.0.1`.
> Le serveur y renvoie le client pour l'event de matchmaking ; s'il pointe sur `127.0.0.1`,
> le déploiement en raid **timeout**. Mets le hostname/IP public du serveur.

### Dev local (.NET)

```bash
dotnet build src/Prospect.Server.Api/Prospect.Server.Api.csproj -c "Season 2 Release"
dotnet run  --project src/Prospect.Server.Api -c "Season 2 Release"
```

---

## Certificat TLS

La connexion est en HTTPS et le client valide le certificat → il doit être **auto-signé et
fait confiance** côté client. Générer le `.pfx` avec `utils/generate_ssl.py`.

> ⚠️ **Le SAN doit contenir `DNS:<ip>` ET `IP:<ip>`**, en plus des hostnames.
> Le HTTP du jeu (libcurl) accepte l'IP en SAN IP, mais le WebSocket (libwebsockets) valide
> l'IP contre les SAN **DNS** → sans `DNS:<ip>`, la connexion SignalR échoue
> (`Hostname mismatch err=62`). Inclure aussi `2EA46.playfabapi.com`, `localhost`,
> `127.0.0.1` et tous les hostnames publics utilisés dans `backend.txt`.

Côté client, l'import du certificat est automatisé par
[`Prospect.Client.Config`](#config-du-client-switch-de-serveur--certificat).

---

## CI/CD

[`.gitea/workflows/build.yml`](.gitea/workflows/build.yml) — sur push `main` / `preprod`
(ou `workflow_dispatch`) :

1. build de l'image depuis le `Dockerfile` ;
2. push sur le registry `git.nfteam.ovh/neckfire/the-cycle` ;
3. notification du résultat (ntfy).

**Modèle de branches :**

| Branche | Tag image | Usage |
|---|---|---|
| `preprod` | `:preprod` (+ `:preprod-<sha>`) | banc de test — valider un build avant de merger |
| `main` | `:latest` (+ `:<sha>`) | production |

Workflow type : coder → push `preprod` → tester sur le serveur preprod → **PR `preprod → main`**
→ la CI republie `:latest`. Le déploiement applique la nouvelle image
(`docker compose pull && docker compose up -d`).

> `Prospect.Client.Config` (outil client, cross-OS) n'est **pas** buildé par cette CI —
> voir sa section publication.

---

## Config du client (switch de serveur + certificat)

L'outil **`Prospect.Client.Config`** (binaire `ProspectServerSwitcher`, multi-OS
Linux/Proton + Windows) fait tout le boulot côté client :

- écrit `backend.txt` (presets **prod** / **preprod** ou URL libre) ;
- récupère le certificat **en direct depuis le serveur ciblé** (TLS) et le rend fiable :
  - **Windows** : import dans *Autorités de certification racines de confiance* (utilisateur) ;
  - **Linux/Proton** : import direct dans le préfixe Wine du jeu via `wine reg import`
    (car `wine certutil` est cassé sous Proton) ;
- lance le jeu.

```bash
# menu interactif
ProspectServerSwitcher

# scriptable
ProspectServerSwitcher --folder "<...>/Prospect/Binaries/Win64" --set preprod
ProspectServerSwitcher --set https://mon-serveur:8443
```

Détails complets, gotchas Proton (préfixe non-Steam) et commandes de publication des
binaires autonomes : **[src/Prospect.Client.Config/README.md](src/Prospect.Client.Config/README.md)**.

> Installation complète pas-à-pas pour un nouveau joueur (télécharger le client S2, le
> LoaderPack, importer le certificat) : **[FRIENDS-INSTALL.md](FRIENDS-INSTALL.md)**.

---

## État des fonctionnalités

**Fonctionne :**

- [x] Login Steam, EULA, tutoriel
- [x] Station solo (S2/S3) : onboarding, matchmaking & déploiement **solo**
- [x] Inventaire & loadout, stash, vente, réparation
- [x] Contrats / quêtes — y compris les objectifs **kills** et **de zone** (auto-crédités :
      pas de serveur dédié pour remonter les events runtime du raid client-hosted)
- [x] Progression des factions
- [x] Season pass : claim + gain d'XP de saison (niveau Fortuna)
- [x] Boutiques d'items (Korolev / ICA / Osiris / QuickShop / CraftingStation)
- [x] Aurum Shop (cosmétiques) + rotation daily/weekly
- [x] Craft, Quarters, solde joueur, connexion quotidienne
- [x] Apparence & emotes
- [x] Assurance : débit de la prime au déploiement + payout à la mort
- [x] Stats de carrière (valeurs à 0 — non traçables sans serveur de jeu)
- [x] Présence des amis (en ligne / en raid)
- [x] Pseudos réels via Steam Web API (le client n'envoie que le SteamID)
- [x] Cartes : Bright Sands, Crescent Falls, Tharis Island

**Non implémenté / hors-périmètre :**

- [ ] Squad / multi dans le **même** raid — nécessite un serveur de jeu dédié
- [ ] Voix de proximité (login/join Vivox = placeholders)
- [ ] Free loadouts & presets (Saison 3 uniquement)
- [ ] Achat de cosmétiques (endpoint d'achat vanity distinct, non câblé)
- [ ] Catalogue des récompenses Fortuna (DataTable côté client, dans des paks chiffrés)
- [ ] Défis quotidiens Fortuna

---

## Hors-périmètre & R&D

Un **serveur de jeu Unreal dédié** (`Prospect.Server.Game` + `Prospect.Unreal`, branche
`game-server`) est en cours de reverse-engineering pour, à terme, permettre le vrai multi.
État : handshake stateless UE, séquençage et décodage des bunches **franchis**, canal de
contrôle ouvert, `NMT_Hello` parsé. **Bloqué** sur le chiffrement **DTLS-PSK** du client
(clé dérivée du `user_id`), derrière un exe packé (BattlEye) → la dérivation n'est pas
extractible statiquement. Détails dans `NETCODE-RND.md` (branche `game-server`).

Le « lobby squad » via l'API seule n'est **pas faisable** : l'invitation d'amis est gérée
100 % côté client Steam.

---

## Crédits & licence

Fork de [`deiteris/Prospect`](https://github.com/deiteris/Prospect) (lui-même issu du
projet Prospect original). Voir [`LICENSE`](LICENSE). Usage privé.
