Promotion: fix sauvegarde des missions en zone (raid client-hosted) #4
@@ -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 <chemin Win64> définit le dossier du jeu (mémorisé)");
|
||||||
|
Console.WriteLine(" --set <cible> é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<Settings>(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; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>ProspectServerSwitcher</AssemblyName>
|
||||||
|
<RootNamespace>Prospect.Client.Config</RootNamespace>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<!-- Cross-platform client tool (Linux/Proton + Windows). Never built by the Linux server CI. -->
|
||||||
|
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -25,6 +25,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "P
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Agent", "Prospect.Agent\Prospect.Agent.vcxproj", "{A9BA7D25-F239-4320-A0DC-85E8001FD669}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Agent", "Prospect.Agent\Prospect.Agent.vcxproj", "{A9BA7D25-F239-4320-A0DC-85E8001FD669}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Client.Config", "Prospect.Client.Config\Prospect.Client.Config.csproj", "{6DECEB07-996B-4EBC-927C-13E170161107}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
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|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.ActiveCfg = Release|x64
|
||||||
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user