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:
parent
31fbf19189
commit
6f2e5f81a8
@ -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) => {
|
||||||
|
|||||||
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
92
src/main/utils/LentiaFiles.cs
Normal file
92
src/main/utils/LentiaFiles.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user