Compare commits
3 Commits
avalonia-g
...
e23f4e1e6e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e23f4e1e6e | ||
|
|
24a5951359 | ||
|
|
0d99caf2b0 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -4,3 +4,17 @@ obj/
|
||||
*.suo
|
||||
.vs/
|
||||
publish/
|
||||
|
||||
# Runtime client data accidentally left when launcher is run from its bin dir
|
||||
# (CWD becomes the source tree). These should never be committed.
|
||||
src/Metin2Launcher/.updates/
|
||||
src/Metin2Launcher/bgm/
|
||||
src/Metin2Launcher/config/
|
||||
src/Metin2Launcher/mark/
|
||||
src/Metin2Launcher/pack/
|
||||
src/Metin2Launcher/log/
|
||||
src/Metin2Launcher/Metin2.exe
|
||||
src/Metin2Launcher/*.dll
|
||||
src/Metin2Launcher/*.pyd
|
||||
src/Metin2Launcher/python314*
|
||||
src/Metin2Launcher/runtime-key.json
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Metin2Launcher.Logging;
|
||||
|
||||
namespace Metin2Launcher.GameLaunch;
|
||||
@@ -53,23 +54,69 @@ public static class GameProcess
|
||||
/// </summary>
|
||||
public static ProcessStartInfo BuildStartInfo(string exePath, string workingDirectory)
|
||||
{
|
||||
ProcessStartInfo psi;
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "wine",
|
||||
WorkingDirectory = workingDirectory,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
psi.ArgumentList.Add(exePath);
|
||||
return psi;
|
||||
// The default ~/.wine prefix lacks tahoma + d3dx9 + vcrun, which makes
|
||||
// the client run with invisible fonts and missing renderer DLLs. We
|
||||
// honor WINEPREFIX from the parent env if set; otherwise prefer a
|
||||
// metin-specific prefix that setup-wine-prefix.sh prepares.
|
||||
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WINEPREFIX")))
|
||||
{
|
||||
var home = Environment.GetEnvironmentVariable("HOME") ?? "/home/jann";
|
||||
var metinPrefix = Path.Combine(home, "metin", "wine-metin");
|
||||
if (Directory.Exists(metinPrefix))
|
||||
psi.Environment["WINEPREFIX"] = metinPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
return new ProcessStartInfo
|
||||
else
|
||||
{
|
||||
FileName = exePath,
|
||||
WorkingDirectory = workingDirectory,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
WorkingDirectory = workingDirectory,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
}
|
||||
ApplyM2PackRuntimeKey(psi, workingDirectory);
|
||||
return psi;
|
||||
}
|
||||
|
||||
// m2pack-secure: the new client refuses to load .m2p archives unless a
|
||||
// runtime master key is delivered out-of-band. The release ships
|
||||
// runtime-key.json next to the exe; we read it and pass the values via
|
||||
// env vars (M2PACK_MASTER_KEY_HEX et al) which the loader picks up.
|
||||
// If the file is missing we just don't set the vars — useful for legacy
|
||||
// releases that still have the static client.
|
||||
private static void ApplyM2PackRuntimeKey(ProcessStartInfo psi, string clientRoot)
|
||||
{
|
||||
var keyFile = Path.Combine(clientRoot, "runtime-key.json");
|
||||
if (!File.Exists(keyFile))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(keyFile));
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("master_key_hex", out var mk))
|
||||
psi.Environment["M2PACK_MASTER_KEY_HEX"] = mk.GetString() ?? "";
|
||||
if (root.TryGetProperty("sign_public_key_hex", out var pk))
|
||||
psi.Environment["M2PACK_SIGN_PUBKEY_HEX"] = pk.GetString() ?? "";
|
||||
if (root.TryGetProperty("key_id", out var kid))
|
||||
psi.Environment["M2PACK_KEY_ID"] = kid.GetInt32().ToString();
|
||||
Log.Info("m2pack runtime key loaded from runtime-key.json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("failed to load runtime-key.json", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,12 @@ public sealed class UpdateOrchestrator
|
||||
// 3. diff
|
||||
progress.Report(new UpdateProgress { State = LauncherState.Diffing, Manifest = loaded });
|
||||
|
||||
var platform = OperatingSystem.IsWindows() ? "windows" : "linux";
|
||||
var applicable = manifest.Files.Where(f => f.AppliesTo(platform)).ToList();
|
||||
// The client is always a Windows PE binary launched through Wine on
|
||||
// Linux hosts, so the manifest "platform" we apply is always "windows"
|
||||
// regardless of host OS. Using host OS would filter out Metin2.exe and
|
||||
// the python314/openssl/mingw runtime DLLs whenever the launcher runs
|
||||
// on Linux, leaving an unbootable install dir.
|
||||
var applicable = manifest.Files.Where(f => f.AppliesTo("windows")).ToList();
|
||||
|
||||
var needed = new List<(ManifestFile File, string FinalPath, string StagingPath)>();
|
||||
long totalBytes = 0;
|
||||
@@ -128,6 +132,7 @@ public sealed class UpdateOrchestrator
|
||||
Log.Info("client already up to date");
|
||||
var currentManifestPath = Path.Combine(_clientRoot, LauncherConfig.StateDirName, "current-manifest.json");
|
||||
try { File.WriteAllBytes(currentManifestPath, loaded.RawBytes); } catch { }
|
||||
PruneStaleFiles(manifest);
|
||||
progress.Report(new UpdateProgress { State = LauncherState.UpToDate, Percent = 100, Manifest = loaded });
|
||||
return new Result(true, LauncherState.UpToDate, loaded);
|
||||
}
|
||||
@@ -200,8 +205,41 @@ public sealed class UpdateOrchestrator
|
||||
var current = Path.Combine(_clientRoot, LauncherConfig.StateDirName, "current-manifest.json");
|
||||
try { File.WriteAllBytes(current, loaded.RawBytes); } catch { }
|
||||
|
||||
PruneStaleFiles(manifest);
|
||||
|
||||
Log.Info($"update complete: {toApply.Count} files applied");
|
||||
progress.Report(new UpdateProgress { State = LauncherState.UpToDate, Percent = 100, Manifest = loaded });
|
||||
return new Result(true, LauncherState.UpToDate, loaded);
|
||||
}
|
||||
|
||||
// Walk clientRoot and delete any file not in the manifest. Skips the
|
||||
// .updates state dir. Called after both the apply path and the
|
||||
// already-up-to-date short-circuit so switching to a leaner release
|
||||
// doesn't leave behind orphaned files (e.g. legacy .pck after .m2p
|
||||
// migration).
|
||||
private void PruneStaleFiles(Manifest.Manifest manifest)
|
||||
{
|
||||
var keepRel = new HashSet<string>(
|
||||
manifest.Files.Select(f => f.Path.Replace('/', Path.DirectorySeparatorChar)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
int pruned = 0;
|
||||
try
|
||||
{
|
||||
var rootFull = Path.GetFullPath(_clientRoot);
|
||||
var stateDirFull = Path.GetFullPath(Path.Combine(_clientRoot, LauncherConfig.StateDirName));
|
||||
foreach (var path in Directory.EnumerateFiles(rootFull, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (path.StartsWith(stateDirFull, StringComparison.Ordinal)) continue;
|
||||
var rel = Path.GetRelativePath(rootFull, path);
|
||||
if (keepRel.Contains(rel)) continue;
|
||||
try { File.Delete(path); pruned++; }
|
||||
catch (Exception ex) { Log.Warn($"failed to prune {rel}: {ex.Message}"); }
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn("prune walk failed: " + ex.Message);
|
||||
}
|
||||
if (pruned > 0) Log.Info($"pruned {pruned} stale files not in manifest");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user