Author SHA1 Message Date
neckfireandClaude Opus 4.8 a4e006f84f docs: guide d installation client pour les amis (serveur prive)
Build & Deploy / build (push) Successful in 15s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:33:37 +00:00
neckfire 3cdf8bc7e2 Merge pull request 'Boutiques : GetStoreItems (vendeurs + Aurum) + fix vanities possédées' (#2) from preprod into main
Build & Deploy / build (push) Successful in 13s
2026-07-14 12:07:18 +00:00
neckfireandClaude Opus 4.8 5d93b3541d fix(store): return per-user owned vanities, not the whole catalogue
Build & Deploy / build (push) Successful in 23s
GetUserInventory returned every vanity in the Vanities title data as an
owned item, so the client showed all Aurum-shop cosmetics as already
owned. Return only the cosmetics the player actually owns: purchased
(VanityItems) plus currently equipped (CharacterVanity slots and the
GlobalVanity sprays / banner / emotes / drop pod), so the starter outfit
stays owned while the rest of the Aurum shop becomes purchasable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:55:50 +02:00
neckfireandClaude Opus 4.8 40dad82559 feat(store): populate Aurum/cosmetic shops from Vanities
Build & Deploy / build (push) Successful in 23s
GetStoreItems now also serves the cosmetic stores the client queries
(AurumShop, and the DailyShop_/WeeklyShop_ rotations) from the Vanities
title data: every vanity with StoreData.Amount > 0 is offered for that
price in Aurum (AU). AurumShop lists the full priced catalogue (632
items); the daily/weekly ids show a deterministic rotating slice so they
stay stable within a period but change across periods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:46:17 +02:00
neckfireandClaude Opus 4.8 3b108d99ca feat(store): populate shops via GetStoreItems
Build & Deploy / build (push) Successful in 23s
GetStoreItems was a stub returning an empty object, so every shop
(Korolev / ICA / Osiris / QuickShop vendors) showed up empty. Build the
store on the fly from the Blueprints title data: for the requested
StoreId, list every item that declares an ItemShopsCraftingData entry
for that store, converting its currency recipe ingredients into
VirtualCurrencyPrices (SoftCurrency->SC, Aurum->AU, InsuranceCurrency->IN).
Item-ingredient (crafting) entries are left to the crafting UI.

Adds the StoreId/CatalogVersion fields to the request and the
FGetStoreItemsResult / FStoreItem PlayFab response models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:53:37 +02:00
5 changed files with 313 additions and 11 deletions
+63
View File
@@ -0,0 +1,63 @@
# 🎮 The Cycle: Frontier — Rejoindre le serveur privé (installation client)
Guide pour **installer le client et jouer sur le serveur privé**. Tu n'installes **rien côté serveur** — juste le jeu + un loader + un certificat.
> ⚠️ **À savoir avant de commencer**
> - Le jeu a été retiré de Steam en septembre 2022 : il faut **l'avoir déjà eu dans ta bibliothèque Steam** pour le retélécharger.
> - **Ce n'est pas du co-op en raid.** Chacun joue sa propre instance (stations solo) ; l'émulateur ne supporte pas plusieurs joueurs dans le même raid. Vous partagez le même serveur (progression, boutiques, comptes), pas la même partie.
> - Le serveur doit être **allumé** au moment où tu joues (demande à l'hébergeur).
---
## Ce que l'hébergeur doit te fournir
1. **`LoaderPack.zip`** (le loader qui redirige le jeu vers le serveur privé).
2. **`certificate.crt`** (le certificat **public** du serveur — sans clé privée, safe à partager).
---
## Étapes (Windows)
### 1. Installer le client Saison 2
Deux options :
- **Simple** : installe The Cycle: Frontier depuis ta bibliothèque Steam (`steam://install/868270`).
- **Recommandé (client Saison 2 exact)** via la console Steam :
1. Steam ouvert, `Win+R``steam://nav/console`.
2. Va sur les manifests SteamDB du dépôt 868271, cherche `4623363103423775682` (client S2 v2.7.2).
3. Copie la commande **Steam console** et colle-la dans la console Steam → le téléchargement démarre.
### 2. Installer le LoaderPack
1. Va dans le dossier du jeu → **`Prospect\Binaries\Win64`**.
2. Ouvre `LoaderPack.zip` et **glisse tout son contenu** dans ce dossier `Win64`.
3. Crée un **raccourci** vers `Prospect.Client.Loader.exe` (c'est avec ça que tu lanceras le jeu).
### 3. Faire confiance au certificat du serveur
1. Double-clique **`certificate.crt`** → **Installer le certificat**.
2. **Emplacement : Utilisateur actuel** → Suivant.
3. **Placer tous les certificats dans le magasin suivant****Parcourir****Autorités de certification racines de confiance** → OK → Suivant → Terminer.
4. Accepte l'avertissement de sécurité (il mentionne `2EA46.playfabapi.com`).
### 4. Pointer le client vers le serveur
Dans **`Prospect\Binaries\Win64`**, crée (ou édite) un fichier **`backend.txt`** contenant **exactement** cette ligne :
```
https://tc.nfteam.ovh:8443
```
### 5. Lancer le jeu
1. **Steam doit être ouvert** (connecté à ton compte).
2. Lance le jeu via le **raccourci `Prospect.Client.Loader`** (⚠️ pas via Steam directement).
3. Connexion automatique avec ton compte Steam. Bienvenue sur Fortuna III ! 🚀
---
## Dépannage
| Symptôme | Cause / solution |
|---|---|
| `libcurl error 7 (Couldn't connect to server)` | Le serveur est **éteint** → demande à l'hébergeur de l'allumer. |
| Erreur de certificat / `hostname mismatch` | `certificate.crt` **pas importé** (ou pas dans « Autorités racines de confiance »). Refais l'étape 3. |
| `missing license` au téléchargement | Tu n'as jamais eu le jeu sur ce compte Steam → impossible de le retélécharger. |
| Parties du corps manquantes (client S3) | Va en station → change ton apparence une fois (corrige les IDs). |
---
*Serveur privé The Cycle: Frontier — émulateur Prospect. Usage privé entre amis.*
@@ -155,11 +155,119 @@ public class ClientController : Controller
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetStoreItems(FGetStoreItems request)
{
return Ok(new ClientResponse<object>
// The vendors (Korolev / ICA / Osiris / QuickShop) and other stores are defined
// inside the Blueprints title data: each item lists a per-store entry under
// ItemShopsCraftingData keyed by the store id, with the currency price in its
// recipe ingredients. This endpoint was previously a stub returning {}, which is
// why every shop showed up empty. We build the store on the fly from that data.
var storeId = request.StoreId ?? "";
var store = new List<FStoreItem>();
try
{
var titleData = _titleDataService.Find(new List<string> { "Blueprints" });
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
if (blueprints != null && !string.IsNullOrEmpty(storeId))
{
uint position = 0;
foreach (var (itemId, blueprint) in blueprints)
{
if (blueprint.ItemShopsCraftingData == null) continue;
if (!blueprint.ItemShopsCraftingData.TryGetValue(storeId, out var shopData)) continue;
if (shopData?.ItemRecipeIngredients == null) continue;
// Only currency ingredients form a purchasable price. Entries whose
// recipe is item-based (crafting) are handled by the crafting UI from
// the blueprint data the client already has, not by the buy store.
var prices = new Dictionary<string, uint>();
foreach (var ingredient in shopData.ItemRecipeIngredients)
{
var code = ingredient.Currency switch
{
"SoftCurrency" => "SC",
"Aurum" => "AU",
"InsuranceCurrency" => "IN",
_ => null,
};
if (code == null || ingredient.Amount < 0) continue;
prices[code] = prices.TryGetValue(code, out var current)
? current + (uint)ingredient.Amount
: (uint)ingredient.Amount;
}
if (prices.Count == 0) continue;
store.Add(new FStoreItem
{
ItemId = itemId,
VirtualCurrencyPrices = prices,
RealCurrencyPrices = new Dictionary<string, uint>(),
DisplayPosition = position++,
});
}
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to build store items for store {StoreId}", storeId);
}
// Cosmetic / premium stores are backed by the Vanities title data (not Blueprints):
// each vanity carries its price in StoreData.Amount, paid in Aurum. AurumShop lists
// the whole priced catalogue; the daily/weekly rotations (ids from RequestStore
// RotationData) show a deterministic slice so they change over time but stay stable
// within a period.
var isAurumShop = storeId == "AurumShop";
var isDailyShop = storeId.StartsWith("DailyShop_");
var isWeeklyShop = storeId.StartsWith("WeeklyShop_");
if (store.Count == 0 && (isAurumShop || isDailyShop || isWeeklyShop))
{
try
{
var vanityData = _titleDataService.Find(new List<string> { "Vanities" });
var vanities = JsonSerializer.Deserialize<List<TitleDataVanityInfo>>(vanityData["Vanities"]);
var priced = vanities?
.Where(v => v.StoreData != null && v.StoreData.Amount > 0)
.OrderBy(v => v.Name, StringComparer.Ordinal)
.ToList() ?? new List<TitleDataVanityInfo>();
IEnumerable<TitleDataVanityInfo> selection = priced;
if ((isDailyShop || isWeeklyShop) && priced.Count > 0)
{
var size = isWeeklyShop ? 12 : 6;
int.TryParse(storeId[(storeId.IndexOf('_') + 1)..], out var rotation);
var start = (int)(((long)rotation * size) % priced.Count);
selection = Enumerable.Range(0, Math.Min(size, priced.Count))
.Select(i => priced[(start + i) % priced.Count]);
}
uint position = 0;
foreach (var vanity in selection)
{
store.Add(new FStoreItem
{
ItemId = vanity.Name,
VirtualCurrencyPrices = new Dictionary<string, uint> { ["AU"] = (uint)vanity.StoreData.Amount },
RealCurrencyPrices = new Dictionary<string, uint>(),
DisplayPosition = position++,
});
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to build vanity store {StoreId}", storeId);
}
}
_logger.LogInformation("GetStoreItems '{StoreId}': {Count} item(s)", storeId, store.Count);
return Ok(new ClientResponse<FGetStoreItemsResult>
{
Code = 200,
Status = "OK",
Data = new {}
Data = new FGetStoreItemsResult
{
StoreId = string.IsNullOrEmpty(storeId) ? null : storeId,
CatalogVersion = "StaticItems",
Store = store,
}
});
}
@@ -244,13 +352,15 @@ public class ClientController : Controller
public async Task<IActionResult> GetUserInventory(FGetUserInventoryRequest request)
{
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems"});
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems", "CharacterVanity", "GlobalVanity"});
var inventory = JsonSerializer.Deserialize<FYCustomItemInfo[]>(userData["Inventory"].Value);
// TODO: Per-user vanity
// var vanity = JsonSerializer.Deserialize<CustomVanityItem[]>(userData["VanityItems"].Value);
var titleData = _titleDataService.Find(new List<string>{"Blueprints", "Vanities"});
// Per-user vanity ownership: the player owns the cosmetics they've bought (VanityItems)
// plus whatever is currently equipped (CharacterVanity / GlobalVanity), so the starter
// outfit stays owned. Previously the whole Vanities catalogue was returned as owned,
// which made every item in the Aurum shop show up as already "owned".
var ownedVanityIds = CollectOwnedVanityIds(userData);
var titleData = _titleDataService.Find(new List<string>{"Blueprints"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var vanity = JsonSerializer.Deserialize<TitleDataVanityInfo[]>(titleData["Vanities"]);
List<FItemInstance> items = new List<FItemInstance>();
foreach (var item in inventory) {
if (!blueprints.ContainsKey(item.BaseItemId)) {
@@ -276,9 +386,9 @@ public class ClientController : Controller
});
}
foreach (var item in vanity) {
foreach (var vanityId in ownedVanityIds) {
items.Add(new FItemInstance {
ItemId = item.Name,
ItemId = vanityId,
// ItemInstanceId is not required since it's not a unique item.
ItemClass = "Vanity",
// PurchaseDate = DateTime.Now, // TODO
@@ -303,6 +413,60 @@ public class ClientController : Controller
});
}
// Collects the vanity ids a player actually owns: everything they've purchased
// (VanityItems) plus everything currently equipped (CharacterVanity slots and the
// GlobalVanity sprays / banner / emotes / drop pod). Parsed defensively with
// JsonDocument so malformed / partial data never breaks the inventory response.
private static HashSet<string> CollectOwnedVanityIds(Dictionary<string, FUserDataRecord> userData)
{
var owned = new HashSet<string>(StringComparer.Ordinal);
void AddId(string? id) { if (!string.IsNullOrEmpty(id)) owned.Add(id); }
if (userData.TryGetValue("VanityItems", out var viRec) && !string.IsNullOrWhiteSpace(viRec.Value))
{
try
{
using var doc = JsonDocument.Parse(viRec.Value);
if (doc.RootElement.ValueKind == JsonValueKind.Array)
foreach (var el in doc.RootElement.EnumerateArray())
if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty("baseItemId", out var b))
AddId(b.GetString());
}
catch { /* ignore malformed VanityItems */ }
}
if (userData.TryGetValue("CharacterVanity", out var cvRec) && !string.IsNullOrWhiteSpace(cvRec.Value))
{
try
{
using var doc = JsonDocument.Parse(cvRec.Value);
if (doc.RootElement.ValueKind == JsonValueKind.Object)
foreach (var prop in doc.RootElement.EnumerateObject())
if (prop.Value.ValueKind == JsonValueKind.Object && prop.Value.TryGetProperty("id", out var id))
AddId(id.GetString());
}
catch { /* ignore malformed CharacterVanity */ }
}
if (userData.TryGetValue("GlobalVanity", out var gvRec) && !string.IsNullOrWhiteSpace(gvRec.Value))
{
try
{
using var doc = JsonDocument.Parse(gvRec.Value);
var root = doc.RootElement;
if (root.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("activeGlobalVanityIds", out var arr) && arr.ValueKind == JsonValueKind.Array)
foreach (var el in arr.EnumerateArray()) AddId(el.GetString());
if (root.TryGetProperty("droppodId", out var dp)) AddId(dp.GetString());
}
}
catch { /* ignore malformed GlobalVanity */ }
}
return owned;
}
[HttpPost("GetTitleData")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace Prospect.Server.Api.Models.Client.Data;
// PlayFab StoreItem: a single catalog item offered for sale in a store, with its prices.
public class FStoreItem
{
/// <summary>
/// [optional] Store specific custom data. The data only exists as part of this store; it is not transferred to item instances.
/// </summary>
[JsonPropertyName("CustomData")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public object? CustomData { get; set; }
/// <summary>
/// [optional] Intended display position for this item. Note that 0 is the first position and -1 is the last position.
/// </summary>
[JsonPropertyName("DisplayPosition")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint? DisplayPosition { get; set; }
/// <summary>
/// Unique identifier of the item as it exists in the catalog - note that this must exactly match the ItemId from the catalog.
/// </summary>
[JsonPropertyName("ItemId")]
public string ItemId { get; set; } = "";
/// <summary>
/// [optional] Override prices for this item for specific currencies.
/// </summary>
[JsonPropertyName("RealCurrencyPrices")]
public Dictionary<string, uint> RealCurrencyPrices { get; set; } = new();
/// <summary>
/// [optional] Override prices for this item in virtual currencies and "RM" (the base Real Money purchase price, in USD pennies).
/// </summary>
[JsonPropertyName("VirtualCurrencyPrices")]
public Dictionary<string, uint> VirtualCurrencyPrices { get; set; } = new();
}
@@ -1,8 +1,19 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
namespace Prospect.Server.Api.Models.Client;
public class FGetStoreItems
{
}
/// <summary>
/// [optional] Catalog version to store items from. Use default catalog version if null.
/// </summary>
[JsonPropertyName("CatalogVersion")]
public string? CatalogVersion { get; set; }
/// <summary>
/// Unique identifier for the store which is being requested.
/// </summary>
[JsonPropertyName("StoreId")]
public string? StoreId { get; set; }
}
@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
namespace Prospect.Server.Api.Models.Client;
public class FGetStoreItemsResult
{
/// <summary>
/// [optional] The base catalog that this store is a part of.
/// </summary>
[JsonPropertyName("CatalogVersion")]
public string? CatalogVersion { get; set; }
/// <summary>
/// [optional] Array of items which can be purchased from this store.
/// </summary>
[JsonPropertyName("Store")]
public List<FStoreItem> Store { get; set; } = new();
/// <summary>
/// [optional] The ID of this store.
/// </summary>
[JsonPropertyName("StoreId")]
public string? StoreId { get; set; }
}