diff --git a/Service/EngineService.cs b/Service/EngineService.cs index 44d9788..44e19a0 100644 --- a/Service/EngineService.cs +++ b/Service/EngineService.cs @@ -37,11 +37,22 @@ public IEnumerable ScanInstalledEngines(string installLocation) var binaryName = $"{_launcherCLIAppName}{EngineExtension}"; - foreach (var versionDir in Directory.EnumerateDirectories(installLocation)) + IEnumerable versionDirs; + try { versionDirs = Directory.EnumerateDirectories(installLocation).ToList(); } + catch (UnauthorizedAccessException) { yield break; } + catch (IOException) { yield break; } + + foreach (var versionDir in versionDirs) { - var binaryPath = Directory - .EnumerateFiles(versionDir, binaryName, SearchOption.AllDirectories) - .FirstOrDefault(); + string? binaryPath = null; + try + { + binaryPath = Directory + .EnumerateFiles(versionDir, binaryName, SearchOption.AllDirectories) + .FirstOrDefault(); + } + catch (UnauthorizedAccessException) { } + catch (IOException) { } if (binaryPath != null) yield return new InstalledEngine diff --git a/ViewModels/AssetDetailViewModel.cs b/ViewModels/AssetDetailViewModel.cs index c28f813..83e51a8 100644 --- a/ViewModels/AssetDetailViewModel.cs +++ b/ViewModels/AssetDetailViewModel.cs @@ -13,7 +13,8 @@ internal class AssetFormatOption : ReactiveObject public string Resolution { get; } public string Url { get; } public string Label => $"{Format.ToUpperInvariant()} · {Resolution}"; - public List CompanionUrls { get; } = new(); + // RelativePath is the inc.Key from the Poly Haven API (e.g. "textures/foo_diff_4k.jpg", "foo.bin") + public List<(string RelativePath, string Url)> CompanionFiles { get; } = new(); private bool _isSelected; public bool IsSelected diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 0bae26a..aba0a68 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -441,6 +441,7 @@ public MainWindowViewModel(IProjectService projectService, IEngineService engine _ = RunBackgroundTask("Scan local assets", () => Task.Run(() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(ScanLocalAssets))); _ = RunBackgroundTask("Load projects", LoadProjectsAsync); + _ = Task.Run(MigrateAssetFoldersAsync); }); var canConfirm = this.WhenAnyValue( @@ -877,7 +878,7 @@ await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { var incUrl = incFile["url"]?.GetValue() ?? string.Empty; if (!string.IsNullOrEmpty(incUrl)) - option.CompanionUrls.Add(incUrl); + option.CompanionFiles.Add((inc.Key, incUrl)); } } } @@ -943,14 +944,17 @@ async Task UI(System.Action a) => await DownloadFileWithProgressAsync(fmt.Url, destPath, pct => UI(() => { op.Progress = pct; })); - // Download companion files (e.g. .bin buffer for GLTF) + // Download companion files, preserving the relative path from the API + // (e.g. "textures/foo_diff_4k.jpg" → destDir/textures/foo_diff_4k.jpg) var destDir2 = System.IO.Path.GetDirectoryName(destPath)!; - foreach (var companionUrl in fmt.CompanionUrls) + foreach (var (relativePath, companionUrl) in fmt.CompanionFiles) { try { - var companionFileName = System.IO.Path.GetFileName(new Uri(companionUrl).LocalPath); - var companionDest = System.IO.Path.Combine(destDir2, companionFileName); + var companionDest = System.IO.Path.Combine( + destDir2, + relativePath.Replace('/', System.IO.Path.DirectorySeparatorChar)); + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(companionDest)!); await DownloadFileWithProgressAsync(companionUrl, companionDest, _ => Task.CompletedTask); } catch { /* non-fatal */ } @@ -997,6 +1001,49 @@ private void ScanLocalAssets() RefreshDownloadedStates(); } + private async Task MigrateAssetFoldersAsync() + { + var root = Models.AppPaths.Assets; + if (!Directory.Exists(root)) return; + + string[] imageExts = ["jpg", "jpeg", "png", "webp", "hdr", "exr"]; + + // Collect all flat image files that need moving + var toMove = new List<(string Src, string Dest)>(); + foreach (var assetDir in Directory.EnumerateDirectories(root)) + foreach (var resDir in Directory.EnumerateDirectories(assetDir)) + { + foreach (var file in Directory.EnumerateFiles(resDir)) + { + var ext = Path.GetExtension(file).TrimStart('.').ToLowerInvariant(); + if (!imageExts.Contains(ext)) continue; + var dest = Path.Combine(resDir, "textures", Path.GetFileName(file)); + if (!File.Exists(dest)) + toMove.Add((file, dest)); + } + } + + if (toMove.Count == 0) return; + + await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => + _ = ShowToastAsync($"Migrating {toMove.Count} texture file(s) into textures/ folders…")); + + var moved = 0; + foreach (var (src, dest) in toMove) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + File.Move(src, dest, overwrite: true); + moved++; + } + catch { /* non-fatal — leave file in place */ } + } + + await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => + _ = ShowToastAsync($"Asset folders updated — {moved} texture(s) moved to textures/")); + } + private void RefreshDownloadedStates() { var ids = LocalAssets.Select(a => a.AssetId) diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index ebd9fab..083fa29 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -78,7 +78,7 @@ mc:Ignorable="d" d:DesignWidth="1500" d:DesignHeight="800"