diff --git a/.gitignore b/.gitignore index 7282dbf..06f9cf6 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ CodeCoverage/ *.VisualState.xml TestResult.xml nunit-*.xml + +temp \ No newline at end of file diff --git a/src/main/Program.cs b/src/main/Program.cs index 32c519d..4c0c235 100644 --- a/src/main/Program.cs +++ b/src/main/Program.cs @@ -1,5 +1,7 @@ using Lentia.Core.Auth.OAuth2; using Lentia.Core.Constants; +using Lentia.Core.Game; +using Lentia.Core.Game.Extra; using Lentia.Utils; using Photino.NET; using System.Text.Json; @@ -10,11 +12,12 @@ namespace Lentia; class Program { private static LentRules.AuthenticateResponse? _authenticatedPlayer; private static readonly string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static readonly string gameRoot = Path.Combine(appData, ".lentia2"); private static readonly LentRules.Authenticator _ygg = new(); [STAThread] static void Main(string[] args) { - SettingsManager.InitSettings(Path.Combine(appData, ".lentia2")); + SettingsManager.InitSettings(gameRoot); CreateLoginWindow().WaitForClose(); } @@ -206,6 +209,23 @@ class Program { payload = new { success = false, error = discordLoginResult.Error }; } break; + + case "launcher::game": + try { + MinecraftVersion version = await GameHelper.PrepareGame("1.12.2", gameRoot, true); + GameHelper.Launch(version, _authenticatedPlayer!, gameRoot, (logLine) => { + var logMessage = new { + requestId = "game::log", + payload = new { message = logLine.ToString() } + }; + window.SendWebMessage(JsonSerializer.Serialize(logMessage, LauncherConstants._jsonOptions)); + }); + payload = new { success = true }; + } catch (Exception) { + payload = new { success = false }; + throw; + } + break; } var response = new { requestId, payload }; diff --git a/src/main/core/game/Assets.cs b/src/main/core/game/Assets.cs new file mode 100644 index 0000000..e67d565 --- /dev/null +++ b/src/main/core/game/Assets.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Lentia.Core.Constants; +using Lentia.Core.Utils; + +namespace Lentia.Core.Game; +public class AssetsIndex { + [JsonPropertyName("objects")] + public Dictionary? Objects { get; set; } +} + +public class AssetObject { + [JsonPropertyName("hash")] + public string? Hash { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } +} + +public class AssetsHelper { + + public static string GetAssetLocalPath(string hash, string assetsDir) { + string subFolder = hash.Substring(0, 2); + return Path.Combine(assetsDir, subFolder, hash); + } + + public static async Task GetAssetsIndexAsync(string url) { + var response = await LauncherConstants.Http.GetAsync(url); + + if (response.IsSuccessStatusCode) { + string json = await response.Content.ReadAsStringAsync(); + var index = JsonSerializer.Deserialize(json, LauncherConstants._jsonOptions); + + return index ?? throw new Exception("L'index des assets est vide ou invalide."); + } + + throw new Exception($"Impossible de récupérer l'index des assets : {response.StatusCode}"); + } + + public static async Task DownloadAssets(AssetsIndex index, string rootDir) { + string objectsDir = Path.Combine(rootDir, "assets", "objects"); + + foreach (var entry in index.Objects!) { + string hash = entry.Value.Hash!; + long expectedSize = entry.Value.Size; + string localPath = GetAssetLocalPath(hash, objectsDir); + + bool needsDownload = !File.Exists(localPath); + + if (!needsDownload) { + var fileInfo = new FileInfo(localPath); + if (fileInfo.Length != expectedSize) { + needsDownload = true; + } + } + + if (needsDownload) { + string url = $"https://resources.download.minecraft.net/{hash.Substring(0, 2)}/{hash}"; + await FileHelper.DownloadFileAsync(url, localPath); + } + } + } + + public static async Task DownloadAssetsParallel(AssetsIndex index, string rootDir) { + string objectsDir = Path.Combine(rootDir, "assets", "objects"); + + var uniqueAssets = index.Objects! + .GroupBy(x => x.Value.Hash) + .Select(g => g.First().Value) + .ToList(); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 8 }; + + await Parallel.ForEachAsync(uniqueAssets, options, async (asset, token) => { + string hash = asset.Hash!; + long expectedSize = asset.Size; + string localPath = GetAssetLocalPath(hash, objectsDir); + + if (File.Exists(localPath)) { + if (new FileInfo(localPath).Length == expectedSize) return; + } + + string prefix = hash.Substring(0, 2); + string url = $"https://resources.download.minecraft.net/{prefix}/{hash}"; + + try { + await FileHelper.DownloadFileAsync(url, localPath); + } catch (Exception ex) { + Console.WriteLine($"Échec : {hash} - {ex.Message}"); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/core/game/Game.cs b/src/main/core/game/Game.cs new file mode 100644 index 0000000..fc012d2 --- /dev/null +++ b/src/main/core/game/Game.cs @@ -0,0 +1,124 @@ +using System.Diagnostics; +using Lentia.Core.Auth.Yggdrasil; +using Lentia.Core.Game.Extra; +using Lentia.Core.Utils; +using Lentia.Utils; + +namespace Lentia.Core.Game; + +public static class GameHelper { + + public static async Task PrepareGame(string targetVersion, string gameRoot, bool installForge) { + try { + string versionUrl = await VersionsHelper.GetVersionUrlAsync(targetVersion); + MinecraftVersion version = await VersionsHelper.GetVersionDetailsAsync(versionUrl); + MinecraftVersion workingVersion = version; + + if (installForge) { + MinecraftVersion? forgeMeta = await ForgeHelper.ProcessForge(version, gameRoot); + if (forgeMeta != null) { + workingVersion = version; + } + } + + AssetsIndex assetsIndex = await AssetsHelper.GetAssetsIndexAsync(version.AssetIndex!.Url!); + await AssetsHelper.DownloadAssetsParallel(assetsIndex, gameRoot); + await JavaHelper.GetJavaExecutablePath(version, gameRoot); + await VersionsHelper.DownloadClientJar(version, gameRoot); + await LibrariesHelper.DownloadLibraries(workingVersion.Libraries!, gameRoot); + LibrariesHelper.ExtractNatives(workingVersion, gameRoot); + await VersionsHelper.DownloadLoggingConfig(version, gameRoot); + + return workingVersion; + } catch (Exception) { + throw; + } + } + + public static string BuildClasspath(MinecraftVersion version, string gameRoot) { + var paths = new List(); + string libRoot = Path.Combine(gameRoot, "libraries"); + + paths.Add(Path.Combine(gameRoot, "versions", version.Id!, $"{version.Id}.jar")); + + foreach (var lib in version.Libraries!) { + if (LibrariesHelper.IsAllowed(lib.Rules, OsHelper.GetOSName())) { + string relPath = ForgeHelper.GeneratePathFromArtifactName(lib.Name!); + paths.Add(Path.Combine(libRoot, relPath)); + } + } + + string separator = Path.PathSeparator.ToString(); + return string.Join(separator, paths); + } + + public static List GenerateGameArguments(MinecraftVersion version, AuthenticateResponse auth, string gameRoot) { + string? rawArgs = version.MinecraftArguments; + + if (string.IsNullOrEmpty(rawArgs)) { + return new List(); + } + + rawArgs = rawArgs.Replace("${auth_player_name}", auth.User.Username); + rawArgs = rawArgs.Replace("${version_name}", version.Id ?? "Lentia"); + rawArgs = rawArgs.Replace("${game_directory}", gameRoot); + rawArgs = rawArgs.Replace("${assets_root}", Path.Combine(gameRoot, "assets")); + rawArgs = rawArgs.Replace("${assets_index_name}", version.AssetIndex?.Id ?? "legacy"); + rawArgs = rawArgs.Replace("${auth_uuid}", auth.SelectedProfile.Id); + rawArgs = rawArgs.Replace("${auth_access_token}", auth.AccessToken); + rawArgs = rawArgs.Replace("${user_type}", "mojang"); + + rawArgs = rawArgs.Replace("${user_properties}", "{}"); + + return rawArgs.Split(" ", StringSplitOptions.RemoveEmptyEntries).ToList(); + } + + public static void Launch(MinecraftVersion version, AuthenticateResponse auth, string gameRoot, Action logHandler) { + string javaPath = SettingsManager.ReadSettings().JavaPath; + + string nativesDir = Path.Combine(gameRoot, "versions", version.Id!, "natives"); + + var jvmArgs = new List { + $"-Xmx{SettingsManager.ReadSettings().Ram.Max}M", + $"-Djava.library.path={nativesDir}", + "-Dminecraft.launcher.brand=Lentia", + "-Dminecraft.launcher.version=1.0.0" + }; + + // if (version.Logging?.Client?.Argument != null) { + // string logConfig = Path.Combine(gameRoot, "assets", "log_configs", version.Logging.Client.File?.Id ?? "client-log4j2.xml"); + // jvmArgs.Add(version.Logging.Client.Argument.Replace("${path}", logConfig)); + // } + + string classpath = BuildClasspath(version, gameRoot); + jvmArgs.Add("-cp"); + jvmArgs.Add(classpath); + + jvmArgs.Add(version.MainClass ?? "net.minecraft.client.main.Main"); + + var gameArgs = GenerateGameArguments(version, auth, gameRoot); + jvmArgs.AddRange(gameArgs); + + var startInfo = new ProcessStartInfo(javaPath) { + WorkingDirectory = gameRoot, + Arguments = string.Join(" ", jvmArgs.Select(a => a.Contains(" ") ? $"\"{a}\"" : a)), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }; + + process.OutputDataReceived += (s, e) => { if (e.Data != null) logHandler(e.Data); }; + process.ErrorDataReceived += (s, e) => { if (e.Data != null) logHandler($"[ERROR] {e.Data}"); }; + process.Exited += (sender, e) => { + logHandler("GAME_CLOSED"); + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + } + +} \ No newline at end of file diff --git a/src/main/core/game/Java.cs b/src/main/core/game/Java.cs new file mode 100644 index 0000000..d80e2e1 --- /dev/null +++ b/src/main/core/game/Java.cs @@ -0,0 +1,96 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Net.Http.Json; +using System.Text.Json; +using Lentia.Core.Constants; +using Lentia.Core.Utils; +using Lentia.Utils; +using System.Text.Json.Serialization; + +namespace Lentia.Core.Game; + +public class JavaRuntimeManifest { + [JsonPropertyName("files")] + public Dictionary? Files { get; set; } +} + +public class JavaFileEntry { + [JsonPropertyName("type")] + public string? Type { get; set; } + [JsonPropertyName("downloads")] + public JavaDownloads? Downloads { get; set; } +} + +public class JavaDownloads { + [JsonPropertyName("raw")] + public DownloadInfo? Raw { get; set; } +} + +public static class JavaHelper { + + public static async Task GetJavaExecutablePath(MinecraftVersion version, string root) { + string component = version.JavaVersion?.Component ?? "jre-legacy"; + int majorVersion = version.JavaVersion?.MajorVersion ?? 8; + + string configuredJava = SettingsManager.ReadSettings().JavaPath; + + if (IsJavaCompatible(configuredJava, majorVersion)) { + return configuredJava; + } + + string platform = OsHelper.GetPlatformString(); + string localJavaPath = Path.Combine(root, "runtime", component, platform, "bin", + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "java.exe" : "java"); + + if (File.Exists(localJavaPath)) { + return localJavaPath; + } + + await DownloadJavaRuntime(root, platform, component); + + return localJavaPath; + } + + private static bool IsJavaCompatible(string javaPath, int requiredMajor) { + try { + var psi = new ProcessStartInfo(javaPath, "-version") { + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + using var process = Process.Start(psi); + string output = process?.StandardError.ReadToEnd() ?? ""; + + string search = requiredMajor <= 8 ? $"1.{requiredMajor}" : $"{requiredMajor}."; + return output.Contains(search); + } catch { + return false; + } + } + + private static async Task DownloadJavaRuntime(string root, string platform, string component) { + string allJsonUrl = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc6c0dba9f635e09f3900331038590632291/all.json"; + var allRuntimes = await LauncherConstants.Http.GetFromJsonAsync(allJsonUrl); + + if (!allRuntimes.TryGetProperty(platform, out var platformData) || + !platformData.TryGetProperty(component, out var componentVersions)) { + throw new Exception($"Le runtime {component} n'est pas disponible pour {platform}"); + } + + string manifestUrl = componentVersions[0].GetProperty("manifest").GetProperty("url").GetString()!; + var manifest = await LauncherConstants.Http.GetFromJsonAsync(manifestUrl); + + string javaDir = Path.Combine(root, "runtime", component, platform); + await Parallel.ForEachAsync(manifest!.Files!, new ParallelOptions { MaxDegreeOfParallelism = 8 }, async (entry, token) => { + string localPath = Path.Combine(javaDir, entry.Key); + if (entry.Value.Type == "directory") { + Directory.CreateDirectory(localPath); + } else if (entry.Value.Downloads?.Raw != null) { + var download = entry.Value.Downloads.Raw; + if (!File.Exists(localPath) || new FileInfo(localPath).Length != download.Size) { + await FileHelper.DownloadFileAsync(download.Url!, localPath); + } + } + }); + } +} \ No newline at end of file diff --git a/src/main/core/game/Libraries.cs b/src/main/core/game/Libraries.cs new file mode 100644 index 0000000..816a196 --- /dev/null +++ b/src/main/core/game/Libraries.cs @@ -0,0 +1,90 @@ +using System.IO.Compression; +using Lentia.Core.Utils; + +namespace Lentia.Core.Game; + +public static class LibrariesHelper { + + public static async Task DownloadLibraries(List libraries, string root) { + string osName = OsHelper.GetOSName(); + string librariesDir = Path.Combine(root, "libraries"); + + var downloadsToProcess = new List(); + + foreach (var library in libraries) { + if (!IsAllowed(library.Rules, osName)) continue; + + if (library.Downloads?.Artifact != null) { + downloadsToProcess.Add(library.Downloads.Artifact); + } + + if (library.Natives != null && library.Natives.TryGetValue(osName, out string? classifierKey)) { + if (library.Downloads?.Classifiers != null && library.Downloads.Classifiers.TryGetValue(classifierKey, out var nativeInfo)) { + downloadsToProcess.Add(nativeInfo); + } + } + } + + var uniqueDownloads = downloadsToProcess + .GroupBy(d => d.Path) + .Select(g => g.First()) + .ToList(); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 8 }; + await Parallel.ForEachAsync(uniqueDownloads, options, async (info, token) => { + await DownloadDownloadInfo(info, librariesDir); + }); +} + + private static async Task DownloadDownloadInfo(DownloadInfo info, string baseDir) { + if (string.IsNullOrEmpty(info.Url) || string.IsNullOrEmpty(info.Path)) return; + + string localPath = Path.Combine(baseDir, info.Path); + + if (File.Exists(localPath) && new FileInfo(localPath).Length == info.Size) return; + + await FileHelper.DownloadFileAsync(info.Url, localPath); + } + + public static void ExtractNatives(MinecraftVersion version, string root) { + string osName = OsHelper.GetOSName(); + string librariesDir = Path.Combine(root, "libraries"); + string nativesDir = Path.Combine(root, "versions", version.Id!, "natives"); + + if (Directory.Exists(nativesDir)) Directory.Delete(nativesDir, true); + Directory.CreateDirectory(nativesDir); + + foreach (var library in version.Libraries!) { + if (!IsAllowed(library.Rules, osName) || library.Natives == null) continue; + + if (library.Natives.TryGetValue(osName, out string? classifierKey)) { + if (library.Downloads?.Classifiers != null && library.Downloads.Classifiers.TryGetValue(classifierKey, out var nativeInfo)) { + + string jarPath = Path.Combine(librariesDir, nativeInfo.Path!); + if (!File.Exists(jarPath)) continue; + + using (ZipArchive archive = ZipFile.OpenRead(jarPath)) { + foreach (ZipArchiveEntry entry in archive.Entries) { + if (entry.FullName.EndsWith("/") || entry.FullName.StartsWith("META-INF")) continue; + string destinationPath = Path.Combine(nativesDir, entry.Name); + entry.ExtractToFile(destinationPath, true); + } + } + } + } + } + } + + public static bool IsAllowed(List? rules, string osName) { + if (rules == null || rules.Count == 0) return true; + + bool allowed = false; + foreach (var rule in rules) { + if (rule.Os == null || rule.Os.Name == osName) { + allowed = rule.Action == "allow"; + } + } + return allowed; + } + +} \ No newline at end of file diff --git a/src/main/core/game/Versions.cs b/src/main/core/game/Versions.cs new file mode 100644 index 0000000..dd49711 --- /dev/null +++ b/src/main/core/game/Versions.cs @@ -0,0 +1,255 @@ +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using Lentia.Core.Constants; +using Lentia.Core.Utils; + +namespace Lentia.Core.Game; + +public class Arguments { + [JsonPropertyName("game")] + public List? Game { get; set; } + + [JsonPropertyName("jvm")] + public List? Jvm { get; set; } +} + +public class ConditionalArgument { + [JsonPropertyName("rules")] + public List? Rules { get; set; } + + [JsonPropertyName("value")] + public JsonElement Value { get; set; } +} + +public class MinecraftVersion { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("arguments")] + public Arguments? Arguments { get; set; } + + [JsonPropertyName("assetIndex")] + public AssetIndexInfo? AssetIndex { get; set; } + + [JsonPropertyName("downloads")] + public Dictionary? Downloads { get; set; } + + [JsonPropertyName("libraries")] + public List? Libraries { get; set; } + + [JsonPropertyName("mainClass")] + public string? MainClass { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("logging")] + public LoggingInfo? Logging { get; set; } + + [JsonPropertyName("javaVersion")] + public JavaVersionInfo? JavaVersion { get; set; } + + [JsonPropertyName("minecraftArguments")] + public string? MinecraftArguments { get; set; } +} + +public class AssetIndexInfo { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("sha1")] + public string? Sha1 { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("totalSize")] + public long TotalSize { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } +} + +public class Library { + [JsonPropertyName("downloads")] + public LibraryDownloads? Downloads { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("rules")] + public List? Rules { get; set; } + + [JsonPropertyName("natives")] + public Dictionary? Natives { get; set; } +} + +public class LibraryDownloads { + [JsonPropertyName("artifact")] + public DownloadInfo? Artifact { get; set; } + + [JsonPropertyName("classifiers")] + public Dictionary? Classifiers { get; set; } +} + +public class DownloadInfo { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("sha1")] + public string? Sha1 { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("path")] + public string? Path { get; set; } +} + +public class Rule { + [JsonPropertyName("action")] + public string? Action { get; set; } + + [JsonPropertyName("os")] + public OsRestriction? Os { get; set; } +} + +public class OsRestriction { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +public class VersionManifestV2 +{ + [JsonPropertyName("latest")] + public LatestVersion? Latest { get; set; } + + [JsonPropertyName("versions")] + public List? Versions { get; set; } +} + +public class LatestVersion +{ + [JsonPropertyName("release")] + public string? Release { get; set; } + + [JsonPropertyName("snapshot")] + public string? Snapshot { get; set; } +} + +public class VersionEntry +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("time")] + public DateTime Time { get; set; } + + [JsonPropertyName("releaseTime")] + public DateTime ReleaseTime { get; set; } + + [JsonPropertyName("sha1")] + public string? Sha1 { get; set; } + + [JsonPropertyName("complianceLevel")] + public int ComplianceLevel { get; set; } +} + +public class LoggingInfo { + [JsonPropertyName("client")] + public LoggingClient? Client { get; set; } +} + +public class LoggingClient { + + [JsonPropertyName("argument")] + public string? Argument { get; set; } + + [JsonPropertyName("file")] + public DownloadInfo? File { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } +} + +public class JavaVersionInfo { + [JsonPropertyName("component")] + public string? Component { get; set; } + + [JsonPropertyName("majorVersion")] + public int MajorVersion { get; set; } +} + +public static class VersionsHelper { + + public static async Task GetVersionUrlAsync(string targetVersion) { + var response = await LauncherConstants.Http.GetAsync("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"); + + var manifest = await response.Content.ReadFromJsonAsync(); + var versionEntry = manifest?.Versions?.FirstOrDefault(v => v.Id == targetVersion); + + + return versionEntry?.Url ?? throw new Exception("Version introuvable dans le manifest Mojang."); + } + + public static async Task GetVersionDetailsAsync(string versionUrl) { + var response = await LauncherConstants.Http.GetAsync(versionUrl); + + if (response.IsSuccessStatusCode) { + string jsonResponse = await response.Content.ReadAsStringAsync(); + var version = JsonSerializer.Deserialize(jsonResponse, LauncherConstants._jsonOptions); + + return version ?? throw new Exception("Impossible de lire les détails de la version."); + } else { + throw new Exception($"Erreur lors de la récupération des détails : {response.StatusCode}"); + } + } + + public static async Task DownloadClientJar(MinecraftVersion version, string root) { + string jarVersionPath = Path.Combine(root, "versions", version.Id!, $"{version.Id}.jar"); + try { + bool needsDownload = !File.Exists(jarVersionPath); + + if (!needsDownload) { + var fileInfo = new FileInfo(jarVersionPath); + long expectedSize = version.Downloads!["client"].Size; + if (fileInfo.Length != expectedSize) { + needsDownload = true; + } + } + + if (needsDownload) { + await FileHelper.DownloadFileAsync(version.Downloads!["client"].Url!, jarVersionPath); + } + } catch (Exception) { + throw; + } + } + + public static async Task DownloadLoggingConfig(MinecraftVersion version, string root) { + if (version.Logging?.Client?.File == null) return; + + var fileInfo = version.Logging.Client.File; + string logConfigDir = Path.Combine(root, "assets", "log_configs"); + string destinationPath = Path.Combine(logConfigDir, fileInfo.Id ?? "client-log4j2.xml"); + + if (File.Exists(destinationPath) && new FileInfo(destinationPath).Length == fileInfo.Size) { + return; + } + + await FileHelper.DownloadFileAsync(fileInfo.Url!, destinationPath); + } +} \ No newline at end of file diff --git a/src/main/core/game/extra/Forge.cs b/src/main/core/game/extra/Forge.cs new file mode 100644 index 0000000..fee7b22 --- /dev/null +++ b/src/main/core/game/extra/Forge.cs @@ -0,0 +1,152 @@ +using System.Text.Json; +using System.IO.Compression; +using Lentia.Core.Constants; +using System.Text.Json.Serialization; +using Lentia.Core.Utils; +using System.Net.Http.Json; + +namespace Lentia.Core.Game.Extra; + +public static class ForgeHelper { + public class ForgePromotions { + [JsonPropertyName("promos")] + public Dictionary? Promos { get; set; } + } + + public static async Task FetchAndDownloadForge(string mcVersion, string root, bool recommended = true) { + string promoUrl = "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json"; + + LauncherConstants.Http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + + var promoData = await LauncherConstants.Http.GetFromJsonAsync(promoUrl); + string promoKey = $"{mcVersion}-{(recommended ? "recommended" : "latest")}"; + + if (promoData?.Promos == null || !promoData.Promos.TryGetValue(promoKey, out string? buildVersion)) { + throw new Exception($"Version Forge introuvable pour {promoKey}"); + } + + bool isModern = IsVersionModern(mcVersion); + string fullVersion = $"{mcVersion}-{buildVersion}"; + + string classifier = isModern ? "installer" : "universal"; + string downloadUrl = $"https://maven.minecraftforge.net/net/minecraftforge/forge/{fullVersion}/forge-{fullVersion}-{classifier}.jar"; + + string destination = Path.Combine(root, "temp", $"forge-{fullVersion}-{classifier}.jar"); + + await FileHelper.DownloadFileAsync(downloadUrl, destination); + + return destination; + } + + private static bool IsVersionModern(string mcVersion) { + if (string.IsNullOrEmpty(mcVersion)) return false; + var parts = mcVersion.Split("."); + if (parts.Length < 2) return false; + + if (int.TryParse(parts[1], out int minor)) { + return minor >= 12; + } + return false; + } + + public static void MergeForge(MinecraftVersion baseVersion, string forgeJsonContent) { + var forgeVersion = JsonSerializer.Deserialize(forgeJsonContent, LauncherConstants._jsonOptions); + if (forgeVersion == null) return; + + baseVersion.MainClass = forgeVersion.MainClass; + baseVersion.MinecraftArguments = forgeVersion.MinecraftArguments; + + if (forgeVersion.Libraries != null) { + foreach (var lib in forgeVersion.Libraries) { + if (lib.Downloads?.Artifact == null) { + TranslateToModernLibrary(lib); + } + + if (lib.Name!.Contains("net.minecraftforge:forge") && string.IsNullOrEmpty(lib.Downloads?.Artifact?.Url)) { + FixForgeUniversalUrl(lib); + } + + baseVersion.Libraries?.Add(lib); + } + } + } + + private static void TranslateToModernLibrary(Library lib) { + if (string.IsNullOrEmpty(lib.Name)) return; + + var parts = lib.Name.Split(":"); + if (parts.Length < 3) return; + + string group = parts[0].Replace(".", "/"); + string artifact = parts[1]; + string version = parts[2]; + string path = $"{group}/{artifact}/{version}/{artifact}-{version}.jar"; + + string baseUrl = "https://libraries.minecraft.net/"; + if (!baseUrl.EndsWith("/")) baseUrl += "/"; + + lib.Downloads = new LibraryDownloads { + Artifact = new DownloadInfo { + Path = path, + Url = baseUrl + path + } + }; + } + + private static void FixForgeUniversalUrl(Library lib) { + var parts = lib.Name!.Split(":"); + string version = parts[2]; + lib.Downloads!.Artifact!.Url = $"https://maven.minecraftforge.net/net/minecraftforge/forge/{version}/forge-{version}-universal.jar"; + } + + public static async Task ExtractForgeJson(string jarPath) { + using (ZipArchive archive = ZipFile.OpenRead(jarPath)) { + var entry = archive.Entries.FirstOrDefault(e => e.Name == "version.json"); + + if (entry == null) { + throw new Exception("Fichier version.json introuvable dans l'archive Forge."); + } + + using var reader = new StreamReader(entry.Open()); + string jsonContent = await reader.ReadToEndAsync(); + + return JsonSerializer.Deserialize(jsonContent, LauncherConstants._jsonOptions); + } + } + public static async Task ProcessForge(MinecraftVersion baseVersion, string root) { + if (baseVersion.Id == null) throw new ArgumentNullException(nameof(baseVersion.Id)); + + string downloadedJarPath = await FetchAndDownloadForge(baseVersion.Id, root, false); + + var forgeMeta = await ExtractForgeJson(downloadedJarPath); + if (forgeMeta == null) return null; + + var forgeLib = forgeMeta.Libraries?.FirstOrDefault(l => l.Name!.Contains("net.minecraftforge:forge")); + + if (forgeLib != null) { + string relPath = GeneratePathFromArtifactName(forgeLib.Name!); + string finalDestination = Path.Combine(root, "libraries", relPath); + + Directory.CreateDirectory(Path.GetDirectoryName(finalDestination)!); + + if (File.Exists(finalDestination)) File.Delete(finalDestination); + File.Move(downloadedJarPath, finalDestination); + } + + MergeForge(baseVersion, JsonSerializer.Serialize(forgeMeta, LauncherConstants._jsonOptions)); + + return forgeMeta; + } + public static string GeneratePathFromArtifactName(string name) { + if (string.IsNullOrEmpty(name) || !name.Contains(":")) return name; + + var parts = name.Split(":"); + + string group = parts[0].Replace(".", "/"); + string artifact = parts[1]; + string version = parts[2]; + + return $"{group}/{artifact}/{version}/{artifact}-{version}.jar"; + } + +} \ No newline at end of file diff --git a/src/main/core/utils/File.cs b/src/main/core/utils/File.cs new file mode 100644 index 0000000..1ac73dc --- /dev/null +++ b/src/main/core/utils/File.cs @@ -0,0 +1,24 @@ +using Lentia.Core.Constants; + +namespace Lentia.Core.Utils; + +public static class FileHelper { + public static async Task DownloadFileAsync(string url, string destinationPath) { + string? directory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(directory)) { + Directory.CreateDirectory(directory); + } + + if (File.Exists(destinationPath)) { + File.Delete(destinationPath); + } + + using var response = await LauncherConstants.Http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + using var streamToReadFrom = await response.Content.ReadAsStreamAsync(); + using var streamToWriteTo = File.Create(destinationPath); + + await streamToReadFrom.CopyToAsync(streamToWriteTo); + } +} \ No newline at end of file diff --git a/src/main/core/utils/Os.Cs b/src/main/core/utils/Os.Cs new file mode 100644 index 0000000..5f036c8 --- /dev/null +++ b/src/main/core/utils/Os.Cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace Lentia.Core.Utils; + +public static class OsHelper { + public static string GetPlatformString() { + string os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "mac-os" : "linux"; + string arch = RuntimeInformation.OSArchitecture == Architecture.X64 ? "x64" : "x86"; + return $"{os}-{arch}"; + } + + public static string GetOSName() { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "windows"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "osx"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux"; + return "unknown"; + } +} \ No newline at end of file diff --git a/src/main/utils/Settings.cs b/src/main/utils/Settings.cs index fcd6956..4a76bc1 100644 --- a/src/main/utils/Settings.cs +++ b/src/main/utils/Settings.cs @@ -92,10 +92,10 @@ public static class SettingsManager { next = Activator.CreateInstance(prop.PropertyType); prop.SetValue(current, next); } - current = next; + current = next!; } - var lastProp = current.GetType().GetProperty(properties[^1], + var lastProp = current!.GetType().GetProperty(properties[^1], System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase); if (lastProp == null || !lastProp.CanWrite) diff --git a/wwwroot/assets/css/logged.css b/wwwroot/assets/css/logged.css index bdc0ac5..1841468 100644 --- a/wwwroot/assets/css/logged.css +++ b/wwwroot/assets/css/logged.css @@ -372,4 +372,13 @@ div.profile > section.cosmectics > article.capes > div.capes > div.cape:hover { div.profile > section.cosmectics > article.capes > div.capes > div.cape.active, div.profile > section.cosmectics > article.capes > div.capes > div.cape.active:hover { filter: brightness(1); +} + + +div.loader > div.full > div.loading { + background-color: #E89032; +} + +div.loader > div.full { + background-color: transparent; } \ No newline at end of file diff --git a/wwwroot/assets/js/logged.js b/wwwroot/assets/js/logged.js index 13748ca..436d561 100644 --- a/wwwroot/assets/js/logged.js +++ b/wwwroot/assets/js/logged.js @@ -2,6 +2,7 @@ const buttons = document.querySelectorAll("button[frame]") const dynmapFrame = document.querySelector("article.frame.dynmap > iframe") const capesSelector = document.querySelector("article.capes > div.capes") const logsContainer = document.querySelector("div.container.logs") +const playButton = document.querySelector("button.play") let viewerInstance = new skinview3d.SkinViewer({ canvas: document.getElementById("skin"), @@ -231,17 +232,55 @@ window.changeUsername = async function changeUsername(newName) { window.gamelog = {} -window.gamelog.put = async function put(log) { +window.gamelog.put = function put(log) { const logElement = document.createElement("p") logElement.innerText = log - logsContainer.appendChild(log) + logsContainer.appendChild(logElement) } -window.gamelog.clear = async function clear() { +window.gamelog.clear = function clear() { logsContainer.innerHTML = "" } +window.play = async function play() { + gamelog.clear() + showLoadingBar() + playButton.setAttribute("disabled", "") + const gameLaunch = await system.call("launcher::game") + if (gameLaunch.success) { + hideLoadingBar() + } else { + hideLoadingBar() + playButton.removeAttribute("disabled") + } +} + +window.external.receiveMessage(message => { + try { + const data = JSON.parse(message) + + switch (data.requestId) { + case "game::log": + const logMessage = data.payload.message + if (logMessage != "GAME_CLOSED") { + console.log("MC:", logMessage) + gamelog.put(logMessage) + logsContainer.scrollTop = logsContainer.scrollHeight + } else { + playButton.removeAttribute("disabled") + } + break; + + default: + break; + } + } catch (e) { + console.error("Erreur réception message Photino:", e) + } +}) + await initSkin() await initSettings() await initCapesSelector() +hideLoadingBar() showFrame("game") \ No newline at end of file diff --git a/wwwroot/logged.html b/wwwroot/logged.html index f0de786..0638f1f 100644 --- a/wwwroot/logged.html +++ b/wwwroot/logged.html @@ -85,9 +85,17 @@

Lentia

- +
+
+
+
+
+
+
+