using System.Linq; using System.Security.Cryptography; using CliWrap; using DTLib.Extensions; using Mlaumcherb.Client.Avalonia.зримое; using Mlaumcherb.Client.Avalonia.классы; using Mlaumcherb.Client.Avalonia.сеть; using Mlaumcherb.Client.Avalonia.сеть.TaskFactories; using Mlaumcherb.Client.Avalonia.холопы; using Newtonsoft.Json.Linq; using static Mlaumcherb.Client.Avalonia.холопы.PathHelper; namespace Mlaumcherb.Client.Avalonia; public class GameVersion { private readonly GameVersionProps _props; public string Name => _props.Name; public IOPath WorkingDirectory { get; } private IOPath JavaExecutableFilePath; private GameVersionDescriptor _descriptor; private JavaArguments _javaArgs; private GameArguments _gameArgs; private Libraries _libraries; private CancellationTokenSource? _gameCts; private CommandTask? _commandTask; public static Task> GetAllVersionsAsync() { var propsList = new List(); // local descriptors Directory.Create(GetVersionsDir()); foreach (IOPath subdir in Directory.GetDirectories(GetVersionsDir())) { string name = subdir.LastName().ToString(); var d = new GameVersionProps(name, null); if(!File.Exists(d.LocalDescriptorPath)) throw new Exception("Can't find version descriptor file in directory '{subdir}'. Rename it as directory name."); propsList.Add(d); } // reverse sort propsList.Sort((a, b) => b.CompareTo(a)); return Task.FromResult(propsList); } public static async Task CreateFromPropsAsync(GameVersionProps props) { if (!File.Exists(props.LocalDescriptorPath)) { if (props.RemoteDescriptorUrl is null) throw new NullReferenceException("can't download game version descriptor '" + props.Name + "', because RemoteDescriptorUrl is null"); await NetworkHelper.DownloadFile(props.RemoteDescriptorUrl, props.LocalDescriptorPath); } return new GameVersion(props); } private GameVersion(GameVersionProps props) { _props = props; string descriptorText = File.ReadAllText(props.LocalDescriptorPath); JObject descriptorRaw = JObject.Parse(descriptorText); // Descriptors can inherit from other descriptors. // For example, 1.12.2-forge-14.23.5.2860 inherits from 1.12.2 if (descriptorRaw.TryGetValue("inheritsFrom", out var v)) { string parentDescriptorId = v.Value() ?? throw new Exception("inheritsFrom is null"); LauncherApp.Logger.LogInfo(Name, $"merging descriptor '{parentDescriptorId}' with '{Name}'"); IOPath parentDescriptorPath = GetVersionDescriptorPath(parentDescriptorId); if (!File.Exists(parentDescriptorPath)) throw new Exception($"Версия '{Name} требует установить версию '{parentDescriptorId}'"); string parentDescriptorText = File.ReadAllText(parentDescriptorPath); JObject parentDescriptorRaw = JObject.Parse(parentDescriptorText); parentDescriptorRaw.Merge(descriptorRaw, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); descriptorRaw = parentDescriptorRaw; // removing dependency descriptorRaw.Remove("inheritsFrom"); File.WriteAllText(props.LocalDescriptorPath, descriptorRaw.ToString()); } _descriptor = descriptorRaw.ToObject() ?? throw new Exception($"can't parse descriptor file '{props.LocalDescriptorPath}'"); _javaArgs = new JavaArguments(_descriptor); _gameArgs = new GameArguments(_descriptor); _libraries = new Libraries(_descriptor); WorkingDirectory = GetVersionDir(_descriptor.id); JavaExecutableFilePath = GetJavaExecutablePath(_descriptor.javaVersion.component, LauncherApp.Config.debug); } public async Task UpdateFiles(bool checkHashes, Action networkTaskCreatedCallback) { LauncherApp.Logger.LogInfo(Name, $"started updating version {Name}"); List taskFactories = [ new AssetsDownloadTaskFactory(_descriptor), new LibrariesDownloadTaskFactory(_descriptor, _libraries), new VersionJarDownloadTaskFactory(_descriptor), ]; if (LauncherApp.Config.download_java) { taskFactories.Add(new JavaDownloadTaskFactory(_descriptor)); } //TODO: modpack /*if (modpack != null) { taskFactories.Add(new ModpackDownloadTaskFactory(modpack)); }*/ var networkTasks = new List(); for (int i = 0; i < taskFactories.Count; i++) { var nt = await taskFactories[i].CreateAsync(checkHashes); if (nt != null) { networkTasks.Add(nt); networkTaskCreatedCallback.Invoke(nt); } } foreach (var nt in networkTasks) { await nt.StartAsync(); } // create log4j config file if (!File.Exists(GetLog4jConfigFilePath())) { await using var res = EmbeddedResources.GetResourceStream( $"Mlaumcherb.Client.Avalonia.встроенное.{GetLog4jConfigFileName()}"); await using var file = File.OpenWrite(GetLog4jConfigFilePath()); await res.CopyToAsync(file); } _props.IsDownloaded = true; LauncherApp.Logger.LogInfo(Name, $"finished updating version {Name}"); } //minecraft player uuid explanation //https://gist.github.com/CatDany/0e71ca7cd9b42a254e49/ //java uuid generation in c# //https://stackoverflow.com/questions/18021808/uuid-interop-with-c-sharp-code public static string GetPlayerUUID(string name) { byte[] name_bytes = Encoding.UTF8.GetBytes("OfflinePlayer:" + name); var md5 = MD5.Create(); byte[] hash = md5.ComputeHash(name_bytes); hash[6] &= 0x0f; hash[6] |= 0x30; hash[8] &= 0x3f; hash[8] |= 0x80; StringBuilder sb = new StringBuilder(); for (int i = 0; i < hash.Length; i++) sb.Append(hash[i].ToString("x2")); sb.Insert(8, '-').Insert(13, '-').Insert(18, '-').Insert(23, '-'); return sb.ToString(); } public async Task Launch() { if (string.IsNullOrWhiteSpace(LauncherApp.Config.player_name)) throw new Exception("invalid player name"); Directory.Create(WorkingDirectory); string uuid = GetPlayerUUID(LauncherApp.Config.player_name); Dictionary placeholder_values = new() { { "resolution_width", "1600" }, { "resolution_height", "900" }, { "xms", LauncherApp.Config.max_memory.ToString() }, { "xmx", LauncherApp.Config.max_memory.ToString() }, { "auth_player_name", LauncherApp.Config.player_name }, { "auth_access_token", uuid }, { "auth_session", $"token:{uuid}:{uuid}" }, { "auth_xuid", "" }, { "auth_uuid", uuid }, { "clientid", "" }, { "user_properties", "{}" }, { "user_type", "userType" }, { "launcher_name", "java-minecraft-launcher" }, { "launcher_version", "1.6.84-j" }, { "classpath", _libraries.Libs.Select(l => l.jarFilePath) .Append(GetVersionJarFilePath(_descriptor.id)) .MergeToString(';') }, { "assets_index_name", _descriptor.assets }, { "assets_root", GetAssetsDir().ToString() }, { "game_assets", GetAssetsDir().ToString() }, { "game_directory", WorkingDirectory.ToString() }, { "natives_directory", GetNativeLibrariesDir(_descriptor.id).ToString() }, { "library_directory", GetLibrariesDir().ToString() }, { "classpath_separator", ";"}, { "path", GetLog4jConfigFilePath().ToString() }, { "version_name", _descriptor.id }, { "version_type", _descriptor.type }, { "arch", PlatformHelper.GetArchOld() }, // { "quickPlayMultiplayer", "" }, // { "quickPlayPath", "" }, // { "quickPlayRealms", "" }, // { "quickPlaySingleplayer", "" }, { "client_jar", GetVersionJarFilePath(_descriptor.id).ToString() }, }; List argsList = new(); argsList.AddRange(_javaArgs.FillPlaceholders(placeholder_values)); argsList.Add(_descriptor.mainClass); argsList.AddRange(_gameArgs.FillPlaceholders(placeholder_values)); var command = Cli.Wrap(JavaExecutableFilePath.ToString()) .WithWorkingDirectory(WorkingDirectory.ToString()) .WithArguments(argsList) .WithValidation(CommandResultValidation.None); if (LauncherApp.Config.redirect_game_output) { command = command .WithStandardOutputPipe(PipeTarget.ToDelegate(LogGameOut)) .WithStandardErrorPipe(PipeTarget.ToDelegate(LogGameError)); } LauncherApp.Logger.LogInfo(Name, "launching the game"); LauncherApp.Logger.LogDebug(Name, "java: " + command.TargetFilePath); LauncherApp.Logger.LogDebug(Name, "working_dir: " + command.WorkingDirPath); LauncherApp.Logger.LogDebug(Name, "arguments: \n\t" + argsList.MergeToString("\n\t")); _gameCts = new(); _commandTask = command.ExecuteAsync(_gameCts.Token); var result = await _commandTask; LauncherApp.Logger.LogInfo(Name, $"game exited with code {result.ExitCode}"); } private void LogGameOut(string line) { LauncherApp.Logger.LogInfo(Name, line); } private void LogGameError(string line) { LauncherApp.Logger.LogWarn(Name, line); } public void Close() { _gameCts?.Cancel(); } public override string ToString() => Name; }