diff --git a/src/main/Program.cs b/src/main/Program.cs index 52496f8..172a433 100644 --- a/src/main/Program.cs +++ b/src/main/Program.cs @@ -1,7 +1,6 @@ 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; @@ -50,7 +49,7 @@ class Program { switch (method) { case "launcher::version": - payload = "v1.0.0-alpha"; + payload = "v1.0.0-beta"; break; case "window::close": @@ -212,6 +211,7 @@ class Program { case "launcher::game": try { + await GenericFilesService.FetchAndSyncGenericFiles(gameRoot); var options = new LaunchOptions { Version = new LaunchOptions.VersionOptions { Number = "1.12.2", @@ -222,6 +222,9 @@ class Program { Min = "512M" }, ModLoader = LaunchOptions.ModLoaderType.Forge, + CustomArgs = new List { + $"-javaagent:{Path.Combine(gameRoot, LauncherConstants.AgentsPath.AuthlibInjector)}={LauncherConstants.Urls.YggdrasilServer}" + } }; MinecraftVersion version = await GameHelper.PrepareGame(options, gameRoot); GameHelper.Launch(version, _authenticatedPlayer!, gameRoot, options, (logLine) => { diff --git a/src/main/core/Constants.cs b/src/main/core/Constants.cs index 7203259..6282445 100644 --- a/src/main/core/Constants.cs +++ b/src/main/core/Constants.cs @@ -1,13 +1,18 @@ -using System.Net.Http; using System.Text.Json; namespace Lentia.Core.Constants { public static class LauncherConstants { public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - public static class Urls - { + public static class Urls { public const string MojangManifest = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; - public const string MojangAuthServer = "https://yggdrasil.azures.fr/authserver"; + public const string YggdrasilServer = "https://yggdrasil.azures.fr/"; + public const string MojangAuthServer = YggdrasilServer + "/authserver"; + + public const string ApiUrl = "https://lentia-api.azures.fr"; + } + + public static class AgentsPath { + public const string AuthlibInjector = "libraries/moe/yushi/authlib-injector/1.2.7/authlib-injector-1.2.7.jar"; } } } \ No newline at end of file diff --git a/src/main/utils/LentiaFiles.cs b/src/main/utils/LentiaFiles.cs new file mode 100644 index 0000000..f7c9a8a --- /dev/null +++ b/src/main/utils/LentiaFiles.cs @@ -0,0 +1,92 @@ +using System.Net.Http.Json; +using System.Security.Cryptography; +using Lentia.Core.Constants; +using Lentia.Core.Utils; + +namespace Lentia.Core.Game; + +public class GenericArtifact { + public string Path { get; set; } = string.Empty; + public string Sha1 { get; set; } = string.Empty; + public long Size { get; set; } + public string Url { get; set; } = string.Empty; +} + +public class GameFilesIndex { + public List Root { get; set; } = new(); +} + +public class DownloadsWrapper { + public GenericArtifact Artifact { get; set; } = null!; +} + +public class ArtifactWrapper { + public DownloadsWrapper Downloads { get; set; } = null!; + public string Name { get; set; } = string.Empty; +} + +public static class GenericFilesService { + + public static async Task FetchAndSyncGenericFiles(string gameRoot) { + try { + var response = await HttpHelper.FetchAsync($"{LauncherConstants.Urls.ApiUrl}/api/v2/gamefiles", HttpMethod.Get); + response.EnsureSuccessStatusCode(); + + var index = await response.Content.ReadFromJsonAsync(LauncherConstants._jsonOptions); + + if (index?.Root != null) { + var artifacts = index.Root.Select(r => r.Downloads.Artifact).ToList(); + await DownloadFiles(artifacts, gameRoot); + } + } + catch (Exception ex) { + Console.WriteLine($"[API Error] Impossible de récupérer les fichiers : {ex.Message}"); + } + } + + public static async Task DownloadFiles(List artifacts, string gameRoot) { + var downloadQueue = new List(); + + foreach (var artifact in artifacts) { + string localPath = Path.Combine(gameRoot, artifact.Path); + + if (!IsFileValid(localPath, artifact.Size, artifact.Sha1)) { + downloadQueue.Add(artifact); + } + } + + if (downloadQueue.Count == 0) return; + + var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; + await Parallel.ForEachAsync(downloadQueue, options, async (artifact, token) => { + string localPath = Path.Combine(gameRoot, artifact.Path); + + try { + await FileHelper.DownloadFileAsync(artifact.Url, localPath); + + if (!IsFileValid(localPath, artifact.Size, artifact.Sha1)) { + throw new Exception($"Échec de la validation post-téléchargement pour : {artifact.Path}"); + } + } catch (Exception ex) { + Console.WriteLine($"[DownloadError] {artifact.Path} : {ex.Message}"); + } + }); + } + + public static bool IsFileValid(string localPath, long expectedSize, string expectedSha1) { + if (!File.Exists(localPath)) return false; + + var fileInfo = new FileInfo(localPath); + if (fileInfo.Length != expectedSize) return false; + + string localHash = GetFileSha1(localPath); + return localHash.Equals(expectedSha1, StringComparison.OrdinalIgnoreCase); + } + + private static string GetFileSha1(string filePath) { + using var stream = File.OpenRead(filePath); + using var sha1 = SHA1.Create(); + byte[] hashBytes = sha1.ComputeHash(stream); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/wwwroot/assets/css/logged.css b/wwwroot/assets/css/logged.css index 1841468..137a6c5 100644 --- a/wwwroot/assets/css/logged.css +++ b/wwwroot/assets/css/logged.css @@ -54,6 +54,8 @@ main > aside > nav > button:first-of-type > img { width: 60px; height: 60px; border-radius: 5px; + image-rendering: pixelated; + image-rendering: crisp-edges; } main > aside > nav > button:nth-child(2) { @@ -70,7 +72,7 @@ main > aside > nav > button:last-child { margin-bottom: 0; } -main > aside > nav > button:has(> i.fas.fa-sign-out-alt):hover { +main > aside > nav > button:has(> i.fas.fa-sign-out-alt):not(:disabled):hover { color: #ff4747; } @@ -124,6 +126,11 @@ button.play:disabled { filter: grayscale(100%); } +button.logout:disabled { + cursor: not-allowed; + filter: brightness(0.70); +} + button.play > i { margin-right: 5px; font-size: 14px; diff --git a/wwwroot/assets/js/logged.js b/wwwroot/assets/js/logged.js index 436d561..75f0e13 100644 --- a/wwwroot/assets/js/logged.js +++ b/wwwroot/assets/js/logged.js @@ -3,6 +3,7 @@ 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") +const logoutButton = document.querySelector("button.logout") let viewerInstance = new skinview3d.SkinViewer({ canvas: document.getElementById("skin"), @@ -43,7 +44,29 @@ window.getPlayer = async function getPlayer() { return result } +window.displayMinecraftHead = function displayMinecraftHead(skinUrl, targetImg) { + const canvas = document.createElement("canvas") + const ctx = canvas.getContext("2d") + const skinImg = new Image() + + skinImg.crossOrigin = "anonymous" + + skinImg.onload = function() { + canvas.width = 8 + canvas.height = 8 + + ctx.imageSmoothingEnabled = false + + ctx.drawImage(skinImg, 8, 8, 8, 8, 0, 0, 8, 8) + ctx.drawImage(skinImg, 40, 8, 8, 8, 0, 0, 8, 8) + document.querySelector(targetImg).src = canvas.toDataURL() + } + + skinImg.src = skinUrl +} + window.profile = await getPlayer() +displayMinecraftHead(`https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url.replace(/^\//, "")}`, "img.avatar") window.refreshProfile = async function refreshProfile() { window.profile = await getPlayer() @@ -97,13 +120,16 @@ window.initSkin = async function initSkin() { return } + const skinUrl = `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url.replace(/^\//, "")}` viewerInstance = new skinview3d.SkinViewer({ canvas: document.getElementById("skin"), width: container.clientWidth, height: container.clientHeight, - skin: `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url.replace(/^\//, "")}` + skin: skinUrl }) + displayMinecraftHead(skinUrl, "img.avatar") + const activeCape = window.profile.capes.find(s => s.state === "ACTIVE") || null const capeUrl = activeCape == null ? null : `https://yggdrasil.azures.fr/textures/${activeCape.url.replace(/^\//, "")}?t=${Date.now()}` @@ -147,6 +173,7 @@ window.validateSkinSelection = async function validateSkinSelection(localPath) { if (activeSkin) { const skinUrl = `https://yggdrasil.azures.fr/textures/${activeSkin.url.replace(/^\//, "")}?t=${Date.now()}` const capeUrl = activeCape == null ? null : `https://yggdrasil.azures.fr/textures/${activeCape.url.replace(/^\//, "")}?t=${Date.now()}` + displayMinecraftHead(skinUrl, "img.avatar") await viewerInstance.loadSkin(skinUrl, { model: variant.toLowerCase() === "slim" ? "slim" : "default" }) @@ -246,12 +273,14 @@ window.play = async function play() { gamelog.clear() showLoadingBar() playButton.setAttribute("disabled", "") + logoutButton.setAttribute("disabled", "") const gameLaunch = await system.call("launcher::game") if (gameLaunch.success) { hideLoadingBar() } else { hideLoadingBar() playButton.removeAttribute("disabled") + logoutButton.removeAttribute("disabled") } } diff --git a/wwwroot/logged.html b/wwwroot/logged.html index 0638f1f..5ef7c10 100644 --- a/wwwroot/logged.html +++ b/wwwroot/logged.html @@ -11,7 +11,7 @@