diff --git a/CLAUDE.md b/CLAUDE.md index 1d23814..bd98cc9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ Source layout under `src/OrthoStreamXP/`. Sub-folders are for navigation only: * - `Aircraft/`: `AircraftPositionListener` (X-Plane UDP), `ViewTracker`, `SceneryLoads`. - `Colour/`: `ColourMatcher` (evens patchwork), `SeasonClock` (seasonal tint by simulated date/location). - `Textures/` — imagery to DDS: `TextureBuilder` (fetch → stitch → resize → mipmaps → BC1 encode → atomic write; own encoder, SkiaSharp is decode-only), `TextureQuality`, `Images` at the root; `Tiles/` (`TileSource`/`TileStore`: download + cache), `Dds/`, `Scheduling/` (priority gates), `Seasons/`, `Colour/` (`ColourMatch`, `Exposure`, `ColourTable` = the shared BC1 lookup-table recolouring, `PhotoColours` = match → season → exposure as X-Plane reads, also used by the Settings preview). -- `Setup/` — `Settings/` (`SettingsDocument`, ini under `%LOCALAPPDATA%\OrthoStreamXP`, `QualityPreset`, `ConnectionTest`), `Cache/` (inventory, areas, eviction), `Packages/` (downloads AutoOrtho's mesh regions from GitHub into the packages folder as `OSXP_*` packages: `MeshCatalog`, `AssetDownload`, `ZipUnpacker`, `OverlayPackage`, `RegionInstaller`), `Installation/` (`AppPaths`, `AppUpdates`, `LegacyNames` migrating older settings/cache locations, `SystemCheck`, Velopack hooks). +- `Setup/` — `Settings/` (`SettingsDocument`, ini under `%LOCALAPPDATA%\OrthoStreamXP`, `QualityPreset`, `ConnectionTest`), `Cache/` (inventory, areas, eviction), `Packages/` (downloads AutoOrtho's mesh regions from GitHub into the packages folder as `OrthoStream_*` packages: `MeshCatalog`, `AssetDownload`, `ZipUnpacker`, `OverlayPackage`, `RegionInstaller`), `Installation/` (`AppPaths`, `AppUpdates`, `LegacyNames` migrating older settings/cache locations, `SystemCheck`, Velopack hooks). - `Tuning/` — flight reports, facts and the preset/setting recommendations. - Logs go to `%LOCALAPPDATA%\OrthoStreamXP\logs`. diff --git a/docs/packages.md b/docs/packages.md index ff36f5f..5fb92e3 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -10,12 +10,12 @@ The meshes are downloaded from a public collection on [GitHub](https://github.co | Region | Package | Download (with roads, forests and buildings) | |---|---|---| -| Europe | `OSXP_Eur` | about 16 GB | -| North America | `OSXP_NA` | about 25 GB | -| South America | `OSXP_SA` | about 9 GB | -| Africa | `OSXP_Afr` | about 12 GB | -| Asia | `OSXP_Asi` | about 33 GB | -| Australia and the Pacific | `OSXP_AusPac` | about 6 GB | +| Europe | `OrthoStream_Eur` | about 16 GB | +| North America | `OrthoStream_NA` | about 25 GB | +| South America | `OrthoStream_SA` | about 9 GB | +| Africa | `OrthoStream_Afr` | about 12 GB | +| Asia | `OrthoStream_Asi` | about 33 GB | +| Australia and the Pacific | `OrthoStream_AusPac` | about 6 GB | The page shows the exact sizes of the current release. @@ -29,7 +29,7 @@ Leave about 1.7 times the download size free on the drive: the files take more r ## Roads, forests and buildings -A square of a downloaded mesh no longer shows X-Plane's own roads, forests and buildings. **Roads, forests and buildings** downloads them with each region, into one shared package, `OSXP_Overlays`. Turn it off if another package of yours already brings them. The choice applies to the next region you install or update. +A square of a downloaded mesh no longer shows X-Plane's own roads, forests and buildings. **Roads, forests and buildings** downloads them with each region, into one shared package, `OrthoStream_Overlays`. Turn it off if another package of yours already brings them. The choice applies to the next region you install or update. ## Downloading, stopping and resuming diff --git a/src/OrthoStreamXP/Configuration/MountSettings.cs b/src/OrthoStreamXP/Configuration/MountSettings.cs index 8de4249..5bd1f78 100644 --- a/src/OrthoStreamXP/Configuration/MountSettings.cs +++ b/src/OrthoStreamXP/Configuration/MountSettings.cs @@ -82,6 +82,8 @@ public static MountSettings FromValues(SettingValues values, bool console = true var texturePlan = new TexturePlan(levelChange); // One cache folder for every setup: each texture plan keeps its files in its own subfolder. string planCache = Path.Join(cache, texturePlan.FolderName); + // The regions an earlier version installed take their new names before the packages are listed (nothing is mounted yet). + Setup.LegacyPackageNames.Move(values.String("base-packages"), cache, values.String("xplane")); IReadOnlyList packages = PlanPackages(values, planCache, out string? customScenery, out IReadOnlyList notPackages); string sceneryPacksValue = values.String("scenery-packs") ?? "on"; bool updateSceneryPacks = ParseOnOff(sceneryPacksValue) ?? throw new SettingsException($"scenery-packs must be on or off, not '{sceneryPacksValue}'"); diff --git a/src/OrthoStreamXP/Gui/ViewModels/PackagesViewModel.cs b/src/OrthoStreamXP/Gui/ViewModels/PackagesViewModel.cs index f2f0ef3..2fdac3b 100644 --- a/src/OrthoStreamXP/Gui/ViewModels/PackagesViewModel.cs +++ b/src/OrthoStreamXP/Gui/ViewModels/PackagesViewModel.cs @@ -189,6 +189,11 @@ public async Task DeleteAsync(RegionRow row) { await Task.Run(() => Installer(folder).Delete(row.Region.Id)).ConfigureAwait(true); } + catch (InstallException ex) + { + row.Error = ex.Message; + Log.Warning(row.Error); + } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { row.Error = $"The {row.Name} mesh could not be deleted: {ex.Message}"; diff --git a/src/OrthoStreamXP/Program.cs b/src/OrthoStreamXP/Program.cs index 0f32453..780d9b9 100644 --- a/src/OrthoStreamXP/Program.cs +++ b/src/OrthoStreamXP/Program.cs @@ -11,8 +11,10 @@ // What earlier versions kept beside the program, or in the cache, moves to the app's folder before anything reads it. OrthoStreamXP.Setup.LegacyNames.MoveSettings(AppContext.BaseDirectory, OrthoStreamXP.Setup.AppPaths.Settings); -OrthoStreamXP.Setup.LegacyNames.MoveCache(OrthoStreamXP.Setup.SettingsDocument.Load(OrthoStreamXP.Setup.AppPaths.Settings).Get("cache"), - OrthoStreamXP.Setup.AppPaths.Logs, OrthoStreamXP.Setup.AppPaths.History); +OrthoStreamXP.Setup.SettingsDocument settings = OrthoStreamXP.Setup.SettingsDocument.Load(OrthoStreamXP.Setup.AppPaths.Settings); +OrthoStreamXP.Setup.LegacyNames.MoveCache(settings.Get("cache"), OrthoStreamXP.Setup.AppPaths.Logs, OrthoStreamXP.Setup.AppPaths.History); Directory.CreateDirectory(OrthoStreamXP.Setup.AppPaths.History); +// The regions 1.1.1 installed as OSXP_* take their new names before the window lists them. +OrthoStreamXP.Setup.LegacyPackageNames.Move(settings.Get("base-packages"), settings.Get("cache"), settings.Get("xplane")); return GuiHost.Run(); diff --git a/src/OrthoStreamXP/Scenery/FileSystem/SceneryPacks.cs b/src/OrthoStreamXP/Scenery/FileSystem/SceneryPacks.cs index 725ef7b..75808b0 100644 --- a/src/OrthoStreamXP/Scenery/FileSystem/SceneryPacks.cs +++ b/src/OrthoStreamXP/Scenery/FileSystem/SceneryPacks.cs @@ -11,8 +11,8 @@ internal static class SceneryPacks { public const string FileName = "scenery_packs.ini"; - /// The names of the packages OrthoStreamXP downloads itself (OSXP_Eur, OSXP_Overlays...). - public const string DownloadedPrefix = "OSXP_"; + /// The names of the packages OrthoStreamXP downloads itself (OrthoStream_Eur, OrthoStream_Overlays...). + public const string DownloadedPrefix = "OrthoStream_"; private const string Enabled = "SCENERY_PACK ", Disabled = "SCENERY_PACK_DISABLED "; private static readonly string[] NewFileHeader = ["I", "1000 Version", "SCENERY", ""]; @@ -58,6 +58,46 @@ public static bool MoveToEnd(string customScenery, IReadOnlyList package return true; } + /// + /// Renames the entries of packages that took a new name (: old and new) in scenery_packs.ini, + /// in place: their position and whether they are disabled stay. The previous file is kept as scenery_packs.ini.bak. + /// Returns whether the file changed. + /// + public static bool RenameEntries(string customScenery, IReadOnlyList<(string Old, string New)> names) + { + string file = Path.Join(customScenery, FileName); + if (!File.Exists(file)) return false; + string text = File.ReadAllText(file); + string[] lines = text.ReplaceLineEndings("\n").TrimEnd('\n').Split('\n'); + List updated = Rename(lines, names); + if (lines.SequenceEqual(updated)) return false; + string newline = text.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n"; + File.Copy(file, file + ".bak", overwrite: true); + AtomicFile.Write(file, Encoding.UTF8.GetBytes(string.Join(newline, updated) + newline)); + return true; + } + + /// The lines with each old name's entry naming the new one instead; one already listed under its new name keeps only that entry. + internal static List Rename(IReadOnlyList lines, IReadOnlyList<(string Old, string New)> names) + { + string[] olds = [.. names.Select(n => n.Old)], news = [.. names.Select(n => n.New)]; + var listed = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string line in lines) + if (PackageOf(line, news, out _) is { } name) listed.Add(name); + var updated = new List(lines.Count); + foreach (string line in lines) + { + if (PackageOf(line, olds, out bool isDisabled) is not { } old) + { + updated.Add(line); + continue; + } + string renamed = names.First(n => n.Old.Equals(old, StringComparison.OrdinalIgnoreCase)).New; + if (listed.Add(renamed)) updated.Add($"{(isDisabled ? Disabled : Enabled)}Custom Scenery/{renamed}/"); + } + return updated; + } + /// /// The lines without the packages' entries, then one entry per package at the end. Overlay packages /// (such as XPME_Overlays) come first among them, so their overlays draw on top of the other base packages; diff --git a/src/OrthoStreamXP/Setup/Packages/LegacyPackageNames.cs b/src/OrthoStreamXP/Setup/Packages/LegacyPackageNames.cs new file mode 100644 index 0000000..373246a --- /dev/null +++ b/src/OrthoStreamXP/Setup/Packages/LegacyPackageNames.cs @@ -0,0 +1,91 @@ +using OrthoStreamXP.Scenery; + +namespace OrthoStreamXP.Setup; + +/// +/// OrthoStreamXP 1.1.1 installed its regions as OSXP_Eur, OSXP_Overlays..., names OrthoStudio XP's own packages +/// (z_osxp_eur, y_osxp_overlays) are easily mistaken for. They take the names: +/// +/// the packages, in the packages folder, with the hidden download folder of an install that was stopped; +/// their textures in the cache (each setup's folder holds one folder per package); +/// their entries in X-Plane's scenery_packs.ini, in place, so their order and whether they are disabled stay. +/// +/// Only folders OrthoStreamXP installed are renamed (they hold its marker or its overlays record), nothing is renamed over a +/// folder that already exists, and what is in use is renamed at the next start: every step can run again. +/// +internal static class LegacyPackageNames +{ + public const string OldPrefix = "OSXP_"; + private const string OldWorkFolderName = ".osxp-download"; + + /// Each downloaded package's old name and its new one: the regions' and the overlays'. + public static IReadOnlyList<(string Old, string New)> Names { get; } = + [.. MeshRegion.Known.Select(k => k.Package).Append(MeshRegion.OverlaysPackageName) + .Select(name => (OldPrefix + name[SceneryPacks.DownloadedPrefix.Length..], name))]; + + /// Renames the packages in , their textures in and their entries in the scenery_packs.ini of the X-Plane in . + public static void Move(string? packagesFolder, string? cache, string? xplane) + { + if (string.IsNullOrWhiteSpace(packagesFolder) || !Directory.Exists(packagesFolder)) return; + MovePackages(packagesFolder); + (string Old, string New)[] moved = [.. Names.Where(n => !Directory.Exists(Path.Join(packagesFolder, n.Old)) + && Directory.Exists(Path.Join(packagesFolder, n.New)))]; + if (moved.Length == 0) return; + + if (!string.IsNullOrWhiteSpace(cache) && Directory.Exists(cache)) + foreach (string setup in Directory.EnumerateDirectories(cache).Where(f => CacheInventory.ParseSetupName(Path.GetFileName(f)) is not null)) + foreach (var (old, renamed) in moved) + TryMove(Path.Join(setup, old), Path.Join(setup, renamed)); + + string? customScenery = string.IsNullOrWhiteSpace(xplane) ? null : Path.Join(xplane, "Custom Scenery"); + if (customScenery is null || !Directory.Exists(customScenery)) return; + try + { + SceneryPacks.RenameEntries(customScenery, moved); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // In use (X-Plane writing it): renamed at the next start. + } + } + + /// Renames the packages OrthoStreamXP installed in , and its download folder; returns the old names left (in use, or a folder of the new name already there). + public static IReadOnlyList MovePackages(string packagesFolder) + { + var left = new List(); + foreach (var (old, renamed) in Names) + { + string from = Path.Join(packagesFolder, old); + if (IsInstalledByUs(from) && !TryMove(from, Path.Join(packagesFolder, renamed))) left.Add(old); + } + string work = Path.Join(packagesFolder, OldWorkFolderName); + if (!TryMove(work, Path.Join(packagesFolder, RegionInstaller.WorkFolderName))) left.Add(OldWorkFolderName); + return left; + } + + /// The folder of under its old name, when it is still there. + public static string? OldFolderOf(string packagesFolder, string package) + { + string folder = Path.Join(packagesFolder, OldPrefix + package[SceneryPacks.DownloadedPrefix.Length..]); + return IsInstalledByUs(folder) ? folder : null; + } + + private static bool IsInstalledByUs(string folder) => + File.Exists(Path.Join(folder, RegionInstaller.MarkerName)) || File.Exists(Path.Join(folder, OverlayPackage.RecordName)); + + /// Whether is gone: moved now, or there was none. + private static bool TryMove(string from, string to) + { + if (!Directory.Exists(from)) return true; + if (Directory.Exists(to)) return false; + try + { + Directory.Move(from, to); + return true; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return false; // in use: renamed at the next start + } + } +} diff --git a/src/OrthoStreamXP/Setup/Packages/MeshRegion.cs b/src/OrthoStreamXP/Setup/Packages/MeshRegion.cs index 98234c7..5fe423e 100644 --- a/src/OrthoStreamXP/Setup/Packages/MeshRegion.cs +++ b/src/OrthoStreamXP/Setup/Packages/MeshRegion.cs @@ -19,12 +19,12 @@ internal sealed record MeshRegion(string Id, string Release, IReadOnlyListThe regions in the order they are listed, with their names and their packages' names. public static readonly IReadOnlyList<(string Id, string Name, string Package)> Known = [ - ("eur", "Europe", "OSXP_Eur"), - ("na", "North America", "OSXP_NA"), - ("sa", "South America", "OSXP_SA"), - ("afr", "Africa", "OSXP_Afr"), - ("asi", "Asia", "OSXP_Asi"), - ("aus_pac", "Australia and the Pacific", "OSXP_AusPac"), + ("eur", "Europe", SceneryPacks.DownloadedPrefix + "Eur"), + ("na", "North America", SceneryPacks.DownloadedPrefix + "NA"), + ("sa", "South America", SceneryPacks.DownloadedPrefix + "SA"), + ("afr", "Africa", SceneryPacks.DownloadedPrefix + "Afr"), + ("asi", "Asia", SceneryPacks.DownloadedPrefix + "Asi"), + ("aus_pac", "Australia and the Pacific", SceneryPacks.DownloadedPrefix + "AusPac"), ]; public static bool IsKnown(string id) => Known.Any(k => k.Id == id); diff --git a/src/OrthoStreamXP/Setup/Packages/OverlayPackage.cs b/src/OrthoStreamXP/Setup/Packages/OverlayPackage.cs index 297492d..16141d0 100644 --- a/src/OrthoStreamXP/Setup/Packages/OverlayPackage.cs +++ b/src/OrthoStreamXP/Setup/Packages/OverlayPackage.cs @@ -9,7 +9,7 @@ namespace OrthoStreamXP.Setup; /// internal sealed class OverlayPackage(string packagesFolder) { - private const string RecordName = ".orthostreamxp-overlays.json"; + public const string RecordName = ".orthostreamxp-overlays.json"; public string Folder { get; } = Path.Join(packagesFolder, MeshRegion.OverlaysPackageName); diff --git a/src/OrthoStreamXP/Setup/Packages/RegionInstaller.cs b/src/OrthoStreamXP/Setup/Packages/RegionInstaller.cs index 0814355..565073c 100644 --- a/src/OrthoStreamXP/Setup/Packages/RegionInstaller.cs +++ b/src/OrthoStreamXP/Setup/Packages/RegionInstaller.cs @@ -1,5 +1,6 @@ using System.Runtime.ExceptionServices; using System.Text.Json; +using OrthoStreamXP.Scenery; namespace OrthoStreamXP.Setup; @@ -16,7 +17,7 @@ internal sealed class InstallException(string message, Exception? inner = null) /// Installs, updates and deletes the regions of AutoOrtho's mesh in the scenery packages folder, each as a package of its /// own () beside the person's other packages, its overlays in the shared overlays package. /// -/// Everything is prepared in the hidden .osxp-download folder there, which is no package (it has no Earth nav data): a +/// Everything is prepared in the hidden .orthostream-download folder there, which is no package (it has no Earth nav data): a /// zip is unpacked as soon as it is whole, then deleted, and a record of the zips unpacked lets a stopped install go on /// where it was. The region takes its place only once all of it is there, with its marker written last, so X-Plane never /// sees half a region. An update is prepared the same way and swapped in at the end. @@ -25,7 +26,7 @@ internal sealed class InstallException(string message, Exception? inner = null) internal sealed class RegionInstaller(AssetDownload download, string packagesFolder) { public const string MarkerName = ".orthostreamxp-region.json"; - public const string WorkFolderName = ".osxp-download"; + public const string WorkFolderName = ".orthostream-download"; /// Unpacked, a region takes about this much more than its zips (DSFs grow, each small .ter takes a whole disk block). private const double UnpackedRatio = 1.7; private const long SpareBytes = 2L << 30; @@ -44,7 +45,9 @@ public IReadOnlyDictionary Installed() var found = new Dictionary(); foreach (var (id, _, package) in MeshRegion.Known) { - string marker = Path.Join(PackagesFolder, package, MarkerName); + // Still under its old name when it was in use at the start: renamed before anything is installed or deleted. + string folder = LegacyPackageNames.OldFolderOf(PackagesFolder, package) ?? Path.Join(PackagesFolder, package); + string marker = Path.Join(folder, MarkerName); try { if (File.Exists(marker) && JsonSerializer.Deserialize(File.ReadAllText(marker)) is { } region) @@ -75,6 +78,7 @@ public long AlreadyDone(MeshRegion region, bool withOverlays) public async Task InstallAsync(MeshRegion region, bool withOverlays, IProgress progress, CancellationToken cancel) { CheckRegion(region); + MoveOldNames(); string work = WorkOf(region); PrepareWork(region, work); State state = LoadState(work); @@ -196,8 +200,10 @@ private static Task> UnpackPartsAsync(IReadOnlyListDeletes an installed region and its overlays. + /// A package still under its old name could not be renamed. public void Delete(string regionId) { + MoveOldNames(); string package = Path.Join(PackagesFolder, MeshRegion.PackageOf(regionId)); // The marker first: a region half deleted is no longer taken for installed. string marker = Path.Join(package, MarkerName); @@ -234,6 +240,18 @@ private void PutInPlace(MeshRegion region, string work, IReadOnlyList? o DeleteIfEmpty(WorkRoot); } + /// + /// Renames what an earlier version installed () first: installed or deleted under the new + /// name alone, a region would be in the packages folder twice, or not deleted. + /// + private void MoveOldNames() + { + IReadOnlyList left = LegacyPackageNames.MovePackages(PackagesFolder); + if (left.Count == 0) return; + throw new InstallException($"{string.Join(", ", left)} in {PackagesFolder} could not take its new name ({SceneryPacks.DownloadedPrefix}...). " + + "Close what uses it, such as File Explorer, or remove the folder of the new name if one is there, and try again."); + } + private static void DeleteIfEmpty(string folder) { if (Directory.Exists(folder) && !Directory.EnumerateFileSystemEntries(folder).Any()) Directory.Delete(folder); diff --git a/tests/OrthoStreamXP.Tests/LegacyPackageNamesTests.cs b/tests/OrthoStreamXP.Tests/LegacyPackageNamesTests.cs new file mode 100644 index 0000000..0253039 --- /dev/null +++ b/tests/OrthoStreamXP.Tests/LegacyPackageNamesTests.cs @@ -0,0 +1,126 @@ +using OrthoStreamXP.Scenery; +using OrthoStreamXP.Setup; + +namespace OrthoStreamXP.Tests; + +/// The regions 1.1.1 installed as OSXP_* take the OrthoStream_* names, with their textures and scenery_packs.ini entries. +public sealed class LegacyPackageNamesTests : IDisposable +{ + private readonly TempTree _tree = new(); + + public void Dispose() => _tree.Dispose(); + + private string Packages => _tree.PathOf("packages"); + + private string Cache => _tree.PathOf("cache"); + + private string XPlane => _tree.PathOf("X-Plane 12"); + + private string SceneryPacksFile => _tree.PathOf("X-Plane 12/Custom Scenery/scenery_packs.ini"); + + private void InstallOld(string package) + { + _tree.WriteText($"packages/{package}/Earth nav data/+40+000/+45+005.dsf", "dsf"); + _tree.WriteText($"packages/{package}/{RegionInstaller.MarkerName}", """{"Id":"eur","Release":"1","Overlays":true,"InstalledUtc":"2026-11-10T00:00:00Z"}"""); + } + + [Fact] + public void The_regions_their_textures_and_their_scenery_packs_entries_take_the_new_names() + { + InstallOld("OSXP_Eur"); + _tree.WriteText("packages/OSXP_Overlays/Earth nav data/+40+000/+45+005.dsf", "overlay"); + _tree.WriteText($"packages/OSXP_Overlays/{OverlayPackage.RecordName}", "{}"); + _tree.WriteText("packages/.osxp-download/afr/1/state.json", "{}"); + _tree.WriteText("cache/Ultra_4096-4096/OSXP_Eur/textures/x.dds", "dds"); + _tree.WriteText("cache/Sharp_2048-2048/OSXP_Eur/textures/y.dds", "dds"); + _tree.WriteText("X-Plane 12/Custom Scenery/scenery_packs.ini", string.Join("\r\n", + "I", "1000 Version", "SCENERY", + "SCENERY_PACK Custom Scenery/KSEA Demo Area/", + "SCENERY_PACK_DISABLED Custom Scenery/OSXP_Overlays/", + "SCENERY_PACK Custom Scenery/z_osxp_eur/", + "SCENERY_PACK Custom Scenery/OSXP_Eur/") + "\r\n"); + + LegacyPackageNames.Move(Packages, Cache, XPlane); + + Assert.Equal(["OrthoStream_Eur", "OrthoStream_Overlays"], SceneryPacks.FindPackages(Packages, out _).Select(Path.GetFileName)); + Assert.True(File.Exists(_tree.PathOf($"packages/OrthoStream_Overlays/{OverlayPackage.RecordName}"))); + Assert.True(File.Exists(_tree.PathOf("packages/.orthostream-download/afr/1/state.json"))); + Assert.False(Directory.Exists(_tree.PathOf("packages/.osxp-download"))); + Assert.True(File.Exists(_tree.PathOf("cache/Ultra_4096-4096/OrthoStream_Eur/textures/x.dds"))); + Assert.True(File.Exists(_tree.PathOf("cache/Sharp_2048-2048/OrthoStream_Eur/textures/y.dds"))); + Assert.Equal(string.Join("\r\n", + "I", "1000 Version", "SCENERY", + "SCENERY_PACK Custom Scenery/KSEA Demo Area/", + "SCENERY_PACK_DISABLED Custom Scenery/OrthoStream_Overlays/", // still disabled, still in its place + "SCENERY_PACK Custom Scenery/z_osxp_eur/", // OrthoStudio XP's own: untouched + "SCENERY_PACK Custom Scenery/OrthoStream_Eur/") + "\r\n", File.ReadAllText(SceneryPacksFile)); + Assert.True(File.Exists(SceneryPacksFile + ".bak")); + using var http = new HttpClient(); + Assert.Equal("1", new RegionInstaller(new AssetDownload(http), Packages).Installed()["eur"].Release); + } + + [Fact] + public void Running_again_changes_nothing() + { + InstallOld("OSXP_Eur"); + _tree.WriteText("X-Plane 12/Custom Scenery/scenery_packs.ini", "I\n1000 Version\nSCENERY\nSCENERY_PACK Custom Scenery/OSXP_Eur/\n"); + LegacyPackageNames.Move(Packages, Cache, XPlane); + File.Delete(SceneryPacksFile + ".bak"); + + LegacyPackageNames.Move(Packages, Cache, XPlane); + + Assert.Equal("I\n1000 Version\nSCENERY\nSCENERY_PACK Custom Scenery/OrthoStream_Eur/\n", File.ReadAllText(SceneryPacksFile)); + Assert.False(File.Exists(SceneryPacksFile + ".bak")); + } + + [Fact] + public void A_folder_OrthoStreamXP_did_not_install_keeps_its_name() + { + _tree.WriteText("packages/OSXP_Eur/Earth nav data/+40+000/+45+005.dsf", "someone else's"); + _tree.WriteText("X-Plane 12/Custom Scenery/scenery_packs.ini", "I\n1000 Version\nSCENERY\nSCENERY_PACK Custom Scenery/OSXP_Eur/\n"); + + LegacyPackageNames.Move(Packages, Cache, XPlane); + + Assert.True(Directory.Exists(_tree.PathOf("packages/OSXP_Eur"))); + Assert.False(Directory.Exists(_tree.PathOf("packages/OrthoStream_Eur"))); + Assert.Contains("Custom Scenery/OSXP_Eur/", File.ReadAllText(SceneryPacksFile), StringComparison.Ordinal); + } + + [Fact] + public void Nothing_is_renamed_over_a_folder_of_the_new_name() + { + InstallOld("OSXP_Eur"); + _tree.WriteText("packages/OrthoStream_Eur/Earth nav data/+50+010/+51+011.dsf", "new"); + + IReadOnlyList left = LegacyPackageNames.MovePackages(Packages); + + Assert.Equal(["OSXP_Eur"], left); + Assert.True(File.Exists(_tree.PathOf("packages/OSXP_Eur/Earth nav data/+40+000/+45+005.dsf"))); + Assert.Equal("new", File.ReadAllText(_tree.PathOf("packages/OrthoStream_Eur/Earth nav data/+50+010/+51+011.dsf"))); + } + + [Fact] + public void A_region_left_under_its_old_name_shows_as_installed_but_is_not_installed_or_deleted_beside_it() + { + InstallOld("OSXP_Eur"); + _tree.WriteText("packages/OrthoStream_Eur/stray.txt", "in the way"); + using var http = new HttpClient(); + var installer = new RegionInstaller(new AssetDownload(http), Packages); + + Assert.Equal("1", installer.Installed()["eur"].Release); // not offered for download again + + InstallException error = Assert.Throws(() => installer.Delete("eur")); + Assert.Contains("OSXP_Eur", error.Message, StringComparison.Ordinal); + Assert.True(Directory.Exists(_tree.PathOf("packages/OSXP_Eur"))); + } + + [Fact] + public void An_entry_already_listed_under_its_new_name_is_not_doubled() + { + List lines = SceneryPacks.Rename( + ["SCENERY_PACK Custom Scenery/OrthoStream_Eur/", "SCENERY_PACK_DISABLED Custom Scenery/OSXP_Eur/"], + [("OSXP_Eur", "OrthoStream_Eur")]); + + Assert.Equal(["SCENERY_PACK Custom Scenery/OrthoStream_Eur/"], lines); + } +} diff --git a/tests/OrthoStreamXP.Tests/MeshPackagesTests.cs b/tests/OrthoStreamXP.Tests/MeshPackagesTests.cs index fbb1dbf..a6c2a6a 100644 --- a/tests/OrthoStreamXP.Tests/MeshPackagesTests.cs +++ b/tests/OrthoStreamXP.Tests/MeshPackagesTests.cs @@ -66,7 +66,7 @@ public void The_catalog_takes_the_newest_full_release_of_each_region() Assert.Equal("0.0.51", eur.Release); Assert.Equal(["z_eur_00.zip"], eur.Mesh.Select(a => a.Name)); Assert.Equal(["y_eur_overlays.zip.00", "y_eur_overlays.zip.01", "y_eur_overlays.zip.02"], eur.Overlays.Select(a => a.Name)); - Assert.Equal("OSXP_Eur", eur.PackageName); + Assert.Equal("OrthoStream_Eur", eur.PackageName); MeshRegion na = regions[1]; Assert.Equal(["z_na_00.zip", "z_na_01.zip"], na.Mesh.Select(a => a.Name)); Assert.Equal("u/z_na_00.zip.sha256", na.Mesh[0].Sha256Url); @@ -86,15 +86,15 @@ public async Task A_region_becomes_one_package_and_its_overlays_go_to_the_shared await Installer().InstallAsync(eur, withOverlays: true, NoProgress, CancellationToken.None); - Assert.Equal("dsf A", File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "Earth nav data", "+40+000", "+45+005.dsf"))); - Assert.Equal("dsf B", File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "Earth nav data", "+50+010", "+51+011.dsf"))); - Assert.Equal("ter A", File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "terrain", "1_2_BI16.ter"))); - Assert.Equal("overlay A", File.ReadAllText(Path.Join(Packages, "OSXP_Overlays", "Earth nav data", "+40+000", "+45+005.dsf"))); + Assert.Equal("dsf A", File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "Earth nav data", "+40+000", "+45+005.dsf"))); + Assert.Equal("dsf B", File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "Earth nav data", "+50+010", "+51+011.dsf"))); + Assert.Equal("ter A", File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "terrain", "1_2_BI16.ter"))); + Assert.Equal("overlay A", File.ReadAllText(Path.Join(Packages, "OrthoStream_Overlays", "Earth nav data", "+40+000", "+45+005.dsf"))); Assert.False(Directory.Exists(Path.Join(Packages, RegionInstaller.WorkFolderName))); InstalledRegion installed = Assert.Single(Installer().Installed()).Value; Assert.Equal(("eur", "0.0.51", true), (installed.Id, installed.Release, installed.Overlays)); // Both are packages the mount picks up, the work folder never is. - Assert.Equal(["OSXP_Eur", "OSXP_Overlays"], SceneryPacks.FindPackages(Packages, out _).Select(Path.GetFileName)); + Assert.Equal(["OrthoStream_Eur", "OrthoStream_Overlays"], SceneryPacks.FindPackages(Packages, out _).Select(Path.GetFileName)); } [Fact] @@ -105,7 +105,7 @@ public async Task Without_overlays_only_the_mesh_is_installed() await Installer().InstallAsync(eur, withOverlays: false, NoProgress, CancellationToken.None); - Assert.False(Directory.Exists(Path.Join(Packages, "OSXP_Overlays"))); + Assert.False(Directory.Exists(Path.Join(Packages, "OrthoStream_Overlays"))); Assert.False(Installer().Installed()["eur"].Overlays); Assert.DoesNotContain(_server.Requests, r => r.Url.Contains("overlays", StringComparison.Ordinal)); } @@ -123,7 +123,7 @@ public async Task A_partial_download_is_resumed_from_where_it_stopped() await Installer().InstallAsync(eur, withOverlays: true, NoProgress, CancellationToken.None); Assert.Equal(1000, Assert.Single(_server.Requests, r => r.Url.EndsWith("z_eur_00.zip", StringComparison.Ordinal)).From); - Assert.Equal(content, File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "Earth nav data", "+40+000", "+45+005.dsf"))); + Assert.Equal(content, File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "Earth nav data", "+40+000", "+45+005.dsf"))); } [Fact] @@ -144,7 +144,7 @@ public async Task A_stopped_install_goes_on_with_the_zips_it_has_not_unpacked() Assert.Single(_server.Requests, r => r.Url.EndsWith("z_eur_00.zip", StringComparison.Ordinal)); Assert.Single(_server.Requests, r => r.Url.EndsWith("z_eur_01.zip", StringComparison.Ordinal)); - Assert.Equal("B", File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "Earth nav data", "+50+010", "+51+011.dsf"))); + Assert.Equal("B", File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "Earth nav data", "+50+010", "+51+011.dsf"))); } [Fact] @@ -166,8 +166,8 @@ public async Task The_next_zip_downloads_while_the_one_before_is_unpacked() await Installer().InstallAsync(eur, withOverlays: true, NoProgress, CancellationToken.None); Assert.False(firstRecordedWhenSecondAsked); // asked before the first zip was unpacked and recorded - Assert.Equal(500, Directory.GetFiles(Path.Join(Packages, "OSXP_Eur", "terrain")).Length); - Assert.Equal("B", File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "Earth nav data", "+50+010", "+51+011.dsf"))); + Assert.Equal(500, Directory.GetFiles(Path.Join(Packages, "OrthoStream_Eur", "terrain")).Length); + Assert.Equal("B", File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "Earth nav data", "+50+010", "+51+011.dsf"))); } [Fact] @@ -200,7 +200,7 @@ public async Task A_zip_that_does_not_match_its_published_sha256_is_refused_and_ Assert.Contains("SHA-256", error.Message, StringComparison.Ordinal); Assert.False(File.Exists(Path.Join(Packages, RegionInstaller.WorkFolderName, "na", "1", "zips", "z_na_00.zip"))); - Assert.False(Directory.Exists(Path.Join(Packages, "OSXP_NA"))); + Assert.False(Directory.Exists(Path.Join(Packages, "OrthoStream_NA"))); } [Fact] @@ -215,7 +215,7 @@ public async Task A_matching_sha256_is_accepted() await Installer().InstallAsync(na, true, NoProgress, CancellationToken.None); - Assert.True(File.Exists(Path.Join(Packages, "OSXP_NA", "Earth nav data", "+40-080", "+45-075.dsf"))); + Assert.True(File.Exists(Path.Join(Packages, "OrthoStream_NA", "Earth nav data", "+40-080", "+45-075.dsf"))); } [Fact] @@ -275,9 +275,9 @@ await Installer().InstallAsync(Region("eur", "1", [Zip(("z_eur_00/Earth nav data await Installer().InstallAsync(Region("eur", "2", [Zip(("z_eur_00/Earth nav data/+40+000/+45+005.dsf", "new"))], Zip(("y/Earth nav data/+40+000/+45+005.dsf", "new overlay"))), true, NoProgress, CancellationToken.None); - Assert.Equal("new", File.ReadAllText(Path.Join(Packages, "OSXP_Eur", "Earth nav data", "+40+000", "+45+005.dsf"))); - Assert.False(File.Exists(Path.Join(Packages, "OSXP_Eur", "terrain", "gone.ter"))); - Assert.Equal("new overlay", File.ReadAllText(Path.Join(Packages, "OSXP_Overlays", "Earth nav data", "+40+000", "+45+005.dsf"))); + Assert.Equal("new", File.ReadAllText(Path.Join(Packages, "OrthoStream_Eur", "Earth nav data", "+40+000", "+45+005.dsf"))); + Assert.False(File.Exists(Path.Join(Packages, "OrthoStream_Eur", "terrain", "gone.ter"))); + Assert.Equal("new overlay", File.ReadAllText(Path.Join(Packages, "OrthoStream_Overlays", "Earth nav data", "+40+000", "+45+005.dsf"))); Assert.Equal("2", Installer().Installed()["eur"].Release); } @@ -290,12 +290,12 @@ await Installer().InstallAsync(Region("eur", "1", [Zip(("z_eur_00/Earth nav data await Installer().InstallAsync(Region("afr", "1", [Zip(("z_afr_00/Earth nav data/+30+000/+31+001.dsf", "afr"))], Zip(("y/Earth nav data/+30+000/+35+005.dsf", "border, Africa's"), ("y/Earth nav data/+00+010/+05+015.dsf", "Africa only"))), true, NoProgress, CancellationToken.None); - string overlays = Path.Join(Packages, "OSXP_Overlays", "Earth nav data"); + string overlays = Path.Join(Packages, "OrthoStream_Overlays", "Earth nav data"); Assert.Equal("border, Europe's", File.ReadAllText(Path.Join(overlays, "+30+000", "+35+005.dsf"))); // placed first, kept Installer().Delete("afr"); - Assert.False(Directory.Exists(Path.Join(Packages, "OSXP_Afr"))); + Assert.False(Directory.Exists(Path.Join(Packages, "OrthoStream_Afr"))); Assert.False(File.Exists(Path.Join(overlays, "+00+010", "+05+015.dsf"))); Assert.False(Directory.Exists(Path.Join(overlays, "+00+010"))); Assert.Equal("border, Europe's", File.ReadAllText(Path.Join(overlays, "+30+000", "+35+005.dsf"))); @@ -303,7 +303,7 @@ await Installer().InstallAsync(Region("afr", "1", [Zip(("z_afr_00/Earth nav data Installer().Delete("eur"); - Assert.False(Directory.Exists(Path.Join(Packages, "OSXP_Overlays"))); + Assert.False(Directory.Exists(Path.Join(Packages, "OrthoStream_Overlays"))); Assert.Empty(Installer().Installed()); } @@ -343,15 +343,15 @@ public async Task While_the_setup_installs_a_region_the_Packages_page_starts_no_ public void The_downloaded_meshes_are_listed_below_the_persons_own_packages() { List lines = SceneryPacks.Reorder(["I", "1000 Version", "SCENERY", ""], - ["OSXP_Eur", "OSXP_Overlays", "XPME_Europe", "yOrtho4XP_Overlays", "zOrtho4XP_+47+008"]); + ["OrthoStream_Eur", "OrthoStream_Overlays", "XPME_Europe", "yOrtho4XP_Overlays", "zOrtho4XP_+47+008"]); Assert.Equal([ "I", "1000 Version", "SCENERY", - "SCENERY_PACK Custom Scenery/OSXP_Overlays/", + "SCENERY_PACK Custom Scenery/OrthoStream_Overlays/", "SCENERY_PACK Custom Scenery/yOrtho4XP_Overlays/", "SCENERY_PACK Custom Scenery/XPME_Europe/", "SCENERY_PACK Custom Scenery/zOrtho4XP_+47+008/", - "SCENERY_PACK Custom Scenery/OSXP_Eur/", + "SCENERY_PACK Custom Scenery/OrthoStream_Eur/", ], lines); }