diff --git a/src/Prospect.Client.Config/Program.cs b/src/Prospect.Client.Config/Program.cs new file mode 100644 index 0000000..e3935da --- /dev/null +++ b/src/Prospect.Client.Config/Program.cs @@ -0,0 +1,404 @@ +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 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", "PROD", "https://tc.nfteam.ovh:8443"), + ("preprod", "PREPROD", "https://rd-tc.nfteam.ovh:8444"), + }; + + 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() + { + // 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 + { + 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}"); + + 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(X509Certificate2 cert) + { + try + { + 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); } + } + + // 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); + yield return Path.Combine(home, ".steam", "steam"); + yield return Path.Combine(home, ".local", "share", "Steam"); + } + + // ---- 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; } = ""; + public string WinePrefix { 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.Client.Config/README.md b/src/Prospect.Client.Config/README.md new file mode 100644 index 0000000..c69a4c2 --- /dev/null +++ b/src/Prospect.Client.Config/README.md @@ -0,0 +1,102 @@ +# 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 + +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), 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 + +- **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). 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/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/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 6625fd3..1e2967b 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) @@ -60,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; } @@ -94,11 +101,23 @@ public class EnterMatchmakingMatchFunction : 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; + private readonly ILogger _logger; + + public GetSignalRConnection(IOptions settings, IHttpContextAccessor httpContextAccessor, ILogger logger) { _settings = settings.Value; + _httpContextAccessor = httpContextAccessor; + _logger = logger; } 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; + var lan = remote != null && IsLan(remote); + if (lan) + { + try + { + url = new UriBuilder(url) { Host = LanHost }.Uri.AbsoluteUri; + } + catch { /* keep the configured URL on any parsing issue */ } + } + + // 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 { - 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); + } +} 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/Services/UserData/TitleDataDefault.cs b/src/Prospect.Server.Api/Services/UserData/TitleDataDefault.cs index b34199c..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\":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\":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\":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\":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 7d7e3fc..96e42a1 100644 --- a/src/Prospect.Server.Api/Startup.cs +++ b/src/Prospect.Server.Api/Startup.cs @@ -35,6 +35,9 @@ public class Startup services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); services.AddSingleton(); 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