Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1f3fd1125 | ||
|
|
b05782d4e9 | ||
|
|
d7c89327a7 | ||
|
|
03048b793d | ||
|
|
1d9cf795dd | ||
|
|
7dbab1aab8 | ||
|
|
078d2f80be | ||
|
|
c986a98471 | ||
|
|
57a45d91df | ||
|
|
c6b2330e1d | ||
|
|
9737316dbc | ||
|
|
a4e006f84f | ||
|
|
3cdf8bc7e2 | ||
|
|
67f3095896 |
@@ -1,249 +1,222 @@
|
|||||||
# The Cycle: Frontier — serveur privé (émulateur Prospect)
|
# Prospect <!-- omit in toc -->
|
||||||
|
|
||||||
Émulateur des services en ligne de **The Cycle: Frontier** (jeu retiré de Steam en
|
Also known as "The Cycle: Frontier".
|
||||||
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
|
## Table of Contents <!-- omit in toc -->
|
||||||
> 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).
|
|
||||||
|
|
||||||
---
|
- [Features](#features)
|
||||||
|
- [Running locally](#running-locally)
|
||||||
|
- [1. Prerequisites](#1-prerequisites)
|
||||||
|
- [1.1 How to download Season 2 client from SteamDB using Steam console](#11-how-to-download-season-2-client-from-steamdb-using-steam-console)
|
||||||
|
- [2. Unpack `Prospect.Server.Api`](#2-unpack-prospectserverapi)
|
||||||
|
- [3. Generate and import SSL certificate](#3-generate-and-import-ssl-certificate)
|
||||||
|
- [4. Extract `LoaderPack` to the game](#4-extract-loaderpack-to-the-game)
|
||||||
|
- [5. Run the server](#5-run-the-server)
|
||||||
|
- [6. Run the game](#6-run-the-game)
|
||||||
|
- [Troubleshooting and FAQ](#troubleshooting-and-faq)
|
||||||
|
- [How to remove the certificate?](#how-to-remove-the-certificate)
|
||||||
|
- [`generate_ssl.exe` is flagged as a virus](#generate_sslexe-is-flagged-as-a-virus)
|
||||||
|
- [Body parts are missing with Season 3 client](#body-parts-are-missing-with-season-3-client)
|
||||||
|
- [Prospect.Server.Api does not start](#prospectserverapi-does-not-start)
|
||||||
|
- [Login Failed. Error code: 3](#login-failed-error-code-3)
|
||||||
|
- [Login Failed. Error code: 5](#login-failed-error-code-5)
|
||||||
|
- [Development](#development)
|
||||||
|
|
||||||
## Sommaire
|
## Features
|
||||||
|
|
||||||
- [Comment ça marche](#comment-ça-marche)
|
* [x] Basic login with Steam
|
||||||
- [Structure du dépôt](#structure-du-dépôt)
|
* [x] EULA acceptance
|
||||||
- [Build & lancement du serveur](#build--lancement-du-serveur)
|
* [x] Tutorial
|
||||||
- [Certificat TLS](#certificat-tls)
|
* [x] Single-player station (Season 2 and Season 3):
|
||||||
- [CI/CD](#cicd)
|
* [x] Onboarding
|
||||||
- [Config du client (switch de serveur + certificat)](#config-du-client-switch-de-serveur--certificat)
|
* [ ] Matchmaking and deployment
|
||||||
- [État des fonctionnalités](#état-des-fonctionnalités)
|
* [x] Solo
|
||||||
- [Hors-périmètre & R&D](#hors-périmètre--rd)
|
* [ ] Squad
|
||||||
- [Crédits & licence](#crédits--licence)
|
* [ ] Items insurance
|
||||||
|
* [ ] Free loadouts (Season 3)
|
||||||
|
* [x] Inventory and loadout
|
||||||
|
* [ ] Loadout presets (Season 3)
|
||||||
|
* [x] Quests
|
||||||
|
* [x] Faction progression
|
||||||
|
* [ ] Season pass
|
||||||
|
* [ ] Aurum Shops
|
||||||
|
* [ ] Daily shop
|
||||||
|
* [ ] Weekly shop
|
||||||
|
* [ ] Shop rotation
|
||||||
|
* [x] Daily login
|
||||||
|
* [x] Character appearance and emotes
|
||||||
|
* [x] Item Shops
|
||||||
|
* [x] Crafting station
|
||||||
|
* [x] Quarters
|
||||||
|
* [x] Player balance
|
||||||
|
* [ ] Social features
|
||||||
|
* [ ] Proximity voice
|
||||||
|
* [x] Vivox login
|
||||||
|
* [x] Vivox create and join channel
|
||||||
|
* [ ] Proximity voice works
|
||||||
|
* [x] Game mechanics
|
||||||
|
* [x] Can deploy through terminal
|
||||||
|
* [x] Can deploy with loadout
|
||||||
|
* [x] Can evac
|
||||||
|
* [x] Can do quests (except PvP)
|
||||||
|
* [x] Can gain/lose loot
|
||||||
|
* [x] Can use Alien Forge
|
||||||
|
* [x] Map content
|
||||||
|
* [x] Bright Sands
|
||||||
|
* [x] Crescent Falls
|
||||||
|
* [x] Tharis Island
|
||||||
|
|
||||||
---
|
## Running locally
|
||||||
|
|
||||||
## Comment ça marche
|
> [!NOTE]
|
||||||
|
> If you've already done all steps previously, you can skip to Step 7.
|
||||||
|
|
||||||
Le client officiel parle à PlayFab. On l'intercepte côté client et on le redirige vers
|
### 1. Prerequisites
|
||||||
notre serveur, qui réimplémente juste ce qu'il faut de PlayFab (auth Steam, CloudScript,
|
|
||||||
UserData/TitleData, matchmaking solo).
|
|
||||||
|
|
||||||
```mermaid
|
> [!WARNING]
|
||||||
flowchart LR
|
> The latest Steam version of The Cycle: Frontier currently does not work with Windows 11 24H2!
|
||||||
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 (TCF)]
|
|
||||||
end
|
|
||||||
G -- HTTPS / SignalR --> API[Prospect.Server.Api<br/>émulateur PlayFab]
|
|
||||||
API --> M[(MongoDB)]
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`Prospect.Client.Loader`** lance le jeu en injectant l'agent.
|
> [!IMPORTANT]
|
||||||
- **`Prospect.Agent`** hooke l'URL de l'API PlayFab et la remplace par le contenu de
|
> You must have The Cycle: Frontier from Steam in your Steam library to be able to download it.
|
||||||
**`backend.txt`** (placé dans `Prospect/Binaries/Win64`). Absent → fallback
|
> Otherwise, the download will fail with an error message about missing license.
|
||||||
`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
|
Before you start, you'll need the following software downloaded and installed:
|
||||||
par l'outil [`Prospect.Client.Config`](#config-du-client-switch-de-serveur--certificat).
|
|
||||||
|
|
||||||
---
|
1. [MongoDB Community Edition](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-8.0.4-signed.msi).
|
||||||
|
|
||||||
## Structure du dépôt
|
1. [`Prospect.Server.Api` and `LoaderPack`](https://github.com/deiteris/Prospect/releases) from the Releases section:
|
||||||
|
|
||||||
| Projet | Rôle |
|
- For Season 3 (the latest Steam game client), use Build 6.
|
||||||
|---|---|
|
|
||||||
| **`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
|
- For Season 2 game client, use the latest version.
|
||||||
via `#if SEASON_2_RELEASE` / `SEASON_3_RELEASE` → sinon `#error Unsupported build type`).
|
|
||||||
|
|
||||||
---
|
1. The Cycle: Frontier game client:
|
||||||
|
|
||||||
## Build & lancement du serveur
|
- The latest version from [Steam](https://steamcommunity.com/app/868270).
|
||||||
|
|
||||||
Le serveur tourne en conteneur. L'image est buildée depuis les sources par le
|
- Season 2 client version `4623363103423775682` from SteamDB. See [download instructions below](#11-how-to-download-season-2-client-from-steamdb-using-steam-console).
|
||||||
[`Dockerfile`](Dockerfile) (multi-stage SDK .NET 8 → runtime aspnet 8, publish en
|
|
||||||
`Season 2 Release`).
|
|
||||||
|
|
||||||
### Build de l'image
|
#### 1.1 How to download Season 2 client from SteamDB using Steam console
|
||||||
|
|
||||||
```bash
|
> [!WARNING]
|
||||||
docker build -t the-cycle .
|
> This will overwrite the existing client if you try to download a different manifest!
|
||||||
```
|
|
||||||
|
|
||||||
### Lancement
|
1. With Steam running, press `Win+R` and enter `steam://nav/console`. A Steam console will open.
|
||||||
|
|
||||||
Il faut une **instance MongoDB** joignable et un **certificat TLS** monté (voir section
|
1. Open [The Cycle: Frontier SteamDB manifests](https://steamdb.info/depot/868271/manifests/).
|
||||||
suivante). Variables d'environnement :
|
|
||||||
|
|
||||||
| Variable | Rôle |
|
1. Make sure you have **Copy format** set to **Steam console**.
|
||||||
|---|---|
|
|
||||||
| `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
|
1. Press `CTRL+F` and enter `4623363103423775682` to find the manifest for Season 2 version 2.7.2 client.
|
||||||
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`.
|
1. Click the  icon to copy the download command.
|
||||||
> 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)
|
1. Paste the command in the Steam console and press `Enter`.
|
||||||
|
|
||||||
```bash
|
1. The depot will begin downloading. You should receive a notification and the destination folder when the download is complete.
|
||||||
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"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
### 2. Unpack `Prospect.Server.Api`
|
||||||
|
|
||||||
## Certificat TLS
|
Use your favorite ZIP archiver and unzip the `Prospect.Server.Api.zip` downloaded from this repository.
|
||||||
|
|
||||||
La connexion est en HTTPS et le client valide le certificat → il doit être **auto-signé et
|
### 3. Generate and import SSL certificate
|
||||||
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.
|
> [!IMPORTANT]
|
||||||
> Le HTTP du jeu (libcurl) accepte l'IP en SAN IP, mais le WebSocket (libwebsockets) valide
|
> Do not share the generated certificate! Generated certificate includes a private key that may be used to generate other certificates and compromise your security.
|
||||||
> 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
|
A connection to the server is served over a secured connection. The server uses self-signed certificate that must be added to trusted authorities in order for the game
|
||||||
[`Prospect.Client.Config`](#config-du-client-switch-de-serveur--certificat).
|
to successfully communicate with the local server. Do the following:
|
||||||
|
|
||||||
---
|
1. Open the folder with `Prospect.Server.Api`.
|
||||||
|
|
||||||
## CI/CD
|
1. Double-click `generate_ssl.exe`. `certificate.pfx` will appear in the same folder.
|
||||||
|
|
||||||
[`.gitea/workflows/build.yml`](.gitea/workflows/build.yml) — sur push `main` / `preprod`
|
1. Double-click `certificate.pfx`. The Certificate Import Wizard will open:
|
||||||
(ou `workflow_dispatch`) :
|
|
||||||
|
|
||||||
1. build de l'image depuis le `Dockerfile` ;
|
1. Select **Current User** under Store Location and click **Next**.
|
||||||
2. push sur le registry `git.nfteam.ovh/neckfire/the-cycle` ;
|
|
||||||
3. notification du résultat (ntfy).
|
|
||||||
|
|
||||||
**Modèle de branches :**
|
1. Leave **File to Import** unchanged and click **Next**.
|
||||||
|
|
||||||
| Branche | Tag image | Usage |
|
1. Leave **Password** empty and click **Next**.
|
||||||
|---|---|---|
|
|
||||||
| `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`**
|
1. Select **Place all certificates in the following store** > **Browse...**. Choose **Trusted Root Certification Authorities** and click **OK**. Click **Next**.
|
||||||
→ 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 —
|
1. Click **Finish**. A **Security Warning** popup may appear, make sure it specifies `2EA46.playfabapi.com` certification authority and click **Yes**.
|
||||||
> voir sa section publication.
|
|
||||||
|
|
||||||
---
|
### 4. Extract `LoaderPack` to the game
|
||||||
|
|
||||||
## Config du client (switch de serveur + certificat)
|
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
|
||||||
|
|
||||||
L'outil **`Prospect.Client.Config`** (binaire `ProspectServerSwitcher`, multi-OS
|
1. Open the `LoaderPack` archive.
|
||||||
Linux/Proton + Windows) fait tout le boulot côté client :
|
|
||||||
|
|
||||||
- écrit `backend.txt` (presets **prod** / **preprod** ou URL libre) ;
|
1. Drag and drop the contents of the `LoaderPack` archive to the game.
|
||||||
- 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
|
1. Create a shortcut for the `Prospect.Client.Loader` that you will use later to launch the game.
|
||||||
# menu interactif
|
|
||||||
ProspectServerSwitcher
|
|
||||||
|
|
||||||
# scriptable
|
### 5. Run the server
|
||||||
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
|
Now you are all set! Open the folder with `Prospect.Server.Api` and run `Prospect.Server.Api.exe`. It will open a console if it runs successfully.
|
||||||
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
|
> [!IMPORTANT]
|
||||||
> LoaderPack, importer le certificat) : **[FRIENDS-INSTALL.md](FRIENDS-INSTALL.md)**.
|
> Do not close the console when you run the game.
|
||||||
|
|
||||||
---
|
### 6. Run the game
|
||||||
|
|
||||||
## État des fonctionnalités
|
Once the server is running, make sure that Steam is running and open The Cycle: Frontier using the shortcut you've created before.
|
||||||
|
|
||||||
**Fonctionne :**
|
## Troubleshooting and FAQ
|
||||||
|
|
||||||
- [x] Login Steam, EULA, tutoriel
|
### How to remove the certificate?
|
||||||
- [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 :**
|
If you've installed the certificate for the **Current User**:
|
||||||
|
|
||||||
- [ ] Squad / multi dans le **même** raid — nécessite un serveur de jeu dédié
|
1. Open **Start** and enter `certmgr.msc`.
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
---
|
1. Expand **Trusted Root Certification Authorities** and select **Certificates**.
|
||||||
|
|
||||||
## Hors-périmètre & R&D
|
1. Find `2EA46.playfabapi.com`, right-click it > **Delete**.
|
||||||
|
|
||||||
Un **serveur de jeu Unreal dédié** (`Prospect.Server.Game` + `Prospect.Unreal`, branche
|
If you've installed the certificate for the **Local Machine**, repeat the same steps but instead open `certlm.msc`.
|
||||||
`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
|
### `generate_ssl.exe` is flagged as a virus
|
||||||
100 % côté client Steam.
|
|
||||||
|
|
||||||
---
|
`generate_ssl.exe` is a Python application packed with PyInstaller and some anti-viruses may flag it as a virus.
|
||||||
|
This application is a simple certificate generator and you can find its source code in `utils/generate_ssl.py`.
|
||||||
|
|
||||||
## Crédits & licence
|
### Body parts are missing with Season 3 client
|
||||||
|
|
||||||
Fork de [`deiteris/Prospect`](https://github.com/deiteris/Prospect) (lui-même issu du
|
Currently, the server loads body part IDs for Season 2 by default, so this is expected. You can fix this by going to station and changing your character appearance. This will store the updated body part IDs for your character.
|
||||||
projet Prospect original). Voir [`LICENSE`](LICENSE). Usage privé.
|
|
||||||
|
### Prospect.Server.Api does not start
|
||||||
|
|
||||||
|
Make sure you have [.NET Runtime 8.0](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.11) and [ASP.NET Core 8.0](https://aka.ms/dotnet-core-applaunch?framework=Microsoft.AspNetCore.App&framework_version=8.0.0&arch=x64&rid=win-x64&os=win10) installed.
|
||||||
|
|
||||||
|
### Login Failed. Error code: 3
|
||||||
|
|
||||||
|
Make sure that:
|
||||||
|
|
||||||
|
* You have Steam running.
|
||||||
|
* You have created and **saved** the `steam_appid` file as described in step 6.
|
||||||
|
* The `steam_appid` file type is "TXT File".
|
||||||
|
|
||||||
|
### Login Failed. Error code: 5
|
||||||
|
|
||||||
|
Make sure that `Prospect.Server.Api` server is running.
|
||||||
|
|
||||||
|
If the server is running, press `Alt+Tab` to a game console that opens when you start the game and check for the following:
|
||||||
|
|
||||||
|
* `libcurl error 7 (Couldn't connect to server)` - indicates that the `Prospect.Server.Api` is not running.
|
||||||
|

|
||||||
|
|
||||||
|
* `InvalidAPIEndpoint` - indicates that you are running the game using the original shortcut and not using `Prospect.Client.Loader`.
|
||||||
|

|
||||||
|
|
||||||
|
* `libcurl error 60 (Peer certificate cannot be authenticated with given CA certificates)` - indicates that the certificate was not installed correctly. Make sure that the certificate is present in `certmgr.msc` and there is only one certificate. Try removing the certificate and importing it again by following step 4.
|
||||||
|

|
||||||
|
|
||||||
|
* `HTTP code: 500` - usually indicates that MongoDB is not running. Make sure that MongoDB is installed and and that `MongoDB Server` is running in `services.msc`.
|
||||||
|

|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ public class AdminController : ControllerBase
|
|||||||
$"<div class='card'><div class='v'>{Esc(value)}</div><div class='l'>{Esc(label)}</div></div>";
|
$"<div class='card'><div class='v'>{Esc(value)}</div><div class='l'>{Esc(label)}</div></div>";
|
||||||
|
|
||||||
private static string Layout(string title, string body) =>
|
private static string Layout(string title, string body) =>
|
||||||
Head.Replace("__TITLE__", Esc(title)) + DtScript + body + Foot;
|
Head.Replace("__TITLE__", Esc(title)) + body + Foot;
|
||||||
|
|
||||||
// Static shell (no C# interpolation → JS braces/quotes stay literal). Single quotes only.
|
// Static shell (no C# interpolation → JS braces/quotes stay literal). Single quotes only.
|
||||||
private const string Head = @"<!doctype html><html lang='fr'><head>
|
private const string Head = @"<!doctype html><html lang='fr'><head>
|
||||||
@@ -380,11 +380,8 @@ th.dth{cursor:pointer;user-select:none;white-space:nowrap}th.dth:hover{color:var
|
|||||||
<span class='muted' style='margin-left:auto'>read-only · LAN</span></header>
|
<span class='muted' style='margin-left:auto'>read-only · LAN</span></header>
|
||||||
<main>";
|
<main>";
|
||||||
|
|
||||||
private const string Foot = @"</main></body></html>";
|
private const string Foot = @"</main>
|
||||||
|
<script>
|
||||||
// Client-side table engine, injected right after <main> so DT is defined before any
|
|
||||||
// page-level DT.init() runs. Single quotes only (verbatim string → JS braces stay literal).
|
|
||||||
private const string DtScript = @"<script>
|
|
||||||
const DT={
|
const DT={
|
||||||
init(cfg){const el=document.getElementById(cfg.id);el.innerHTML='<p class=muted>Chargement…</p>';
|
init(cfg){const el=document.getElementById(cfg.id);el.innerHTML='<p class=muted>Chargement…</p>';
|
||||||
fetch(cfg.url).then(r=>r.json()).then(rows=>DT.build(el,cfg,rows)).catch(e=>{el.innerHTML='<p class=muted>Erreur: '+e+'</p>';});},
|
fetch(cfg.url).then(r=>r.json()).then(rows=>DT.build(el,cfg,rows)).catch(e=>{el.innerHTML='<p class=muted>Erreur: '+e+'</p>';});},
|
||||||
@@ -427,5 +424,5 @@ const DT={
|
|||||||
return '<span style=color:'+c+';font-weight:600>'+DT.esc(r)+'</span>';},
|
return '<span style=color:'+c+';font-weight:600>'+DT.esc(r)+'</span>';},
|
||||||
esc(s){const d=document.createElement('div');d.textContent=(s==null?'':''+s);return d.innerHTML;}
|
esc(s){const d=document.createElement('div');d.textContent=(s==null?'':''+s);return d.innerHTML;}
|
||||||
};
|
};
|
||||||
</script>";
|
</script></body></html>";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ public class RequestLoggerMiddleware
|
|||||||
private readonly ILogger<RequestLoggerMiddleware> _logger;
|
private readonly ILogger<RequestLoggerMiddleware> _logger;
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
// Keywords used to surface social/squad/invite traffic while reverse-engineering the
|
|
||||||
// squad-invite flow (see SQUAD-EMULATION.md). Matched against the path AND the body
|
|
||||||
// (the CloudScript function name lives in the body of /Client/ExecuteFunction).
|
|
||||||
private static readonly string[] SocialKeywords =
|
|
||||||
{
|
|
||||||
"group", "party", "lobby", "squad", "invite", "friend", "social", "matchmak",
|
|
||||||
};
|
|
||||||
|
|
||||||
public RequestLoggerMiddleware(ILogger<RequestLoggerMiddleware> logger, RequestDelegate next)
|
public RequestLoggerMiddleware(ILogger<RequestLoggerMiddleware> logger, RequestDelegate next)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -40,27 +32,6 @@ public class RequestLoggerMiddleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _next(context);
|
await _next(context);
|
||||||
|
|
||||||
// ── Capture pass (squad-invite RE, see SQUAD-EMULATION.md) ──────────────
|
|
||||||
// Runs AFTER the pipeline so we know whether the request was actually routed.
|
|
||||||
if (context.Request.Method == "POST")
|
|
||||||
{
|
|
||||||
var path = context.Request.Path.Value ?? "";
|
|
||||||
var haystack = (path + " " + body).ToLowerInvariant();
|
|
||||||
|
|
||||||
// Any POST that fell through to a 404 = an endpoint the emulator does NOT implement
|
|
||||||
// (e.g. a native PlayFab /Group/CreateGroup or /Lobby/* the client tried to call).
|
|
||||||
if (context.Response.StatusCode == 404)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("[CAPTURE UNROUTED] {Method} {Url} -> 404 | Body {Body}",
|
|
||||||
context.Request.Method, context.Request.GetDisplayUrl(), body);
|
|
||||||
}
|
|
||||||
else if (SocialKeywords.Any(k => haystack.Contains(k)))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("[CAPTURE SOCIAL] {Url} ({Status}) | Body {Body}",
|
|
||||||
context.Request.GetDisplayUrl(), context.Response.StatusCode, body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> RequestAsync(HttpRequest request)
|
private static async Task<string> RequestAsync(HttpRequest request)
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ public class GetFriendList : ICloudScriptFunction<FYBaseSocialRequest, object?>
|
|||||||
}
|
}
|
||||||
var userId = context.User.FindAuthUserId();
|
var userId = context.User.FindAuthUserId();
|
||||||
|
|
||||||
// Imported Steam friends (persisted by ClientsideFriendsImport), resolved to players
|
// Resolve the imported Steam friends (persisted by ClientsideFriendsImport) to the
|
||||||
// that actually exist on this private server.
|
// players that actually exist on this server. It's a private server, so only friends
|
||||||
|
// who have logged in here will show up.
|
||||||
var steamIds = new List<string>();
|
var steamIds = new List<string>();
|
||||||
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "ImportedSteamFriends" });
|
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "ImportedSteamFriends" });
|
||||||
if (userData.TryGetValue("ImportedSteamFriends", out var rec) && !string.IsNullOrWhiteSpace(rec.Value))
|
if (userData.TryGetValue("ImportedSteamFriends", out var rec) && !string.IsNullOrWhiteSpace(rec.Value))
|
||||||
@@ -51,10 +52,8 @@ public class GetFriendList : ICloudScriptFunction<FYBaseSocialRequest, object?>
|
|||||||
{
|
{
|
||||||
if (player.Id == userId) continue; // never list yourself
|
if (player.Id == userId) continue; // never list yourself
|
||||||
|
|
||||||
var steamId = player.Auth?.FirstOrDefault(a => a.Type == PlayFabUserAuthType.Steam)?.Key ?? "";
|
// Presence persisted by UpdatePlayerPresenceState: online if seen in the last
|
||||||
var displayName = string.IsNullOrWhiteSpace(player.DisplayName) ? "Prospector" : player.DisplayName;
|
// 3 minutes. EYUserState best-effort: 0 = offline, 1 = online, 2 = in match.
|
||||||
|
|
||||||
// 0 = offline, 1 = online, 2 = in match. Online if seen in the last 3 minutes.
|
|
||||||
var onlineState = 0;
|
var onlineState = 0;
|
||||||
var presenceData = await _userDataService.FindAsync(player.Id, player.Id, new List<string> { "PresenceState" });
|
var presenceData = await _userDataService.FindAsync(player.Id, player.Id, new List<string> { "PresenceState" });
|
||||||
if (presenceData.TryGetValue("PresenceState", out var presenceRec) && !string.IsNullOrWhiteSpace(presenceRec.Value))
|
if (presenceData.TryGetValue("PresenceState", out var presenceRec) && !string.IsNullOrWhiteSpace(presenceRec.Value))
|
||||||
@@ -70,50 +69,22 @@ public class GetFriendList : ICloudScriptFunction<FYBaseSocialRequest, object?>
|
|||||||
catch { /* ignore malformed presence */ }
|
catch { /* ignore malformed presence */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The exact model the UE client binds is not documented, so expose the identity
|
|
||||||
// under every plausible field name (camelCase + PlayFab PascalCase) and both a
|
|
||||||
// flat and nested profile — whichever the client reads, the tile gets populated.
|
|
||||||
friends.Add(new
|
friends.Add(new
|
||||||
{
|
{
|
||||||
// identity
|
profile = new
|
||||||
friendPlayFabId = player.Id,
|
{
|
||||||
FriendPlayFabId = player.Id,
|
|
||||||
playerId = player.Id,
|
playerId = player.Id,
|
||||||
PlayerId = player.Id,
|
displayName = player.DisplayName,
|
||||||
playFabId = player.Id,
|
|
||||||
PlayFabId = player.Id,
|
|
||||||
// name
|
|
||||||
displayName,
|
|
||||||
DisplayName = displayName,
|
|
||||||
titleDisplayName = displayName,
|
|
||||||
TitleDisplayName = displayName,
|
|
||||||
name = displayName,
|
|
||||||
username = displayName,
|
|
||||||
Username = displayName,
|
|
||||||
// steam
|
|
||||||
steamId,
|
|
||||||
SteamId = steamId,
|
|
||||||
steamInfo = new { steamId, SteamId = steamId },
|
|
||||||
SteamInfo = new { SteamId = steamId, steamId },
|
|
||||||
// presence
|
|
||||||
onlineState,
|
|
||||||
OnlineState = onlineState,
|
|
||||||
isOnline = onlineState > 0,
|
|
||||||
inMatch = onlineState == 2,
|
|
||||||
presence = new { onlineState, state = onlineState },
|
|
||||||
avatarUrl = "",
|
avatarUrl = "",
|
||||||
// nested profiles
|
},
|
||||||
profile = new { playerId = player.Id, displayName, avatarUrl = "" },
|
onlineState,
|
||||||
Profile = new { PlayerId = player.Id, DisplayName = displayName, PlayerProfileModel = new { DisplayName = displayName } },
|
|
||||||
tags = Array.Empty<string>(),
|
|
||||||
Tags = Array.Empty<string>(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the list under both the camelCase and PlayFab-cased container keys.
|
// NOTE: the exact response shape the client expects is not yet confirmed; this is a
|
||||||
var result = new { friends, Friends = friends, count = friends.Count };
|
// best-effort structure. The log lets us verify resolution while we validate in game.
|
||||||
_logger.LogInformation("GetFriendList for {User}: {Count} friend(s); payload={Json}", userId, friends.Count, JsonSerializer.Serialize(result));
|
_logger.LogInformation("GetFriendList for {User}: {Count} friend(s) resolved on server", userId, friends.Count);
|
||||||
return result;
|
return new { friends };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user