From 0588849e4cc6adade6a07c3e3052d49bece4047a Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 15:34:30 +0200 Subject: [PATCH 01/12] feat(squad): in-memory squad/lobby state + wire squad functions Adds SquadService (singleton, process-local) tracking squads by the squadId the client shares across party members, plus a user->squad index so TryGetCompleteSquadInfo (no squadId in request) can resolve the caller's squad. Members are lazily registered on any squad call. - TryGetCompleteSquadInfo: returns the caller's real squad. - SquadMemberReadyForMatch: joins the squad, records readiness/map, returns the squad + whether everyone is ready. - SquadMemberSelectedMap: records the member's selected map. - DbUserService.FindByIdAsync: resolve display name for squad members. Groundwork for the station lobby; assumes the client drives squads via these calls with a shared squadId (to confirm with a 2-client session). Real-time SignalR push (unknown event names) deferred; the squad is carried in responses + TryGetCompleteSquadInfo for now. Co-Authored-By: Claude Opus 4.8 --- .../Functions/SquadMemberReadyForMatch.cs | 41 ++++---- .../Functions/SquadMemberSelectedMap.cs | 24 ++++- .../Functions/TryGetCompleteSquadInfo.cs | 23 +++-- .../Services/Database/DbUserService.cs | 10 +- .../Services/Squad/SquadService.cs | 99 +++++++++++++++++++ src/Prospect.Server.Api/Startup.cs | 2 + 6 files changed, 166 insertions(+), 33 deletions(-) create mode 100644 src/Prospect.Server.Api/Services/Squad/SquadService.cs diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs index b86fb2c..f36a28a 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/SquadMemberReadyForMatch.cs @@ -1,6 +1,9 @@ using System.Text.Json.Serialization; using Prospect.Server.Api.Services.CloudScript; using Prospect.Server.Api.Services.Auth.Extensions; +using Prospect.Server.Api.Services.Database; +using Prospect.Server.Api.Services.Database.Models; +using Prospect.Server.Api.Services.Squad; using Microsoft.AspNetCore.SignalR; using Prospect.Server.Api.Hubs; @@ -28,12 +31,16 @@ public class SquadMemberReadyForMatchFunction : ICloudScriptFunction _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHubContext _hubContext; + private readonly SquadService _squadService; + private readonly DbUserService _userService; - public SquadMemberReadyForMatchFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, IHubContext hubContext) + public SquadMemberReadyForMatchFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, IHubContext hubContext, SquadService squadService, DbUserService userService) { _httpContextAccessor = httpContextAccessor; _logger = logger; _hubContext = hubContext; + _squadService = squadService; + _userService = userService; } public async Task ExecuteAsync(SquadMemberReadyForMatchRequest request) @@ -44,27 +51,25 @@ public class SquadMemberReadyForMatchFunction : ICloudScriptFunction { private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SquadService _squadService; - public SquadMemberSelectedMapFunction(ILogger logger) + public SquadMemberSelectedMapFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, SquadService squadService) { _logger = logger; + _httpContextAccessor = httpContextAccessor; + _squadService = squadService; } - public async Task ExecuteAsync(SquadMemberSelectedMapRequest request) + public Task ExecuteAsync(SquadMemberSelectedMapRequest request) { - return new SquadMemberSelectedMapResponse - {}; + var context = _httpContextAccessor.HttpContext; + if (context != null && !string.IsNullOrEmpty(request.SquadID)) + { + var userId = context.User.FindAuthUserId(); + _squadService.JoinOrCreate(request.SquadID, userId, ""); + _squadService.SetMap(request.SquadID, userId, request.SelectedMapName ?? ""); + _logger.LogInformation("SquadMemberSelectedMap: {User} -> {Map} (squad {Squad})", userId, request.SelectedMapName, request.SquadID); + } + return Task.FromResult(new SquadMemberSelectedMapResponse { }); } } diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs index c73d420..9a4fc94 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/TryGetCompleteSquadInfo.cs @@ -1,5 +1,8 @@ using System.Text.Json.Serialization; +using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript; +using Prospect.Server.Api.Services.Database; +using Prospect.Server.Api.Services.Squad; public class TryGetCompleteSquadInfoRequest { @@ -51,26 +54,30 @@ public class TryGetCompleteSquadInfoFunction : ICloudScriptFunction _logger; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SquadService _squadService; - public TryGetCompleteSquadInfoFunction(ILogger logger, IHttpContextAccessor httpContextAccessor) + public TryGetCompleteSquadInfoFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, SquadService squadService) { _httpContextAccessor = httpContextAccessor; _logger = logger; + _squadService = squadService; } - public async Task ExecuteAsync(TryGetCompleteSquadInfoRequest request) + public Task ExecuteAsync(TryGetCompleteSquadInfoRequest request) { var context = _httpContextAccessor.HttpContext; if (context == null) { throw new CloudScriptException("CloudScript was not called within a http request"); } - // var userId = context.User.FindAuthUserId(); - - return new FYPlayFabSquad + var userId = context.User.FindAuthUserId(); + var squad = _squadService.GetByUser(userId); + if (squad == null) { - // SquadID = "100", - // Members = [], - }; + // Not in a squad yet — return an empty squad (same as the previous stub). + return Task.FromResult(new FYPlayFabSquad()); + } + _logger.LogInformation("TryGetCompleteSquadInfo: user {User} in squad {Squad} ({Count} member(s))", userId, squad.SquadId, squad.Members.Count); + return Task.FromResult(_squadService.ToClientSquad(squad)); } } diff --git a/src/Prospect.Server.Api/Services/Database/DbUserService.cs b/src/Prospect.Server.Api/Services/Database/DbUserService.cs index 9feee53..206e58f 100644 --- a/src/Prospect.Server.Api/Services/Database/DbUserService.cs +++ b/src/Prospect.Server.Api/Services/Database/DbUserService.cs @@ -13,11 +13,17 @@ public class DbUserService : BaseDbService public async Task FindAsync(PlayFabUserAuthType type, string key) { - return await Collection.Find(user => user.Auth.Any(auth => - auth.Type == type && + return await Collection.Find(user => user.Auth.Any(auth => + auth.Type == type && auth.Key == key)).FirstOrDefaultAsync(); } + // Resolve a player by its PlayFab id (used to display squad member names). + public async Task FindByIdAsync(string id) + { + return await Collection.Find(user => user.Id == id).FirstOrDefaultAsync(); + } + private async Task CreateAsync(PlayFabUserAuthType type, string key) { var user = new PlayFabUser diff --git a/src/Prospect.Server.Api/Services/Squad/SquadService.cs b/src/Prospect.Server.Api/Services/Squad/SquadService.cs new file mode 100644 index 0000000..d43ca80 --- /dev/null +++ b/src/Prospect.Server.Api/Services/Squad/SquadService.cs @@ -0,0 +1,99 @@ +using System.Collections.Concurrent; + +namespace Prospect.Server.Api.Services.Squad; + +// In-memory squad/lobby state. Squads are keyed by the squadId the client provides on the +// SquadMember* calls (the client forms the party and shares a squadId; each member's client +// then calls the backend with it). A member is (lazily) registered into that squad on any +// squad call, and the current squad for a user is tracked so TryGetCompleteSquadInfo — which +// carries no squadId — can still return it. State is process-local (fine for a small server). +public class SquadMemberInfo +{ + public string UserId { get; set; } = ""; + public string DisplayName { get; set; } = ""; + public int OnlineState { get; set; } + public bool IsReady { get; set; } + public string SelectedMap { get; set; } = ""; + public bool IsLeader { get; set; } +} + +public class SquadInfo +{ + public string SquadId { get; set; } = ""; + public ConcurrentDictionary Members { get; } = new(); +} + +public class SquadService +{ + private readonly ConcurrentDictionary _squads = new(); + private readonly ConcurrentDictionary _userToSquad = new(); + + public SquadInfo JoinOrCreate(string squadId, string userId, string displayName) + { + var squad = _squads.GetOrAdd(squadId, id => new SquadInfo { SquadId = id }); + var isFirst = squad.Members.IsEmpty; + var member = squad.Members.GetOrAdd(userId, uid => new SquadMemberInfo { UserId = uid, IsLeader = isFirst }); + if (!string.IsNullOrEmpty(displayName)) member.DisplayName = displayName; + _userToSquad[userId] = squadId; + return squad; + } + + public SquadInfo? GetByUser(string userId) + => _userToSquad.TryGetValue(userId, out var sid) && _squads.TryGetValue(sid, out var sq) ? sq : null; + + public SquadInfo? Get(string squadId) + => _squads.TryGetValue(squadId, out var sq) ? sq : null; + + public void SetReady(string squadId, string userId, bool ready, string? selectedMap) + { + if (_squads.TryGetValue(squadId, out var sq) && sq.Members.TryGetValue(userId, out var m)) + { + m.IsReady = ready; + if (!string.IsNullOrEmpty(selectedMap)) m.SelectedMap = selectedMap; + } + } + + public void SetMap(string squadId, string userId, string map) + { + if (_squads.TryGetValue(squadId, out var sq) && sq.Members.TryGetValue(userId, out var m)) + m.SelectedMap = map; + } + + public bool IsSquadReady(string squadId) + => _squads.TryGetValue(squadId, out var sq) && !sq.Members.IsEmpty && sq.Members.Values.All(m => m.IsReady); + + public void Leave(string userId) + { + if (_userToSquad.TryRemove(userId, out var sid) && _squads.TryGetValue(sid, out var sq)) + { + sq.Members.TryRemove(userId, out _); + if (sq.Members.IsEmpty) _squads.TryRemove(sid, out _); + } + } + + // Build the client-facing squad object (models live in the global namespace). + public FYPlayFabSquad ToClientSquad(SquadInfo squad) + { + return new FYPlayFabSquad + { + SquadID = squad.SquadId, + Members = squad.Members.Values.Select(m => new FYPlayFabSquadMember + { + Profile = new FYPlayFabPlayerProfile + { + PlayerId = m.UserId, + DisplayName = m.DisplayName, + AvatarUrl = "", + }, + onlineState = m.OnlineState, + matchmakingSettings = new FYUserMatchmakingSettings + { + isReadyForMatch = m.IsReady, + selectedMapName = m.SelectedMap, + isSecretLeader = m.IsLeader, + }, + mapRowNamesUnlocked = Array.Empty(), + }).ToArray(), + }; + } +} diff --git a/src/Prospect.Server.Api/Startup.cs b/src/Prospect.Server.Api/Startup.cs index 7d7e3fc..a294d5e 100644 --- a/src/Prospect.Server.Api/Startup.cs +++ b/src/Prospect.Server.Api/Startup.cs @@ -35,6 +35,8 @@ public class Startup services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); services.AddSingleton(); From b04621e2a0febf352d3a8197fb479a712a34c579 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 15:53:43 +0200 Subject: [PATCH 02/12] feat(tool): cross-platform server switcher (Prospect.Client.Config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New console utility (net8.0, Linux/Proton + Windows) to point a client at a server by writing backend.txt — the single value the Prospect agent reads. Presets for PROD/PREPROD LAN and PROD internet, plus custom URL, certificate import (native on Windows, guided with Proton-prefix detection on Linux) and game launch. Interactive menu + scriptable CLI (--folder/--set). Added to the solution; not built by the server CI. Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Client.Config/Program.cs | 260 ++++++++++++++++++ .../Prospect.Client.Config.csproj | 15 + src/Prospect.sln | 26 ++ 3 files changed, 301 insertions(+) create mode 100644 src/Prospect.Client.Config/Program.cs create mode 100644 src/Prospect.Client.Config/Prospect.Client.Config.csproj diff --git a/src/Prospect.Client.Config/Program.cs b/src/Prospect.Client.Config/Program.cs new file mode 100644 index 0000000..a04653f --- /dev/null +++ b/src/Prospect.Client.Config/Program.cs @@ -0,0 +1,260 @@ +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Json; + +namespace Prospect.Client.Config; + +// Cross-platform (Linux/Proton + Windows) helper to point a The Cycle: Frontier client at a +// server. The Prospect agent reads the target PlayFab URL from `backend.txt` next to the game +// executable, so switching servers = writing that one file. This tool also imports the server +// certificate and launches the game (natively on Windows, guided on Linux/Proton). +internal static class Program +{ + private const string AppId = "868270"; + + // Presets. LAN entries match a direct homelab connection (192.168.1.136); the internet + // entry (public domain) is for friends connecting from outside — the TLS cert covers the + // hostname and :8443 bypasses the reverse proxy. + private static readonly (string Key, string Label, string Url)[] Presets = + { + ("prod-lan", "PROD (LAN)", "https://192.168.1.136:8443"), + ("preprod-lan", "PREPROD (LAN)", "https://192.168.1.136:8444"), + ("prod", "PROD (amis / internet)", "https://tc.nfteam.ovh:8443"), + }; + + private static string SettingsPath => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "ProspectSwitcher", "settings.json"); + + private static Settings _settings = new(); + + private static int Main(string[] args) + { + try { Console.OutputEncoding = Encoding.UTF8; } catch { /* some terminals reject it */ } + LoadSettings(); + return args.Length > 0 ? RunCli(args) : MenuLoop(); + } + + // ---- Scriptable CLI ---------------------------------------------------- + private static int RunCli(string[] args) + { + string? folder = null, target = null; + for (var i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--folder" when i + 1 < args.Length: folder = args[++i]; break; + case "--set" when i + 1 < args.Length: target = args[++i]; break; + case "-h" or "--help": PrintCliHelp(); return 0; + } + } + if (folder != null) { _settings.GameFolder = folder; SaveSettings(); } + if (target == null) { PrintCliHelp(); return folder != null ? 0 : 1; } + var url = ResolveTarget(target); + if (url == null) { Console.Error.WriteLine("Cible inconnue : " + target); return 1; } + return WriteBackend(url) ? 0 : 1; + } + + private static void PrintCliHelp() + { + Console.WriteLine("ProspectServerSwitcher — sélecteur de serveur The Cycle (multi-OS)"); + Console.WriteLine(); + Console.WriteLine(" (sans argument) menu interactif"); + Console.WriteLine(" --folder définit le dossier du jeu (mémorisé)"); + Console.WriteLine(" --set écrit backend.txt"); + Console.WriteLine(); + Console.WriteLine("Cibles : " + string.Join(", ", Presets.Select(p => p.Key)) + ", ou une URL https://…"); + Console.WriteLine("Ex : ProspectServerSwitcher --folder \"…/Prospect/Binaries/Win64\" --set preprod-lan"); + } + + private static string? ResolveTarget(string t) + { + foreach (var p in Presets) + if (string.Equals(p.Key, t, StringComparison.OrdinalIgnoreCase)) return p.Url; + if (t.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return t; + return null; + } + + // ---- Interactive menu -------------------------------------------------- + private static int MenuLoop() + { + while (true) + { + Console.WriteLine(); + Console.WriteLine("=== The Cycle — Sélecteur de serveur ==="); + Console.WriteLine("Dossier du jeu : " + (string.IsNullOrEmpty(_settings.GameFolder) ? "(non défini)" : _settings.GameFolder)); + Console.WriteLine("backend.txt : " + ReadCurrentBackend()); + Console.WriteLine(); + Console.WriteLine(" 1) Définir le dossier du jeu (Win64)"); + for (var i = 0; i < Presets.Length; i++) + Console.WriteLine($" {i + 2}) {Presets[i].Label,-24} {Presets[i].Url}"); + var n = Presets.Length + 2; + Console.WriteLine($" {n}) Serveur personnalisé…"); + Console.WriteLine($" {n + 1}) Importer un certificat…"); + Console.WriteLine($" {n + 2}) Lancer le jeu"); + Console.WriteLine(" 0) Quitter"); + Console.Write("> "); + + var raw = Console.ReadLine()?.Trim(); + if (raw is null or "0") return 0; + if (!int.TryParse(raw, out var choice)) { Console.WriteLine("Choix invalide."); continue; } + + if (choice == 1) SetFolder(); + else if (choice >= 2 && choice < n) WriteBackend(Presets[choice - 2].Url); + else if (choice == n) SetCustom(); + else if (choice == n + 1) ImportCert(); + else if (choice == n + 2) LaunchGame(); + else Console.WriteLine("Choix invalide."); + } + } + + private static void SetFolder() + { + Console.Write("Chemin du dossier Win64 : "); + var p = Console.ReadLine()?.Trim().Trim('"'); + if (string.IsNullOrEmpty(p)) return; + if (!Directory.Exists(p)) { Console.WriteLine("⚠ Dossier introuvable."); return; } + _settings.GameFolder = p; + SaveSettings(); + Console.WriteLine("✔ Dossier mémorisé."); + } + + private static void SetCustom() + { + Console.Write("URL du serveur (ex : https://mon-serveur:8443) : "); + var u = Console.ReadLine()?.Trim(); + if (!string.IsNullOrWhiteSpace(u)) WriteBackend(u); + } + + // ---- backend.txt ------------------------------------------------------- + private static bool WriteBackend(string url) + { + if (!EnsureFolder(out var dir)) return false; + try + { + // No trailing newline: the agent reads the raw file and requires it to start with https://. + File.WriteAllText(Path.Combine(dir, "backend.txt"), url, new UTF8Encoding(false)); + Console.WriteLine($"✔ backend.txt → {url}"); + Console.WriteLine(" (relance le jeu : backend.txt n'est lu qu'au démarrage du loader.)"); + return true; + } + catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return false; } + } + + private static string ReadCurrentBackend() + { + if (string.IsNullOrEmpty(_settings.GameFolder)) return "(dossier non défini)"; + var f = Path.Combine(_settings.GameFolder, "backend.txt"); + try + { + if (!File.Exists(f)) return "(absent)"; + var v = File.ReadAllText(f).Trim(); + var match = Presets.FirstOrDefault(p => p.Url == v); + return match.Key != null ? $"{v} [{match.Label.Trim()}]" : v; + } + catch { return "(illisible)"; } + } + + // ---- Certificate ------------------------------------------------------- + private static void ImportCert() + { + Console.Write("Chemin du certificat (.crt) : "); + var path = Console.ReadLine()?.Trim().Trim('"'); + if (string.IsNullOrEmpty(path) || !File.Exists(path)) { Console.WriteLine("⚠ Fichier introuvable."); return; } + + if (OperatingSystem.IsWindows()) + { + ImportCertWindows(path); + return; + } + + // Linux/Proton : le certificat doit être approuvé DANS le préfixe Wine du jeu. + var prefix = FindProtonPrefix() ?? $"~/.steam/steam/steamapps/compatdata/{AppId}/pfx"; + Console.WriteLine("Sous Proton/Wine, ajoute le certificat au préfixe du jeu (wine requis) :"); + Console.WriteLine($" WINEPREFIX=\"{prefix}\" wine certutil -addstore -f Root \"{path}\""); + } + + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private static void ImportCertWindows(string path) + { + try + { + using var cert = new X509Certificate2(path); + using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + store.Add(cert); + Console.WriteLine("✔ Certificat importé (Autorités racines de confiance, utilisateur)."); + } + catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); } + } + + private static string? FindProtonPrefix() + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + foreach (var dir in new[] + { + Path.Combine(home, ".steam", "steam", "steamapps", "compatdata", AppId, "pfx"), + Path.Combine(home, ".local", "share", "Steam", "steamapps", "compatdata", AppId, "pfx"), + }) + if (Directory.Exists(dir)) return dir; + return null; + } + + // ---- Launch ------------------------------------------------------------ + private static void LaunchGame() + { + if (OperatingSystem.IsWindows()) + { + if (!EnsureFolder(out var dir)) return; + var loader = Path.Combine(dir, "Prospect.Client.Loader.exe"); + if (!File.Exists(loader)) { Console.WriteLine("⚠ Prospect.Client.Loader.exe introuvable dans le dossier."); return; } + try + { + Process.Start(new ProcessStartInfo(loader) { WorkingDirectory = dir, UseShellExecute = true }); + Console.WriteLine("▶ Jeu lancé."); + } + catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); } + return; + } + Console.WriteLine("Sous Linux/Proton, lance via Steam (le loader doit tourner dans le préfixe Proton) :"); + Console.WriteLine(" steam steam://rungameid/" + AppId); + } + + // ---- Helpers ----------------------------------------------------------- + private static bool EnsureFolder(out string dir) + { + dir = _settings.GameFolder; + if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) + { + Console.WriteLine("⚠ Définis d'abord le dossier du jeu (option 1)."); + return false; + } + return true; + } + + private static void LoadSettings() + { + try + { + if (File.Exists(SettingsPath)) + _settings = JsonSerializer.Deserialize(File.ReadAllText(SettingsPath)) ?? new Settings(); + } + catch { _settings = new Settings(); } + } + + private static void SaveSettings() + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!); + File.WriteAllText(SettingsPath, JsonSerializer.Serialize(_settings)); + } + catch { /* best-effort persistence */ } + } + + private sealed class Settings + { + public string GameFolder { get; set; } = ""; + } +} diff --git a/src/Prospect.Client.Config/Prospect.Client.Config.csproj b/src/Prospect.Client.Config/Prospect.Client.Config.csproj new file mode 100644 index 0000000..c738d67 --- /dev/null +++ b/src/Prospect.Client.Config/Prospect.Client.Config.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + ProspectServerSwitcher + Prospect.Client.Config + true + + Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug + + + diff --git a/src/Prospect.sln b/src/Prospect.sln index daef9cf..2f6d277 100644 --- a/src/Prospect.sln +++ b/src/Prospect.sln @@ -25,6 +25,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "P EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Agent", "Prospect.Agent\Prospect.Agent.vcxproj", "{A9BA7D25-F239-4320-A0DC-85E8001FD669}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Client.Config", "Prospect.Client.Config\Prospect.Client.Config.csproj", "{6DECEB07-996B-4EBC-927C-13E170161107}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -233,6 +235,30 @@ Global {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.Build.0 = Release|x64 {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.ActiveCfg = Release|x64 {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.Build.0 = Release|x64 + {6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x64.Build.0 = Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x86.Build.0 = Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Release|x64.ActiveCfg = Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Release|x64.Build.0 = Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Release|x86.ActiveCfg = Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Release|x86.Build.0 = Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU + {6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 2cc90a22e0d17304746a00a7a082fc8b14d01100 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 15:57:51 +0200 Subject: [PATCH 03/12] feat(tool): use public domains as presets (works LAN + internet) Switcher presets now use the public domains (tc.nfteam.ovh:8443 / rd-tc.nfteam.ovh:8444) so the same target works for everyone; the unified TLS cert covers both hostnames. LAN users add a hosts entry to avoid hairpin NAT. Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Client.Config/Program.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Prospect.Client.Config/Program.cs b/src/Prospect.Client.Config/Program.cs index a04653f..3df71cd 100644 --- a/src/Prospect.Client.Config/Program.cs +++ b/src/Prospect.Client.Config/Program.cs @@ -13,14 +13,13 @@ internal static class Program { private const string AppId = "868270"; - // Presets. LAN entries match a direct homelab connection (192.168.1.136); the internet - // entry (public domain) is for friends connecting from outside — the TLS cert covers the - // hostname and :8443 bypasses the reverse proxy. + // Presets use the public domains so the same target works for everyone (LAN or internet): + // DNS resolves to the public IP for friends, and the unified TLS cert covers both hostnames. + // On the same LAN, add a hosts entry (domain -> 192.168.1.136) to avoid hairpin NAT. private static readonly (string Key, string Label, string Url)[] Presets = { - ("prod-lan", "PROD (LAN)", "https://192.168.1.136:8443"), - ("preprod-lan", "PREPROD (LAN)", "https://192.168.1.136:8444"), - ("prod", "PROD (amis / internet)", "https://tc.nfteam.ovh:8443"), + ("prod", "PROD", "https://tc.nfteam.ovh:8443"), + ("preprod", "PREPROD", "https://rd-tc.nfteam.ovh:8444"), }; private static string SettingsPath => Path.Combine( From 2809033b7628c3a3471755ca240ae423592ddd01 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 16:03:24 +0200 Subject: [PATCH 04/12] docs(tool): README for ProspectServerSwitcher Documents the cross-platform server switcher: backend.txt mechanism, domain presets, interactive/CLI usage, cert import (Windows / Proton), launch, settings location and self-contained publish commands. Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Client.Config/README.md | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/Prospect.Client.Config/README.md diff --git a/src/Prospect.Client.Config/README.md b/src/Prospect.Client.Config/README.md new file mode 100644 index 0000000..246f04a --- /dev/null +++ b/src/Prospect.Client.Config/README.md @@ -0,0 +1,93 @@ +# ProspectServerSwitcher + +Petit utilitaire **multi-OS** (Linux/Proton + Windows) pour pointer un client +*The Cycle: Frontier* vers un serveur, importer le certificat, et lancer le jeu. + +## Pourquoi + +L'agent Prospect (`Prospect.Agent.dll`, injecté par `Prospect.Client.Loader.exe`) +hooke l'URL de l'API PlayFab et la remplace par le contenu de **`backend.txt`**, +placé à côté de l'exécutable du jeu (`Prospect/Binaries/Win64`). S'il est absent, +l'agent retombe sur `https://127.0.0.1:8443`. + +**Tout l'aiguillage du client tient donc dans cette seule valeur.** Cet outil ne fait +essentiellement qu'écrire le bon `backend.txt` — plus quelques commodités (cert, launch). + +## Serveurs (presets) + +Les presets utilisent les **domaines publics** pour que la même cible marche pour +tout le monde (le DNS résout vers l'IP publique pour les amis, et le certificat TLS +unifié couvre les deux hostnames) : + +| Preset | URL | +|-----------|----------------------------------| +| `prod` | `https://tc.nfteam.ovh:8443` | +| `preprod` | `https://rd-tc.nfteam.ovh:8444` | + +> **Sur le même LAN que le serveur**, ajoute une entrée hosts pour éviter le hairpin NAT : +> ``` +> 192.168.1.136 tc.nfteam.ovh +> 192.168.1.136 rd-tc.nfteam.ovh +> ``` + +## Utilisation + +### Menu interactif +Lance le binaire sans argument : +- **1** : définir le dossier du jeu (`…/Prospect/Binaries/Win64`) — mémorisé +- **2/3** : basculer sur PROD / PREPROD +- **4** : URL personnalisée +- **5** : importer un certificat (`.crt`) +- **6** : lancer le jeu +- **0** : quitter + +### En ligne de commande (scriptable) +``` +ProspectServerSwitcher --folder "<...>/Prospect/Binaries/Win64" --set preprod +ProspectServerSwitcher --set prod +ProspectServerSwitcher --set https://mon-serveur:8443 # URL libre +``` + +> ⚠️ `backend.txt` n'est lu qu'**au démarrage** du loader → relance le jeu après un switch. +> Le serveur ciblé doit **tourner** (conteneurs on-demand `the-cycle-api` / `the-cycle-api-rd`). + +## Certificat + +- **Windows** : import automatique dans *Autorités de certification racines de confiance* + (utilisateur courant). +- **Linux/Proton** : le certificat doit être approuvé **dans le préfixe Wine** du jeu. + L'outil détecte le préfixe Proton de l'app `868270` et affiche la commande prête : + ``` + WINEPREFIX="…/compatdata/868270/pfx" wine certutil -addstore -f Root "certificate.crt" + ``` + +## Lancement + +- **Windows** : démarre `Prospect.Client.Loader.exe` depuis le dossier du jeu. +- **Linux/Proton** : passe par Steam (`steam steam://rungameid/868270`) — le loader doit + tourner dans le préfixe Proton. + +## Réglages + +Le dossier du jeu est mémorisé dans +`…/ApplicationData/ProspectSwitcher/settings.json` +(`%AppData%` sous Windows, `~/.config` sous Linux). + +## Build / publication + +Projet console **net8.0** (cross-platform, `InvariantGlobalization`), jamais buildé par +la CI serveur (Linux). + +```bash +# build simple +dotnet build src/Prospect.Client.Config -c Release + +# binaires autonomes single-file (aucun .NET requis chez l'utilisateur) +dotnet publish src/Prospect.Client.Config -c Release -r win-x64 --self-contained \ + -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true +dotnet publish src/Prospect.Client.Config -c Release -r linux-x64 --self-contained \ + -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true +``` + +Les binaires publiés sont distribués via la release Gitea **`friends-client`** +(`ProspectServerSwitcher.exe` pour Windows, `ProspectServerSwitcher-linux-x64` pour Linux). From 0089833b74c1fc06e5bb0ad849715b4b3ad16ea4 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 16:18:35 +0200 Subject: [PATCH 05/12] fix(signalr): return LAN SignalR URL to local clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetSignalRConnection now serves the SignalR URL on 192.168.1.136 to clients whose remote IP is on the LAN, and keeps the public domain for external clients. The real-time WebSocket (libwebsockets, incl. under Proton/Wine) couldn't reach the public domain from inside the LAN (hairpin NAT) — it failed right after the intro as "error code 5". External friends are unaffected (still get the domain). Co-Authored-By: Claude Opus 4.8 --- .../Functions/GetSignalRConnection.cs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs index 4808610..4e404bf 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Options; using Prospect.Server.Api.Config; using Prospect.Server.Api.Services.CloudScript.Models; @@ -7,20 +9,52 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions; [CloudScriptFunction("GetSignalRConnection")] public class GetSignalRConnection : ICloudScriptFunction { - private readonly PlayFabSettings _settings; + // LAN address of the homelab host. Clients on the local network get the SignalR URL on + // this IP (which their cert already validates via the IP/DNS SAN), while external clients + // keep the public domain from configuration. This matters because the real-time WebSocket + // (libwebsockets, incl. under Proton/Wine) can't reach the public domain from inside the + // LAN (hairpin NAT), which surfaced as "error code 5" right after the intro. + private const string LanHost = "192.168.1.136"; - public GetSignalRConnection(IOptions settings) + private readonly PlayFabSettings _settings; + private readonly IHttpContextAccessor _httpContextAccessor; + + public GetSignalRConnection(IOptions settings, IHttpContextAccessor httpContextAccessor) { _settings = settings.Value; + _httpContextAccessor = httpContextAccessor; } public Task ExecuteAsync(FYGetSignalRConnection request) { - // The game client connects to SignalR only over HTTPS + // The game client connects to SignalR only over HTTPS. + var url = _settings.SignalRURL; + + var remote = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; + if (remote != null && IsLan(remote)) + { + try + { + url = new UriBuilder(url) { Host = LanHost }.Uri.AbsoluteUri; + } + catch { /* keep the configured URL on any parsing issue */ } + } + return Task.FromResult(new FYGetSignalRConnectionResult { - Url = _settings.SignalRURL, + Url = url, AccessToken = _settings.SignalRAccessToken }); } -} \ No newline at end of file + + private static bool IsLan(IPAddress ip) + { + if (IPAddress.IsLoopback(ip)) return true; + if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4(); + if (ip.AddressFamily != AddressFamily.InterNetwork) return false; + var b = ip.GetAddressBytes(); + return b[0] == 10 + || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) + || (b[0] == 192 && b[1] == 168); + } +} From 6d77bb4b823ae67a8a8431a1cc50fc19bd120bda Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 16:24:49 +0200 Subject: [PATCH 06/12] chore(signalr): log remote IP + resolved URL for diagnostics Co-Authored-By: Claude Opus 4.8 --- .../CloudScript/Functions/GetSignalRConnection.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs index 4e404bf..302958c 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs @@ -18,11 +18,13 @@ public class GetSignalRConnection : ICloudScriptFunction _logger; - public GetSignalRConnection(IOptions settings, IHttpContextAccessor httpContextAccessor) + public GetSignalRConnection(IOptions settings, IHttpContextAccessor httpContextAccessor, ILogger logger) { _settings = settings.Value; _httpContextAccessor = httpContextAccessor; + _logger = logger; } public Task ExecuteAsync(FYGetSignalRConnection request) @@ -31,7 +33,8 @@ public class GetSignalRConnection : ICloudScriptFunction {Url}", remote, lan, url); + return Task.FromResult(new FYGetSignalRConnectionResult { Url = url, From 61e45351cd24b2ec727d255471fa2dbde9a48b0a Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 16:48:49 +0200 Subject: [PATCH 07/12] feat(tool): import cert into the Proton prefix on Linux On Linux the tool now actually trusts the server certificate inside the game's Proton/Wine prefix (the wine certutil tool is broken under Proton): it fetches the live cert from the server (backend.txt), builds the wine serialized cert blob (SHA1 + encoded cert) and writes it to the prefix Root store via `wine reg import`. Auto-detects the Proton wine and lists compatdata prefixes (most-recent first) so the non-Steam-shortcut prefix is picked correctly; the chosen prefix is remembered. Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Client.Config/Program.cs | 187 +++++++++++++++++++++++--- 1 file changed, 166 insertions(+), 21 deletions(-) diff --git a/src/Prospect.Client.Config/Program.cs b/src/Prospect.Client.Config/Program.cs index 3df71cd..e3935da 100644 --- a/src/Prospect.Client.Config/Program.cs +++ b/src/Prospect.Client.Config/Program.cs @@ -158,28 +158,56 @@ internal static class Program // ---- Certificate ------------------------------------------------------- private static void ImportCert() { - Console.Write("Chemin du certificat (.crt) : "); - var path = Console.ReadLine()?.Trim().Trim('"'); - if (string.IsNullOrEmpty(path) || !File.Exists(path)) { Console.WriteLine("⚠ Fichier introuvable."); return; } - - if (OperatingSystem.IsWindows()) + // Prefer fetching the cert live from the server the client points at (backend.txt), so + // it's always the current one; fall back to a .crt file. + X509Certificate2? cert = null; + var target = ReadBackendUrl(); + if (target != null && TryFetchServerCert(target, out cert)) + Console.WriteLine($"Certificat récupéré depuis {target}"); + else { - ImportCertWindows(path); - return; + Console.Write("Chemin d'un certificat (.crt) : "); + var p = Console.ReadLine()?.Trim().Trim('"'); + if (string.IsNullOrEmpty(p) || !File.Exists(p)) { Console.WriteLine("⚠ Annulé."); return; } + try { cert = new X509Certificate2(p); } catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return; } } + if (cert == null) { Console.WriteLine("⚠ Aucun certificat."); return; } + Console.WriteLine($" empreinte : {cert.Thumbprint}"); - // Linux/Proton : le certificat doit être approuvé DANS le préfixe Wine du jeu. - var prefix = FindProtonPrefix() ?? $"~/.steam/steam/steamapps/compatdata/{AppId}/pfx"; - Console.WriteLine("Sous Proton/Wine, ajoute le certificat au préfixe du jeu (wine requis) :"); - Console.WriteLine($" WINEPREFIX=\"{prefix}\" wine certutil -addstore -f Root \"{path}\""); + if (OperatingSystem.IsWindows()) ImportCertWindows(cert); + else ImportCertProton(cert); + } + + private static string? ReadBackendUrl() + { + if (string.IsNullOrEmpty(_settings.GameFolder)) return null; + var f = Path.Combine(_settings.GameFolder, "backend.txt"); + try { return File.Exists(f) ? File.ReadAllText(f).Trim() : null; } catch { return null; } + } + + private static bool TryFetchServerCert(string url, out X509Certificate2? cert) + { + cert = null; + try + { + var uri = new Uri(url); + using var tcp = new System.Net.Sockets.TcpClient(); + tcp.Connect(uri.Host, uri.Port <= 0 ? 443 : uri.Port); + X509Certificate2? captured = null; + using var ssl = new System.Net.Security.SslStream(tcp.GetStream(), false, + (_, c, _, _) => { if (c != null) captured = new X509Certificate2(c.Export(X509ContentType.Cert)); return true; }); + ssl.AuthenticateAsClient(uri.Host); + cert = captured; + return cert != null; + } + catch { return false; } } [System.Runtime.Versioning.SupportedOSPlatform("windows")] - private static void ImportCertWindows(string path) + private static void ImportCertWindows(X509Certificate2 cert) { try { - using var cert = new X509Certificate2(path); using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); store.Add(cert); @@ -188,16 +216,132 @@ internal static class Program catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); } } - private static string? FindProtonPrefix() + // Trust the cert inside the game's Proton/Wine prefix by writing the serialized cert blob + // into the prefix Root store via `wine reg import` (the wine `certutil` tool is unreliable + // under Proton). The prefix is chosen explicitly, so it works even though the client runs + // as a non-Steam shortcut (whose appid isn't the game's 868270). + private static void ImportCertProton(X509Certificate2 cert) + { + var wine = FindProtonWine(); + if (wine == null) { Console.WriteLine("⚠ Aucun wine Proton trouvé (Steam/Proton installé ?)."); return; } + var prefix = SelectPrefix(); + if (prefix == null) return; + + // Wine serialized cert blob: SHA1-hash property (id 3) + encoded cert property (id 0x20). + var der = cert.RawData; + var sha1 = cert.GetCertHash(); + byte[] blob; + using (var ms = new MemoryStream()) + { + void Prop(uint id, byte[] data) + { + ms.Write(BitConverter.GetBytes(id)); + ms.Write(BitConverter.GetBytes(1u)); + ms.Write(BitConverter.GetBytes((uint)data.Length)); + ms.Write(data); + } + Prop(3, sha1); + Prop(0x20, der); + blob = ms.ToArray(); + } + var thumb = Convert.ToHexString(sha1); + var hex = string.Join(",", blob.Select(b => b.ToString("x2"))); + var regFile = Path.Combine(Path.GetTempPath(), "prospect-cert.reg"); + File.WriteAllText(regFile, + "REGEDIT4\r\n\r\n" + + $"[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SystemCertificates\\Root\\Certificates\\{thumb}]\r\n" + + $"\"Blob\"=hex:{hex}\r\n"); + + Console.WriteLine("⚠ Ferme complètement le jeu avant de continuer (sinon le registre ne sera pas pris en compte)."); + Console.Write("Appuie sur Entrée pour importer… "); + Console.ReadLine(); + + var wineserver = Path.Combine(Path.GetDirectoryName(wine)!, "wineserver"); + var regWinePath = "Z:" + regFile.Replace('/', '\\'); // Z: maps to / inside the prefix + RunWine(wineserver, "-k", prefix); // stop any running wineserver (flush) + RunWine(wine, $"reg import \"{regWinePath}\"", prefix); + RunWine(wineserver, "-k", prefix); // flush the change to disk + + var sysreg = Path.Combine(prefix, "system.reg"); + var ok = File.Exists(sysreg) && File.ReadAllText(sysreg).Contains(thumb, StringComparison.OrdinalIgnoreCase); + Console.WriteLine(ok + ? $"✔ Certificat importé dans le préfixe : {prefix}\n Relance le jeu." + : "✖ Import non confirmé — assure-toi que le jeu est fermé et réessaie."); + } + + private static void RunWine(string exe, string args, string prefix) + { + try + { + var psi = new ProcessStartInfo(exe, args) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["WINEPREFIX"] = prefix; + psi.Environment["WINEDEBUG"] = "-all"; + using var p = Process.Start(psi); + p?.WaitForExit(20000); + } + catch (Exception ex) { Console.WriteLine(" (wine) " + ex.Message); } + } + + private static string? FindProtonWine() + { + foreach (var root in SteamRoots()) + { + var common = Path.Combine(root, "steamapps", "common"); + if (!Directory.Exists(common)) continue; + var candidates = Directory.GetDirectories(common, "Proton*") + .Select(d => Path.Combine(d, "files", "bin", "wine")) + .Where(File.Exists) + .OrderByDescending(w => w.Contains("Experimental")) + .ThenByDescending(File.GetLastWriteTimeUtc) + .ToList(); + if (candidates.Count > 0) return candidates[0]; + } + return null; + } + + private static string? SelectPrefix() + { + if (!string.IsNullOrEmpty(_settings.WinePrefix) && Directory.Exists(_settings.WinePrefix)) + return _settings.WinePrefix; + + var prefixes = new List(); + foreach (var root in SteamRoots()) + { + var cd = Path.Combine(root, "steamapps", "compatdata"); + if (!Directory.Exists(cd)) continue; + foreach (var d in Directory.GetDirectories(cd)) + { + var pfx = Path.Combine(d, "pfx"); + if (File.Exists(Path.Combine(pfx, "system.reg"))) prefixes.Add(Path.GetFullPath(pfx)); + } + } + prefixes = prefixes.Distinct() + .OrderByDescending(p => File.GetLastWriteTimeUtc(Path.Combine(p, "system.reg"))) + .ToList(); + if (prefixes.Count == 0) { Console.WriteLine("⚠ Aucun préfixe Proton trouvé."); return null; } + + Console.WriteLine("Préfixes Proton détectés (le plus récemment utilisé en premier = sûrement ton jeu) :"); + for (var i = 0; i < prefixes.Count; i++) + Console.WriteLine($" {i + 1}) {prefixes[i]}"); + Console.Write("Numéro du préfixe (Entrée = 1) : "); + var raw = Console.ReadLine()?.Trim(); + var idx = string.IsNullOrEmpty(raw) ? 0 : (int.TryParse(raw, out var n) ? n - 1 : -1); + if (idx < 0 || idx >= prefixes.Count) { Console.WriteLine("⚠ Choix invalide."); return null; } + _settings.WinePrefix = prefixes[idx]; + SaveSettings(); + return _settings.WinePrefix; + } + + private static IEnumerable SteamRoots() { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - foreach (var dir in new[] - { - Path.Combine(home, ".steam", "steam", "steamapps", "compatdata", AppId, "pfx"), - Path.Combine(home, ".local", "share", "Steam", "steamapps", "compatdata", AppId, "pfx"), - }) - if (Directory.Exists(dir)) return dir; - return null; + yield return Path.Combine(home, ".steam", "steam"); + yield return Path.Combine(home, ".local", "share", "Steam"); } // ---- Launch ------------------------------------------------------------ @@ -255,5 +399,6 @@ internal static class Program private sealed class Settings { public string GameFolder { get; set; } = ""; + public string WinePrefix { get; set; } = ""; } } From c70f674431ac942c6c1a65de77ee0aad94f423fc Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 16:50:30 +0200 Subject: [PATCH 08/12] docs(tool): document automatic Proton prefix cert import Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Client.Config/README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Prospect.Client.Config/README.md b/src/Prospect.Client.Config/README.md index 246f04a..c69a4c2 100644 --- a/src/Prospect.Client.Config/README.md +++ b/src/Prospect.Client.Config/README.md @@ -53,13 +53,22 @@ ProspectServerSwitcher --set https://mon-serveur:8443 # URL libre ## Certificat +L'outil récupère le certificat **en direct depuis le serveur** ciblé (il lit `backend.txt` +et se connecte en TLS), puis le rend fiable : + - **Windows** : import automatique dans *Autorités de certification racines de confiance* - (utilisateur courant). -- **Linux/Proton** : le certificat doit être approuvé **dans le préfixe Wine** du jeu. - L'outil détecte le préfixe Proton de l'app `868270` et affiche la commande prête : - ``` - WINEPREFIX="…/compatdata/868270/pfx" wine certutil -addstore -f Root "certificate.crt" - ``` + (utilisateur courant), via le magasin `X509Store`. +- **Linux/Proton** : import **automatique dans le préfixe Wine** du jeu. Comme `wine certutil` + est cassé sous Proton, l'outil écrit directement le *blob* sérialisé du certificat + (propriété SHA1 `id=3` + certificat encodé `id=0x20`) dans le magasin `Root` du préfixe via + `wine reg import`. Il : + 1. détecte le binaire `wine` de Proton (`…/common/Proton*/files/bin/wine`), + 2. liste les préfixes `compatdata/*/pfx` (le plus récemment utilisé en premier), + 3. importe dans le préfixe choisi (⚠️ **le jeu doit être fermé**), mémorisé ensuite. + + > ⚠️ Le client tourne souvent comme **raccourci non-Steam** → son préfixe n'est **pas** + > `compatdata/868270` mais un appid généré (ex. `3883998305`). D'où la sélection explicite + > du préfixe plutôt qu'une déduction sur l'appid du jeu. ## Lancement From 8057fcae479da92323d7fe598f3273f4d291c56c Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 17:31:44 +0200 Subject: [PATCH 09/12] fix(social): disable friends limit (was capping at 0 friends) FeatureToggles had m_isFriendsLimitEnabled=true with no positive limit, so the client showed 'friend limit reached: 0 friends' and blocked adding friends / squad invites. Disabled the toggle (squads/social already on). Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs b/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs index b34199c..c9c270b 100644 --- a/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs +++ b/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs @@ -19,7 +19,7 @@ public static class TitleDataDefault ["FactionProgressionICA"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":82000},{\"Level\":19,\"Reputation\":113000},{\"Level\":20,\"Reputation\":159000}]", ["FactionProgressionKorolev"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":82000},{\"Level\":19,\"Reputation\":113000},{\"Level\":20,\"Reputation\":159000}]", ["FactionProgressionOsiris"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":82000},{\"Level\":19,\"Reputation\":113000},{\"Level\":20,\"Reputation\":159000}]", - ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":false,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":true,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_cheatFeatureToggle_11\":true,\"m_cheatFeatureToggle_15\":true,\"m_cheatFeatureToggle_35\":true,\"m_cheatFeatureToggle_48\":true,\"m_cheatFeatureToggle_53\":true,\"m_cheatFeatureToggle_54\":true,\"m_cheatSettings_38\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_cheatSettings_39\":10.0,\"m_cheatSettings_40\":6,\"m_cheatSettings_41\":30.0,\"m_cheatSettingBackend_04\":{\"m_miscSetting_01\":1000,\"m_miscSetting_02\":14,\"m_miscSetting_03\":2.0,\"m_miscSetting_04\":14,\"m_miscSetting_05\":60,\"m_miscSetting_06\":20,\"m_miscSetting_07\":true,\"m_miscSetting_08\":true},\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_submixByPassEnabled\":false,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_battlEyeEndPointEnabled\":false,\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", + ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":false,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":false,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_cheatFeatureToggle_11\":true,\"m_cheatFeatureToggle_15\":true,\"m_cheatFeatureToggle_35\":true,\"m_cheatFeatureToggle_48\":true,\"m_cheatFeatureToggle_53\":true,\"m_cheatFeatureToggle_54\":true,\"m_cheatSettings_38\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_cheatSettings_39\":10.0,\"m_cheatSettings_40\":6,\"m_cheatSettings_41\":30.0,\"m_cheatSettingBackend_04\":{\"m_miscSetting_01\":1000,\"m_miscSetting_02\":14,\"m_miscSetting_03\":2.0,\"m_miscSetting_04\":14,\"m_miscSetting_05\":60,\"m_miscSetting_06\":20,\"m_miscSetting_07\":true,\"m_miscSetting_08\":true},\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_submixByPassEnabled\":false,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_battlEyeEndPointEnabled\":false,\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", ["DefaultInventoryInfo"] = "{\"inventoryStashLimit\":90,\"inventoryBagLimit\":300,\"inventorySafeLimit\":5}", ["InventoryInsurance"] = "{\"Default\":{\"InsuranceId\":\"Default\",\"Cost\":0.2,\"Payout\":0.5}}", ["MatchmakingSetup"] = "[]", @@ -46,7 +46,7 @@ public static class TitleDataDefault ["FactionProgressionICA"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":92000},{\"Level\":19,\"Reputation\":133000},{\"Level\":20,\"Reputation\":189000}]", ["FactionProgressionKorolev"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":92000},{\"Level\":19,\"Reputation\":133000},{\"Level\":20,\"Reputation\":189000}]", ["FactionProgressionOsiris"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":92000},{\"Level\":19,\"Reputation\":133000},{\"Level\":20,\"Reputation\":189000}]", - ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":false,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":true,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_fallingThroughWorldZ\":-50000.0,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_freeLoadoutsEnabled\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_isUserJoiningUnassignedSessionPreventionEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_cheatFeatureToggle_01\":true,\"m_cheatFeatureToggle_02\":true,\"m_cheatFeatureToggle_03\":700.0,\"m_cheatFeatureToggle_04\":3000.0,\"m_cheatFeatureToggle_05\":1.0,\"m_cheatFeatureToggle_06\":500.0,\"m_cheatFeatureToggle_07\":1500.0,\"m_cheatFeatureToggle_08\":700.0,\"m_cheatFeatureToggle_09\":60.0,\"m_cheatFeatureToggle_10\":20.0,\"m_isBadPingInsideSquadKickingEnabled\":true,\"m_badPingThresholds\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_badPingMinTimeInSeconds\":10.0,\"m_badPingMaxOccurrences\":6,\"m_badPingResetTime\":30.0,\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_battlEyeEndPointEnabled\":false,\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_victimCompensationSettings\":{\"m_killSnapshotCountLimit\":1000,\"m_killSnapshotDaysLimit\":14,\"m_processVictimCompensationsDelayInHours\":2.0,\"m_deathSnapshotDaysLimit\":14,\"m_deathSnapshotCountLimit\":60,\"m_itemsToCompensateCountLimit\":20,\"m_filterItemsInEndOfMatchSafePockets\":true,\"m_enableDoubleCheaterProcessing\":true},\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableUsingTotalCompletedContracts\":true,\"m_enableUsingTotalEvacs\":false,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_battlEyeReportPlayerEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_timeSlicedMeshMergingEnabled\":true,\"m_uiLazyScrollGridEnabled\":false,\"m_loadoutPresetEnabled\":true,\"m_howlerLootMapMarkerEnabled\":false,\"m_couponsEnabled\":true,\"m_enableFtueReconnectRestart\":true,\"m_enableClientAuthoritativeFtueFlow\":false,\"m_badumsQuestlineEnabled\":true,\"m_evacV2Enabled\":true,\"m_steamDLCSettings\":{\"m_showDiscountedPriceDLC1\":false,\"m_showDiscountedPriceDLC2\":false,\"m_showDiscountedPriceDLC3\":false,\"m_showDiscountedPriceDLC4\":false,\"m_showDiscountedPriceDLCPrimeTime\":false,\"m_showDiscountedPriceDLCHowlerTamer\":false,\"m_showDiscountedPriceDLCVividPartyStyle\":false,\"m_showDiscountedPriceDLCAuthorityPeacekeeper\":false},\"m_clearCombatTargetOnAggroLossForAllAI\":true,\"m_updateWeaponEquippedDataEveryFrame\":false,\"m_aiProximitySenseBehaviourEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", + ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":false,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":false,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_fallingThroughWorldZ\":-50000.0,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_freeLoadoutsEnabled\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_isUserJoiningUnassignedSessionPreventionEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_cheatFeatureToggle_01\":true,\"m_cheatFeatureToggle_02\":true,\"m_cheatFeatureToggle_03\":700.0,\"m_cheatFeatureToggle_04\":3000.0,\"m_cheatFeatureToggle_05\":1.0,\"m_cheatFeatureToggle_06\":500.0,\"m_cheatFeatureToggle_07\":1500.0,\"m_cheatFeatureToggle_08\":700.0,\"m_cheatFeatureToggle_09\":60.0,\"m_cheatFeatureToggle_10\":20.0,\"m_isBadPingInsideSquadKickingEnabled\":true,\"m_badPingThresholds\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_badPingMinTimeInSeconds\":10.0,\"m_badPingMaxOccurrences\":6,\"m_badPingResetTime\":30.0,\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_battlEyeEndPointEnabled\":false,\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_victimCompensationSettings\":{\"m_killSnapshotCountLimit\":1000,\"m_killSnapshotDaysLimit\":14,\"m_processVictimCompensationsDelayInHours\":2.0,\"m_deathSnapshotDaysLimit\":14,\"m_deathSnapshotCountLimit\":60,\"m_itemsToCompensateCountLimit\":20,\"m_filterItemsInEndOfMatchSafePockets\":true,\"m_enableDoubleCheaterProcessing\":true},\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableUsingTotalCompletedContracts\":true,\"m_enableUsingTotalEvacs\":false,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_battlEyeReportPlayerEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_timeSlicedMeshMergingEnabled\":true,\"m_uiLazyScrollGridEnabled\":false,\"m_loadoutPresetEnabled\":true,\"m_howlerLootMapMarkerEnabled\":false,\"m_couponsEnabled\":true,\"m_enableFtueReconnectRestart\":true,\"m_enableClientAuthoritativeFtueFlow\":false,\"m_badumsQuestlineEnabled\":true,\"m_evacV2Enabled\":true,\"m_steamDLCSettings\":{\"m_showDiscountedPriceDLC1\":false,\"m_showDiscountedPriceDLC2\":false,\"m_showDiscountedPriceDLC3\":false,\"m_showDiscountedPriceDLC4\":false,\"m_showDiscountedPriceDLCPrimeTime\":false,\"m_showDiscountedPriceDLCHowlerTamer\":false,\"m_showDiscountedPriceDLCVividPartyStyle\":false,\"m_showDiscountedPriceDLCAuthorityPeacekeeper\":false},\"m_clearCombatTargetOnAggroLossForAllAI\":true,\"m_updateWeaponEquippedDataEveryFrame\":false,\"m_aiProximitySenseBehaviourEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", ["DefaultInventoryInfo"] = "{\"inventoryStashLimit\":90,\"inventoryBagLimit\":300,\"inventorySafeLimit\":5}", ["InventoryInsurance"] = "{\"Default\":{\"InsuranceId\":\"Default\",\"Cost\":0.2,\"Payout\":0.5}}", ["MatchmakingSetup"] = "[]", From 55a9571958133ad7603dda33c23cd5882447c23c Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 17:56:22 +0200 Subject: [PATCH 10/12] fix(matchmaking): deploy signal targets only the deploying player EnterMatchmakingMatch broadcast OnSquadMatchmakingSuccess to Clients.All, so one player deploying pulled every connected player into the match. Map each player to their SignalR connection (GetSignalRConnection tags the URL with uid; CycleHub records it via a SignalRConnectionRegistry) and send the deploy signal only to that connection (broadcast fallback if unmapped). Also enable m_automaticImportPlatformFriends (Steam friends). Co-Authored-By: Claude Opus 4.8 --- src/Prospect.Server.Api/Hubs/CycleHub.cs | 25 ++++++++++++++-- .../Hubs/SignalRConnectionRegistry.cs | 29 +++++++++++++++++++ .../Functions/EnterMatchmakingMatch.cs | 19 +++++++++--- .../Functions/GetSignalRConnection.cs | 9 +++++- .../Services/UserData/TitleDataDefault.cs | 4 +-- src/Prospect.Server.Api/Startup.cs | 1 + 6 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 src/Prospect.Server.Api/Hubs/SignalRConnectionRegistry.cs diff --git a/src/Prospect.Server.Api/Hubs/CycleHub.cs b/src/Prospect.Server.Api/Hubs/CycleHub.cs index ec5e2eb..0d91064 100644 --- a/src/Prospect.Server.Api/Hubs/CycleHub.cs +++ b/src/Prospect.Server.Api/Hubs/CycleHub.cs @@ -1,12 +1,31 @@ -using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR; namespace Prospect.Server.Api.Hubs; public class CycleHub : Hub { + private readonly SignalRConnectionRegistry _registry; + + public CycleHub(SignalRConnectionRegistry registry) + { + _registry = registry; + } + public override async Task OnConnectedAsync() { - Console.WriteLine("Connected {0}", Context.ConnectionId); + // The client connects with the SignalR URL returned by GetSignalRConnection, which + // carries a `uid` query param — use it to map this connection to the player so + // server-initiated messages can target a single player instead of everyone. + var uid = Context.GetHttpContext()?.Request.Query["uid"].ToString(); + if (!string.IsNullOrEmpty(uid)) + _registry.Add(uid, Context.ConnectionId); + Console.WriteLine("Connected {0} (uid={1})", Context.ConnectionId, uid); await base.OnConnectedAsync(); } -} \ No newline at end of file + + public override async Task OnDisconnectedAsync(Exception? exception) + { + _registry.Remove(Context.ConnectionId); + await base.OnDisconnectedAsync(exception); + } +} diff --git a/src/Prospect.Server.Api/Hubs/SignalRConnectionRegistry.cs b/src/Prospect.Server.Api/Hubs/SignalRConnectionRegistry.cs new file mode 100644 index 0000000..3660841 --- /dev/null +++ b/src/Prospect.Server.Api/Hubs/SignalRConnectionRegistry.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; + +namespace Prospect.Server.Api.Hubs; + +// Maps players to their live SignalR connection so server-initiated messages (e.g. the +// matchmaking-success "travel to match" signal) can target a single player instead of being +// broadcast to everyone. The client's SignalR URL carries a `uid` query param (added by +// GetSignalRConnection); CycleHub records it on connect. +public class SignalRConnectionRegistry +{ + private readonly ConcurrentDictionary _userToConn = new(); + private readonly ConcurrentDictionary _connToUser = new(); + + public void Add(string userId, string connectionId) + { + if (string.IsNullOrEmpty(userId)) return; + _userToConn[userId] = connectionId; + _connToUser[connectionId] = userId; + } + + public void Remove(string connectionId) + { + if (_connToUser.TryRemove(connectionId, out var userId)) + _userToConn.TryRemove(userId, out _); + } + + public string? GetConnection(string userId) + => _userToConn.TryGetValue(userId, out var c) ? c : null; +} diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs index 6625fd3..49f1016 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs @@ -24,13 +24,15 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction _hubContext; private readonly UserDataService _userDataService; private readonly TitleDataService _titleDataService; + private readonly SignalRConnectionRegistry _registry; - public EnterMatchmakingMatchFunction(IHubContext hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService) + public EnterMatchmakingMatchFunction(IHubContext hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, SignalRConnectionRegistry registry) { _httpContextAccessor = httpContextAccessor; _hubContext = hubContext; _userDataService = userDataService; _titleDataService = titleDataService; + _registry = registry; } public async Task ExecuteAsync(FYEnterMatchAzureFunction request) @@ -94,11 +96,20 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction {Url}", remote, lan, url); + // Tag the connection with the player id so the hub can map connection -> user and + // target server messages (deploy signal) instead of broadcasting to everyone. + var userId = _httpContextAccessor.HttpContext?.User.FindAuthUserId(); + if (!string.IsNullOrEmpty(userId)) + url += (url.Contains('?') ? "&" : "?") + "uid=" + Uri.EscapeDataString(userId); + + _logger.LogInformation("GetSignalRConnection: remoteIp={Remote} lan={Lan} uid={Uid} -> {Url}", remote, lan, userId, url); return Task.FromResult(new FYGetSignalRConnectionResult { diff --git a/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs b/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs index c9c270b..4501f80 100644 --- a/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs +++ b/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs @@ -19,7 +19,7 @@ public static class TitleDataDefault ["FactionProgressionICA"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":82000},{\"Level\":19,\"Reputation\":113000},{\"Level\":20,\"Reputation\":159000}]", ["FactionProgressionKorolev"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":82000},{\"Level\":19,\"Reputation\":113000},{\"Level\":20,\"Reputation\":159000}]", ["FactionProgressionOsiris"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":82000},{\"Level\":19,\"Reputation\":113000},{\"Level\":20,\"Reputation\":159000}]", - ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":false,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":false,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_cheatFeatureToggle_11\":true,\"m_cheatFeatureToggle_15\":true,\"m_cheatFeatureToggle_35\":true,\"m_cheatFeatureToggle_48\":true,\"m_cheatFeatureToggle_53\":true,\"m_cheatFeatureToggle_54\":true,\"m_cheatSettings_38\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_cheatSettings_39\":10.0,\"m_cheatSettings_40\":6,\"m_cheatSettings_41\":30.0,\"m_cheatSettingBackend_04\":{\"m_miscSetting_01\":1000,\"m_miscSetting_02\":14,\"m_miscSetting_03\":2.0,\"m_miscSetting_04\":14,\"m_miscSetting_05\":60,\"m_miscSetting_06\":20,\"m_miscSetting_07\":true,\"m_miscSetting_08\":true},\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_submixByPassEnabled\":false,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_battlEyeEndPointEnabled\":false,\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", + ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":true,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":false,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_cheatFeatureToggle_11\":true,\"m_cheatFeatureToggle_15\":true,\"m_cheatFeatureToggle_35\":true,\"m_cheatFeatureToggle_48\":true,\"m_cheatFeatureToggle_53\":true,\"m_cheatFeatureToggle_54\":true,\"m_cheatSettings_38\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_cheatSettings_39\":10.0,\"m_cheatSettings_40\":6,\"m_cheatSettings_41\":30.0,\"m_cheatSettingBackend_04\":{\"m_miscSetting_01\":1000,\"m_miscSetting_02\":14,\"m_miscSetting_03\":2.0,\"m_miscSetting_04\":14,\"m_miscSetting_05\":60,\"m_miscSetting_06\":20,\"m_miscSetting_07\":true,\"m_miscSetting_08\":true},\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_submixByPassEnabled\":false,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_battlEyeEndPointEnabled\":false,\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", ["DefaultInventoryInfo"] = "{\"inventoryStashLimit\":90,\"inventoryBagLimit\":300,\"inventorySafeLimit\":5}", ["InventoryInsurance"] = "{\"Default\":{\"InsuranceId\":\"Default\",\"Cost\":0.2,\"Payout\":0.5}}", ["MatchmakingSetup"] = "[]", @@ -46,7 +46,7 @@ public static class TitleDataDefault ["FactionProgressionICA"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":92000},{\"Level\":19,\"Reputation\":133000},{\"Level\":20,\"Reputation\":189000}]", ["FactionProgressionKorolev"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":92000},{\"Level\":19,\"Reputation\":133000},{\"Level\":20,\"Reputation\":189000}]", ["FactionProgressionOsiris"] = "[{\"Level\":1,\"Reputation\":0},{\"Level\":2,\"Reputation\":100},{\"Level\":3,\"Reputation\":250},{\"Level\":4,\"Reputation\":450},{\"Level\":5,\"Reputation\":750},{\"Level\":6,\"Reputation\":1200},{\"Level\":7,\"Reputation\":1800},{\"Level\":8,\"Reputation\":2700},{\"Level\":9,\"Reputation\":3900},{\"Level\":10,\"Reputation\":5700},{\"Level\":11,\"Reputation\":8200},{\"Level\":12,\"Reputation\":12000},{\"Level\":13,\"Reputation\":16000},{\"Level\":14,\"Reputation\":23000},{\"Level\":15,\"Reputation\":32000},{\"Level\":16,\"Reputation\":43000},{\"Level\":17,\"Reputation\":59000},{\"Level\":18,\"Reputation\":92000},{\"Level\":19,\"Reputation\":133000},{\"Level\":20,\"Reputation\":189000}]", - ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":false,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":false,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_fallingThroughWorldZ\":-50000.0,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_freeLoadoutsEnabled\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_isUserJoiningUnassignedSessionPreventionEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_cheatFeatureToggle_01\":true,\"m_cheatFeatureToggle_02\":true,\"m_cheatFeatureToggle_03\":700.0,\"m_cheatFeatureToggle_04\":3000.0,\"m_cheatFeatureToggle_05\":1.0,\"m_cheatFeatureToggle_06\":500.0,\"m_cheatFeatureToggle_07\":1500.0,\"m_cheatFeatureToggle_08\":700.0,\"m_cheatFeatureToggle_09\":60.0,\"m_cheatFeatureToggle_10\":20.0,\"m_isBadPingInsideSquadKickingEnabled\":true,\"m_badPingThresholds\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_badPingMinTimeInSeconds\":10.0,\"m_badPingMaxOccurrences\":6,\"m_badPingResetTime\":30.0,\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_battlEyeEndPointEnabled\":false,\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_victimCompensationSettings\":{\"m_killSnapshotCountLimit\":1000,\"m_killSnapshotDaysLimit\":14,\"m_processVictimCompensationsDelayInHours\":2.0,\"m_deathSnapshotDaysLimit\":14,\"m_deathSnapshotCountLimit\":60,\"m_itemsToCompensateCountLimit\":20,\"m_filterItemsInEndOfMatchSafePockets\":true,\"m_enableDoubleCheaterProcessing\":true},\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableUsingTotalCompletedContracts\":true,\"m_enableUsingTotalEvacs\":false,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_battlEyeReportPlayerEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_timeSlicedMeshMergingEnabled\":true,\"m_uiLazyScrollGridEnabled\":false,\"m_loadoutPresetEnabled\":true,\"m_howlerLootMapMarkerEnabled\":false,\"m_couponsEnabled\":true,\"m_enableFtueReconnectRestart\":true,\"m_enableClientAuthoritativeFtueFlow\":false,\"m_badumsQuestlineEnabled\":true,\"m_evacV2Enabled\":true,\"m_steamDLCSettings\":{\"m_showDiscountedPriceDLC1\":false,\"m_showDiscountedPriceDLC2\":false,\"m_showDiscountedPriceDLC3\":false,\"m_showDiscountedPriceDLC4\":false,\"m_showDiscountedPriceDLCPrimeTime\":false,\"m_showDiscountedPriceDLCHowlerTamer\":false,\"m_showDiscountedPriceDLCVividPartyStyle\":false,\"m_showDiscountedPriceDLCAuthorityPeacekeeper\":false},\"m_clearCombatTargetOnAggroLossForAllAI\":true,\"m_updateWeaponEquippedDataEveryFrame\":false,\"m_aiProximitySenseBehaviourEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", + ["FeatureToggles"] = "[{\"m_isMapUnlockEnabled\":true,\"m_useFullTimeAsyncLoadingThread\":true,\"m_playerReplicatingClientSpawnDependency\":true,\"m_voiceChatEnabled\":true,\"m_autoRequeueingEnabled\":false,\"m_useIdleDetection\":false,\"m_enableColorBlindOptions\":true,\"m_automaticImportPlatformFriends\":true,\"m_eosStatsEnabled\":false,\"m_canCraftWeapons\":true,\"m_isModFunctionalityEnabled\":true,\"m_enableBugReporterInShipping\":false,\"m_enableBugReporter\":true,\"m_isProneEnabled\":false,\"m_isLeaningEnabled\":true,\"m_isAccountLinkingEnabled\":true,\"m_isFriendsImportEnabled\":true,\"m_isFriendsLimitEnabled\":false,\"m_showMatchMapsSelector\":true,\"m_newMeleeSystemEnabled\":true,\"m_enableEffortsComponent\":true,\"m_squadsEnabled\":true,\"m_socialEnabled\":true,\"m_sessionServerShutdownEnabled\":true,\"m_multiplayerStationEnabled\":true,\"m_replicationGraphEnabled\":true,\"m_lootCollisionTraceEnabled\":true,\"m_isOnboardingEnabled\":true,\"m_isPremiumShopEnabled\":true,\"m_areRealMoneyPurchasesEnabled\":false,\"m_isFortunaPassEnabled\":true,\"m_isTutorialEnabled\":true,\"m_isCharacterSelectionEnabled\":true,\"m_showSeasonDateInfoOnRetentionPopup\":false,\"m_enableBIEventsServer\":true,\"m_BIEventsServerRatio\":0.05,\"m_BIEventsUserSamplingRatio\":0.05,\"m_enableBIEventsClient\":true,\"m_playfabCouponsEnabled\":true,\"m_AlienForgeEnabled\":true,\"m_enableInteractionAmountLimit\":true,\"m_safeTeleportWhenStuck\":true,\"m_safeTeleportFallingThroughWorld\":true,\"m_fallingThroughWorldZ\":-50000.0,\"m_basicInsuranceEnabled\":true,\"m_premiumInsuranceEnabled\":true,\"m_relevantPlayerControllerCachingEnabled\":false,\"m_interruptionManagerTravelEnabled\":true,\"m_enableBulletPooling\":false,\"m_isTOCDataMigrationEnabled\":true,\"m_isInventoryLimitEnabled\":true,\"m_checkClientVersionUpToDate\":true,\"m_allowDebugShippingServerCrashes\":false,\"m_disableTargetingInAir\":false,\"m_spawnScoresAffectMatchmaker\":true,\"m_freeLoadoutsEnabled\":true,\"m_frontendCheatWeight\":1.5,\"m_frontendAutoBanEnabled\":true,\"m_backendCheatWeight\":1.5,\"m_backendAutoBanEnabled\":true,\"m_isUserJoiningUnassignedSessionPreventionEnabled\":true,\"m_kickingStorageQueueEnabled\":true,\"m_playFabIdServerValidation\":true,\"m_playFabNickNameServerValidation\":true,\"m_cheatFeatureToggle_01\":true,\"m_cheatFeatureToggle_02\":true,\"m_cheatFeatureToggle_03\":700.0,\"m_cheatFeatureToggle_04\":3000.0,\"m_cheatFeatureToggle_05\":1.0,\"m_cheatFeatureToggle_06\":500.0,\"m_cheatFeatureToggle_07\":1500.0,\"m_cheatFeatureToggle_08\":700.0,\"m_cheatFeatureToggle_09\":60.0,\"m_cheatFeatureToggle_10\":20.0,\"m_isBadPingInsideSquadKickingEnabled\":true,\"m_badPingThresholds\":{\"m_defaultValue\":300.0,\"m_overridenValues\":{\"NorthEurope\":300.0,\"JapanEast\":130.0}},\"m_badPingMinTimeInSeconds\":10.0,\"m_badPingMaxOccurrences\":6,\"m_badPingResetTime\":30.0,\"m_isBattlEyeKickingActive\":false,\"m_isBattlEyeActive\":false,\"m_isBattlEyeKickReasonChecksActive\":false,\"m_battlEyeEndPointEnabled\":false,\"m_cheatingVictimCompensationEnabled\":true,\"m_cheatingVictimCompensationIgnoreItemsByWeightEnabled\":true,\"m_victimCompensationSettings\":{\"m_killSnapshotCountLimit\":1000,\"m_killSnapshotDaysLimit\":14,\"m_processVictimCompensationsDelayInHours\":2.0,\"m_deathSnapshotDaysLimit\":14,\"m_deathSnapshotCountLimit\":60,\"m_itemsToCompensateCountLimit\":20,\"m_filterItemsInEndOfMatchSafePockets\":true,\"m_enableDoubleCheaterProcessing\":true},\"m_levelStreamingFailsafeEnabled\":false,\"m_useCustomOcclusionAudioAsyncLimitation\":true,\"m_useCustomOcclusionAudioSyncLimitation\":true,\"m_isUsingSingleNvrBracket\":false,\"m_isUsingSingleKdrBracket\":false,\"m_sampleBIEvents\":true,\"m_shutDownInstancesOfExistingConnections\":false,\"m_vivoxRejoinOnDisconnect\":false,\"m_cheatProtectionSession\":true,\"m_isMergingMeshComponentsDefaultStation\":true,\"m_isMergingMeshComponentsDefaultMatch\":true,\"m_reconnectFeatureEnabled\":true,\"m_autoFetchTitleData\":true,\"m_fetchingIntervals\":{\"FeatureToggles\":1800.0},\"m_enableShieldedMatchmaking\":true,\"m_enableUsingSpentMoneyThreshold\":true,\"m_enableUsingSteamAccountStatus\":true,\"m_enableUsingVeteranPoints\":true,\"m_enableUsingTotalCompletedContracts\":true,\"m_enableUsingTotalEvacs\":false,\"m_enableBackendOptimalPingCheck\":false,\"m_enableBackendRegionBoundaryCheck\":false,\"m_enableProjectileSimulatedDamagePropagation\":false,\"m_enableResponsivenessCheck\":true,\"m_deactivatedItems\":[],\"m_rolledPerkActivated\":true,\"m_VPNDetectionEnabled\":false,\"m_enableLedgeClimbServerValidation\":true,\"m_allowGameClientTelemetryCustomEvents\":true,\"m_allowGameServerTelemetryCustomEvents\":true,\"m_newReportEnabled\":true,\"m_playerSupportZendeskEndPointEnabled\":true,\"m_playerSupportBIEndPointEnabled\":true,\"m_battlEyeReportPlayerEndPointEnabled\":true,\"m_clientPerformanceTrackingEnabled\":true,\"m_serverPerformanceTrackingEnabled\":true,\"m_timeSlicedMeshMergingEnabled\":true,\"m_uiLazyScrollGridEnabled\":false,\"m_loadoutPresetEnabled\":true,\"m_howlerLootMapMarkerEnabled\":false,\"m_couponsEnabled\":true,\"m_enableFtueReconnectRestart\":true,\"m_enableClientAuthoritativeFtueFlow\":false,\"m_badumsQuestlineEnabled\":true,\"m_evacV2Enabled\":true,\"m_steamDLCSettings\":{\"m_showDiscountedPriceDLC1\":false,\"m_showDiscountedPriceDLC2\":false,\"m_showDiscountedPriceDLC3\":false,\"m_showDiscountedPriceDLC4\":false,\"m_showDiscountedPriceDLCPrimeTime\":false,\"m_showDiscountedPriceDLCHowlerTamer\":false,\"m_showDiscountedPriceDLCVividPartyStyle\":false,\"m_showDiscountedPriceDLCAuthorityPeacekeeper\":false},\"m_clearCombatTargetOnAggroLossForAllAI\":true,\"m_updateWeaponEquippedDataEveryFrame\":false,\"m_aiProximitySenseBehaviourEnabled\":true,\"m_rowName\":\"FeatureToggles\",\"Name\":\"FeatureToggles\"}]", ["DefaultInventoryInfo"] = "{\"inventoryStashLimit\":90,\"inventoryBagLimit\":300,\"inventorySafeLimit\":5}", ["InventoryInsurance"] = "{\"Default\":{\"InsuranceId\":\"Default\",\"Cost\":0.2,\"Payout\":0.5}}", ["MatchmakingSetup"] = "[]", diff --git a/src/Prospect.Server.Api/Startup.cs b/src/Prospect.Server.Api/Startup.cs index a294d5e..96e42a1 100644 --- a/src/Prospect.Server.Api/Startup.cs +++ b/src/Prospect.Server.Api/Startup.cs @@ -36,6 +36,7 @@ public class Startup services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); services.AddSingleton(); From 71a2c8a1e95246b1878c8ecd246c01be15283810 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 18:41:20 +0200 Subject: [PATCH 11/12] feat(deploy): optional dedicated-server travel via GAMESERVER_ADDRESS When GAMESERVER_ADDRESS ("host:port") is set, EnterMatchmaking returns that server (SingleplayerStation=false) and EnterMatchmakingMatch's deploy signal carries its address, so the client travels to the real game server instead of the client-hosted station. Unset = unchanged (normal preprod). Lets a 2nd preprod run the same :preprod image and differ only by this env var (dedicated-server test bench). Co-Authored-By: Claude Opus 4.8 --- .../CloudScript/Functions/EnterMatchmaking.cs | 18 ++++++++++++++++++ .../Functions/EnterMatchmakingMatch.cs | 5 ++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs index 95cd0ae..2924dec 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs @@ -25,6 +25,24 @@ public class EnterMatchmakingFunction : ICloudScriptFunction 1 && int.TryParse(parts[1], out var gsPort) ? gsPort : 7777, + MaintenanceMode = false, + }; + } + return new FYEnterMatchAzureFunctionResult { Success = true, diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs index 49f1016..5bf6316 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs @@ -100,9 +100,12 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction Date: Wed, 15 Jul 2026 01:54:25 +0200 Subject: [PATCH 12/12] fix(contracts): persist raid-zone objective progress (client-hosted raids) Symptom: missions done in raid zones weren't saved -> contracts stuck on 'Objective not met', no claim possible. Root cause: client-hosted raids have no dedicated game server, so UpdatePlayerActiveContracts is never called (confirmed: 0 calls in prod logs). Only Kills and OwnNumOfItem objectives were handled; DeadDrop/VisitArea/ LootContainer had Progress[i]=0 forever -> unclaimable and not shown at station. Fix: EYContractObjectiveType.IsRaidRuntime() groups the objectives that can only be observed in a live raid (Kills/DeadDrop/VisitArea/LootContainer). Auto-credit them on deploy (EnterMatchmakingMatch, persisted) so the station shows them complete, and accept them in ClaimActiveContract. OwnNumOfItem still validated against the real stash; FactionLevel/CompletedMission unchanged. Added a defensive guard against a Progress array shorter than the objective list. Co-Authored-By: Claude Opus 4.8 --- .../Models/Title/ContractInfo.cs | 15 +++++++++++++++ .../Functions/ClaimActiveContract.cs | 17 +++++++++-------- .../Functions/EnterMatchmakingMatch.cs | 15 ++++++++++----- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/Prospect.Server.Api/Models/Title/ContractInfo.cs b/src/Prospect.Server.Api/Models/Title/ContractInfo.cs index 8417b9e..6e47db2 100644 --- a/src/Prospect.Server.Api/Models/Title/ContractInfo.cs +++ b/src/Prospect.Server.Api/Models/Title/ContractInfo.cs @@ -72,6 +72,21 @@ public enum EYContractObjectiveType { MAX = 8 } +public static class ContractObjectiveTypeExtensions { + // Objectives that can only be observed while a raid is running (kills, dead-drops, + // visiting an area, looting a container). The raid is hosted by the player's own + // client and there is no dedicated game server, so their progress is never reported + // to the backend (UpdatePlayerActiveContracts is never called). They are therefore + // auto-credited server-side, otherwise these contracts can never be completed. + // OwnNumOfItem is excluded (validated against the real stash) and so are the meta + // objectives FactionLevel / CompletedMission (validated from persisted player data). + public static bool IsRaidRuntime(this EYContractObjectiveType type) => + type is EYContractObjectiveType.Kills + or EYContractObjectiveType.DeadDrop + or EYContractObjectiveType.VisitArea + or EYContractObjectiveType.LootContainer; +} + public enum EYContractDifficulty { Invalid = 0, diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/ClaimActiveContract.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/ClaimActiveContract.cs index 5796102..5aa031e 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/ClaimActiveContract.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/ClaimActiveContract.cs @@ -131,16 +131,17 @@ public class ClaimActiveContract : ICloudScriptFunction 0) { diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs index 5bf6316..1e2967b 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmakingMatch.cs @@ -62,12 +62,17 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction= contractActive.Progress.Length) { + // Defensive: player progress array shorter than the objective list. + break; + } var objective = contract.Objectives[i]; - // Kill objectives: the client-hosted raid has no dedicated game server - // to report per-kill progress to the backend, so kills would never be - // saved. Auto-credit them on deploy so the objective can be completed - // (mirrors the player-kill auto-credit done in ActivateContract). - if (objective.Type == EYContractObjectiveType.Kills) { + // Raid-runtime objectives (kills, dead-drops, visited areas, looted + // containers) are simulated inside the client-hosted raid; with no + // dedicated game server their progress is never reported to the backend, + // so they would never be saved. Auto-credit them on deploy so the + // objective persists and the station shows the contract as completable. + if (objective.Type.IsRaidRuntime()) { contractActive.Progress[i] = objective.MaxProgress; continue; }