diff --git a/Obsidian/HakoVault/Minecraft/Installation.md b/Obsidian/HakoVault/Minecraft/Installation.md index 8e7094a..d8354b7 100644 --- a/Obsidian/HakoVault/Minecraft/Installation.md +++ b/Obsidian/HakoVault/Minecraft/Installation.md @@ -73,7 +73,11 @@ Hako под ARM64 тоже распознаёт ARM64-хост. На ARM64 сн `MojangCompatibility.Libraries` сохраняет порядок библиотек из манифеста, включая legacy natives. Суффикс `${arch}` в legacy classifier подменяется на `64`, `32` или `arm64`; `natives-windows` без -суффикса также поддерживается для x86. +суффикса также поддерживается для x86. Все выбранные natives — и старого формата с картой `natives`, +и нового формата 26.3+, где каждый native-jar — отдельная запись с classifier в `name` и +`downloads.artifact` — всегда ставятся на распаковку (`ExtractNatives`), поэтому нативные jar не +попадают в classpath и всегда распаковываются в natives-каталог; excludes берутся из `extract.exclude`, +когда они есть у записи. `MinecraftInstaller.InstallAsync` пишет только в корень переданной сборки и использует `InstanceStorage.ContainedPath` этапа хранения для путей и запрета reparse points. Java собирается из @@ -84,13 +88,16 @@ runtime manifest raw-файлов. Link-записи отклоняются на Очистка best-effort и не останавливает установку при ошибках доступа, ввода-вывода или гонках. Это безопасно, поскольку `InstanceStore` не допускает одновременные установки одного корня. Клиент и полный JSON версии размещаются в `minecraft/versions/{id}/`, библиотеки — в -`minecraft/libraries/`, legacy natives после безопасной проверки ZIP — в `minecraft/natives/`, logging — -в `minecraft/assets/log_configs/`. Современные native JAR остаются в libraries. Asset objects хранятся +`minecraft/libraries/`, native JAR после безопасной проверки ZIP — в `minecraft/natives/`, а при +шаблоне JVM `java.library.path=${natives_directory}/java` (формат версии 26.3+) — в +`minecraft/natives/java/`; каталог определяется по аргументам версии (`NativePublishDirectory`), +logging — в `minecraft/assets/log_configs/`. Asset objects хранятся в `minecraft/assets/objects/`; только virtual и `map_to_resources` проверяют имена и создают проверяемые копии, коллизии Windows-путей без учёта регистра отклоняются. Создаются `minecraft/mods` и папка -ресурспаков (`resourcepacks`, либо `texturepacks` для legacy версии). Все legacy native archives сначала -распаковываются во временную staging-папку и публикуются заменой каталога `minecraft/natives` после -завершения; если переименование staging не удалось, прежний каталог восстанавливается. Отмена и ошибки +ресурспаков (`resourcepacks`, либо `texturepacks` для legacy версии). Все native archives сначала +распаковываются во временную staging-папку и публикуются заменой каталога `minecraft/natives` (или +`minecraft/natives/java`) после завершения; если переименование staging не удалось, прежний каталог +восстанавливается. Отмена и ошибки распаковки удаляют staging без частично опубликованных файлов. `Instances/InstallationCoordinator.cs` — app-lifetime FIFO-очередь, одна активная установка, порядок по `CreatedAt`. diff --git a/windows/Hako.Core/Minecraft/MinecraftInstaller.cs b/windows/Hako.Core/Minecraft/MinecraftInstaller.cs index 052bad5..4afa168 100644 --- a/windows/Hako.Core/Minecraft/MinecraftInstaller.cs +++ b/windows/Hako.Core/Minecraft/MinecraftInstaller.cs @@ -39,6 +39,7 @@ public sealed class MinecraftInstaller(MojangClient client, FabricClient? fabric private const int MaximumNativeArchiveEntries = 100_000; private const long MaximumNativeArchiveSize = 1024L * 1024 * 1024; private const int BufferSize = 81920; + private const string NativeDirectoryJavaTemplate = "${natives_directory}/java"; private static readonly HashSet ReservedDeviceNames = BuildReservedDeviceNames(); private readonly FabricClient _fabricClient = fabricClient ?? new FabricClient(); @@ -330,7 +331,8 @@ async Task DownloadAndCancelOnErrorAsync(DownloadJob job) progress?.Report(new InstallationProgress(InstallationProgressStage.PreparingLibraries, totalBytes, totalBytes, jobs.Length, jobs.Length)); if (nativeArchives.Count > 0) { - await ExtractNativesAsync(nativeArchives.DistinctBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray(), root, cancellationToken) + await ExtractNativesAsync(nativeArchives.DistinctBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray(), root, + NativePublishDirectory(manifest), cancellationToken) .ConfigureAwait(false); } @@ -591,9 +593,36 @@ private static async Task CopyAtomicallyAsync( } } + /// + /// Versions whose JVM template points java.library.path at ${natives_directory}/java (Minecraft 26.3+) + /// publish native libraries into that subdirectory; older versions use the natives folder root. + /// + private static string NativePublishDirectory(MojangVersionMetadata manifest) + { + const string nativesRoot = "minecraft/natives"; + if (manifest.Arguments is not { } arguments) + { + return nativesRoot; + } + + foreach (var group in arguments.Values) + { + foreach (var argument in group) + { + if (argument.Values.Any(value => value.Contains(NativeDirectoryJavaTemplate, StringComparison.Ordinal))) + { + return $"{nativesRoot}/java"; + } + } + } + + return nativesRoot; + } + private static async Task ExtractNativesAsync( IReadOnlyList archives, string root, + string destinationRelative, CancellationToken cancellationToken) { var stagingRelative = $"minecraft/.hako-natives-{Guid.NewGuid():N}.tmp"; @@ -608,7 +637,7 @@ private static async Task ExtractNativesAsync( } cancellationToken.ThrowIfCancellationRequested(); - PublishNativeStaging(stagingPath, root, Directory.Move); + PublishNativeStaging(stagingPath, root, Directory.Move, destinationRelative); } finally { @@ -749,7 +778,8 @@ internal static async Task ExtractNativeEntryAsync( } } - internal static void PublishNativeStaging(string stagingPath, string root, Action moveDirectory) + internal static void PublishNativeStaging(string stagingPath, string root, Action moveDirectory, + string destinationRelative = "minecraft/natives") { var pending = new Stack(); pending.Push(stagingPath); @@ -770,7 +800,8 @@ internal static void PublishNativeStaging(string stagingPath, string root, Actio } } - var destination = InstanceStorage.ContainedPath("minecraft/natives", root); + var destination = InstanceStorage.ContainedPath(destinationRelative, root); + CreateDirectory(Path.GetDirectoryName(destinationRelative) ?? "minecraft", root); var backup = InstanceStorage.ContainedPath($"minecraft/.hako-natives-backup-{Guid.NewGuid():N}.tmp", root); var destinationMoved = false; try diff --git a/windows/Hako.Core/Minecraft/MojangClient.cs b/windows/Hako.Core/Minecraft/MojangClient.cs index 2735e22..191264c 100644 --- a/windows/Hako.Core/Minecraft/MojangClient.cs +++ b/windows/Hako.Core/Minecraft/MojangClient.cs @@ -496,7 +496,7 @@ public static IReadOnlyList Libraries( native, native.Path, IsNative: true, - ExtractNatives: library.Extract is not null, + ExtractNatives: true, ExtractionExcludes: library.Extract?.Exclude ?? [])); mappedGroups.Add(library.NativeGroup); } @@ -528,7 +528,7 @@ public static IReadOnlyList Libraries( download, download.Path, IsNative: true, - ExtractNatives: candidate.Library.Extract is not null, + ExtractNatives: true, ExtractionExcludes: candidate.Library.Extract?.Exclude ?? [])); mappedGroups.Add(group.Key); } diff --git a/windows/Hako.Tests/MinecraftInstallationTests.cs b/windows/Hako.Tests/MinecraftInstallationTests.cs index 4bd0bae..cec65c3 100644 --- a/windows/Hako.Tests/MinecraftInstallationTests.cs +++ b/windows/Hako.Tests/MinecraftInstallationTests.cs @@ -200,6 +200,60 @@ public void LegacyWindowsNativeWithoutSuffixIsSupportedOnX86() Assert.IsTrue(libraries.Single().IsNative); } + [TestMethod] + public void ClassifiedNativeEntriesWithoutAnExtractSectionAreQueuedForExtraction() + { + var manifest = new MojangVersionMetadata + { + Libraries = + [ + new MojangLibrary + { + Name = "org.lwjgl:lwjgl:3.4.3", + Downloads = new MojangLibraryDownloads { Artifact = Download("lwjgl-3.4.3.jar") } + }, + new MojangLibrary + { + Name = "org.lwjgl:lwjgl:3.4.3:natives-windows", + Downloads = new MojangLibraryDownloads { Artifact = Download("lwjgl-3.4.3-natives-windows.jar") }, + Rules = [new MojangRule { Action = "allow", Os = new MojangOperatingSystem { Name = "windows" } }] + } + ] + }; + + var libraries = MojangCompatibility.Libraries(manifest, GameArchitecture.X64); + + Assert.AreEqual(2, libraries.Count); + var native = libraries.Single(library => library.IsNative); + Assert.AreEqual("lwjgl-3.4.3-natives-windows.jar", native.Path); + Assert.IsTrue(native.ExtractNatives); + Assert.IsFalse(native.ExtractionExcludes!.Any()); + } + + [TestMethod] + public void NativeMapLibrariesExtractEvenWhenTheExtractSectionIsMissing() + { + var manifest = new MojangVersionMetadata + { + Libraries = + [ + new MojangLibrary + { + Name = "org.lwjgl:lwjgl:3.3.1", + Natives = new Dictionary { ["windows"] = "natives-windows" }, + Downloads = new MojangLibraryDownloads + { + Classifiers = new Dictionary { ["natives-windows"] = Download("lwjgl-natives-windows.jar") } + } + } + ] + }; + + var libraries = MojangCompatibility.Libraries(manifest, GameArchitecture.X64); + + Assert.IsTrue(libraries.Single().ExtractNatives); + } + [TestMethod] [DataRow("8u74-b01", 8)] [DataRow("1.8.0_202", 8)] @@ -778,6 +832,25 @@ public async Task LegacyNativeArchiveHonorsMetaInfExclusion() Assert.IsTrue(File.Exists(Path.Combine(temporary.Path, "minecraft", "natives", "org", "test", "native.dll"))); } + [TestMethod] + public async Task ModernNativesPublishIntoTheJavaLibraryPathSubdirectory() + { + using var temporary = new TestDirectory(); + var zip = CreateZip("lwjgl.dll", "META-INF/MANIFEST.MF"); + var fixture = CreateInstallFixture( + includeAssets: false, + arguments: JvmArguments("-Djava.library.path=${natives_directory}/java", "-Djna.tmpdir=${natives_directory}/jna"), + libraries: [CreateNativeLibrary(zip, ["META-INF/"])], + nativeArchiveBytes: zip); + using var client = new MojangClient(new HttpClient(fixture.Handler)); + var installer = new MinecraftInstaller(client); + + await installer.InstallAsync(fixture.Prepared, temporary.Path); + + Assert.IsTrue(File.Exists(Path.Combine(temporary.Path, "minecraft", "natives", "java", "lwjgl.dll"))); + Assert.IsFalse(File.Exists(Path.Combine(temporary.Path, "minecraft", "natives", "java", "META-INF", "MANIFEST.MF"))); + } + [TestMethod] public async Task FailureInSecondNativeArchiveDoesNotPublishAnyStagedFiles() { @@ -1017,6 +1090,7 @@ private static InstallFixture CreateInstallFixture( IReadOnlyList? assetNames = null, bool virtualAssets = true, bool mapToResources = true, + Dictionary>? arguments = null, IReadOnlyCollection? libraries = null, byte[]? nativeArchiveBytes = null, IReadOnlyDictionary? downloadPayloadOverrides = null, @@ -1076,6 +1150,7 @@ private static InstallFixture CreateInstallFixture( JavaVersion = new MojangJavaRequirement { Component = "java-runtime-gamma", MajorVersion = 17 }, Downloads = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["client"] = clientDownload }, Libraries = [], + Arguments = arguments, AssetIndex = assetReference, Logging = new Dictionary { @@ -1173,6 +1248,11 @@ private static byte[] CreateZip(params string[] entryNames) Size = 7 }; + private static Dictionary> JvmArguments(params string[] values) => new(StringComparer.Ordinal) + { + ["jvm"] = values.Select(value => new MojangArgument { Value = JsonSerializer.SerializeToElement(value) }).ToList() + }; + private sealed record InstallFixture(PreparedInstallation Prepared, PayloadHandler Handler, byte[] ClientBytes); private sealed class PayloadHandler(