Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e006f84f | ||
|
|
3cdf8bc7e2 | ||
|
|
5d93b3541d | ||
|
|
40dad82559 | ||
|
|
3b108d99ca |
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user