Add generic file sync and improve UI for game launch

Introduced GenericFilesService to fetch and validate required game files before launching. Updated launcher version to beta and added support for AuthlibInjector agent in launch options. Improved avatar rendering and button states in the UI, including pixelated avatar display and disabled state handling for logout during game launch.
This commit is contained in:
Gilles Lazures 2026-01-25 23:22:08 +01:00
parent 31fbf19189
commit 6f2e5f81a8
6 changed files with 146 additions and 10 deletions

View File

@ -1,7 +1,6 @@
using Lentia.Core.Auth.OAuth2; using Lentia.Core.Auth.OAuth2;
using Lentia.Core.Constants; using Lentia.Core.Constants;
using Lentia.Core.Game; using Lentia.Core.Game;
using Lentia.Core.Game.Extra;
using Lentia.Utils; using Lentia.Utils;
using Photino.NET; using Photino.NET;
using System.Text.Json; using System.Text.Json;
@ -50,7 +49,7 @@ class Program {
switch (method) { switch (method) {
case "launcher::version": case "launcher::version":
payload = "v1.0.0-alpha"; payload = "v1.0.0-beta";
break; break;
case "window::close": case "window::close":
@ -212,6 +211,7 @@ class Program {
case "launcher::game": case "launcher::game":
try { try {
await GenericFilesService.FetchAndSyncGenericFiles(gameRoot);
var options = new LaunchOptions { var options = new LaunchOptions {
Version = new LaunchOptions.VersionOptions { Version = new LaunchOptions.VersionOptions {
Number = "1.12.2", Number = "1.12.2",
@ -222,6 +222,9 @@ class Program {
Min = "512M" Min = "512M"
}, },
ModLoader = LaunchOptions.ModLoaderType.Forge, ModLoader = LaunchOptions.ModLoaderType.Forge,
CustomArgs = new List<string> {
$"-javaagent:{Path.Combine(gameRoot, LauncherConstants.AgentsPath.AuthlibInjector)}={LauncherConstants.Urls.YggdrasilServer}"
}
}; };
MinecraftVersion version = await GameHelper.PrepareGame(options, gameRoot); MinecraftVersion version = await GameHelper.PrepareGame(options, gameRoot);
GameHelper.Launch(version, _authenticatedPlayer!, gameRoot, options, (logLine) => { GameHelper.Launch(version, _authenticatedPlayer!, gameRoot, options, (logLine) => {

View File

@ -1,13 +1,18 @@
using System.Net.Http;
using System.Text.Json; using System.Text.Json;
namespace Lentia.Core.Constants { namespace Lentia.Core.Constants {
public static class LauncherConstants { public static class LauncherConstants {
public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; 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 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";
} }
} }
} }

View File

@ -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<ArtifactWrapper> 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<GameFilesIndex>(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<GenericArtifact> artifacts, string gameRoot) {
var downloadQueue = new List<GenericArtifact>();
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();
}
}

View File

@ -54,6 +54,8 @@ main > aside > nav > button:first-of-type > img {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 5px; border-radius: 5px;
image-rendering: pixelated;
image-rendering: crisp-edges;
} }
main > aside > nav > button:nth-child(2) { main > aside > nav > button:nth-child(2) {
@ -70,7 +72,7 @@ main > aside > nav > button:last-child {
margin-bottom: 0; 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; color: #ff4747;
} }
@ -124,6 +126,11 @@ button.play:disabled {
filter: grayscale(100%); filter: grayscale(100%);
} }
button.logout:disabled {
cursor: not-allowed;
filter: brightness(0.70);
}
button.play > i { button.play > i {
margin-right: 5px; margin-right: 5px;
font-size: 14px; font-size: 14px;

View File

@ -3,6 +3,7 @@ const dynmapFrame = document.querySelector("article.frame.dynmap > iframe")
const capesSelector = document.querySelector("article.capes > div.capes") const capesSelector = document.querySelector("article.capes > div.capes")
const logsContainer = document.querySelector("div.container.logs") const logsContainer = document.querySelector("div.container.logs")
const playButton = document.querySelector("button.play") const playButton = document.querySelector("button.play")
const logoutButton = document.querySelector("button.logout")
let viewerInstance = new skinview3d.SkinViewer({ let viewerInstance = new skinview3d.SkinViewer({
canvas: document.getElementById("skin"), canvas: document.getElementById("skin"),
@ -43,7 +44,29 @@ window.getPlayer = async function getPlayer() {
return result 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() 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.refreshProfile = async function refreshProfile() {
window.profile = await getPlayer() window.profile = await getPlayer()
@ -97,13 +120,16 @@ window.initSkin = async function initSkin() {
return return
} }
const skinUrl = `https://yggdrasil.azures.fr/textures/${profile.skins.find(skin => skin.state == "ACTIVE").url.replace(/^\//, "")}`
viewerInstance = new skinview3d.SkinViewer({ viewerInstance = new skinview3d.SkinViewer({
canvas: document.getElementById("skin"), canvas: document.getElementById("skin"),
width: container.clientWidth, width: container.clientWidth,
height: container.clientHeight, 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 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()}` 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) { if (activeSkin) {
const skinUrl = `https://yggdrasil.azures.fr/textures/${activeSkin.url.replace(/^\//, "")}?t=${Date.now()}` 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()}` const capeUrl = activeCape == null ? null : `https://yggdrasil.azures.fr/textures/${activeCape.url.replace(/^\//, "")}?t=${Date.now()}`
displayMinecraftHead(skinUrl, "img.avatar")
await viewerInstance.loadSkin(skinUrl, { await viewerInstance.loadSkin(skinUrl, {
model: variant.toLowerCase() === "slim" ? "slim" : "default" model: variant.toLowerCase() === "slim" ? "slim" : "default"
}) })
@ -246,12 +273,14 @@ window.play = async function play() {
gamelog.clear() gamelog.clear()
showLoadingBar() showLoadingBar()
playButton.setAttribute("disabled", "") playButton.setAttribute("disabled", "")
logoutButton.setAttribute("disabled", "")
const gameLaunch = await system.call("launcher::game") const gameLaunch = await system.call("launcher::game")
if (gameLaunch.success) { if (gameLaunch.success) {
hideLoadingBar() hideLoadingBar()
} else { } else {
hideLoadingBar() hideLoadingBar()
playButton.removeAttribute("disabled") playButton.removeAttribute("disabled")
logoutButton.removeAttribute("disabled")
} }
} }

View File

@ -11,7 +11,7 @@
<aside> <aside>
<nav> <nav>
<button frame="profile" onclick="showPage(this.getAttribute('frame'))"> <button frame="profile" onclick="showPage(this.getAttribute('frame'))">
<img src="https://minotar.net/helm/BOBsonic576" alt=""> <img class="avatar" src="" alt="">
</button> </button>
<button frame="game" onclick="showPage(this.getAttribute('frame'))"> <button frame="game" onclick="showPage(this.getAttribute('frame'))">
<i class="fas fa-gamepad"></i> <i class="fas fa-gamepad"></i>
@ -25,7 +25,7 @@
<button frame="dynmap" onclick="initDynmap(); showPage(this.getAttribute('frame'))"> <button frame="dynmap" onclick="initDynmap(); showPage(this.getAttribute('frame'))">
<i class="fas fa-map"></i> <i class="fas fa-map"></i>
</button> </button>
<button onclick="system.call('auth::logout')"> <button class="logout" onclick="system.call('auth::logout')">
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
</button> </button>
</nav> </nav>