Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
14 changes: 7 additions & 7 deletions docs/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/OrthoStreamXP/Configuration/MountSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PackagePlan> packages = PlanPackages(values, planCache, out string? customScenery, out IReadOnlyList<string> notPackages);
string sceneryPacksValue = values.String("scenery-packs") ?? "on";
bool updateSceneryPacks = ParseOnOff(sceneryPacksValue) ?? throw new SettingsException($"scenery-packs must be on or off, not '{sceneryPacksValue}'");
Expand Down
5 changes: 5 additions & 0 deletions src/OrthoStreamXP/Gui/ViewModels/PackagesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down
6 changes: 4 additions & 2 deletions src/OrthoStreamXP/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
44 changes: 42 additions & 2 deletions src/OrthoStreamXP/Scenery/FileSystem/SceneryPacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ internal static class SceneryPacks
{
public const string FileName = "scenery_packs.ini";

/// <summary>The names of the packages OrthoStreamXP downloads itself (OSXP_Eur, OSXP_Overlays...).</summary>
public const string DownloadedPrefix = "OSXP_";
/// <summary>The names of the packages OrthoStreamXP downloads itself (OrthoStream_Eur, OrthoStream_Overlays...).</summary>
public const string DownloadedPrefix = "OrthoStream_";
private const string Enabled = "SCENERY_PACK ", Disabled = "SCENERY_PACK_DISABLED ";
private static readonly string[] NewFileHeader = ["I", "1000 Version", "SCENERY", ""];

Expand Down Expand Up @@ -58,6 +58,46 @@ public static bool MoveToEnd(string customScenery, IReadOnlyList<string> package
return true;
}

/// <summary>
/// Renames the entries of packages that took a new name (<paramref name="names"/>: 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.
/// </summary>
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<string> 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;
}

/// <summary>The lines with each old name's entry naming the new one instead; one already listed under its new name keeps only that entry.</summary>
internal static List<string> Rename(IReadOnlyList<string> lines, IReadOnlyList<(string Old, string New)> names)
{
string[] olds = [.. names.Select(n => n.Old)], news = [.. names.Select(n => n.New)];
var listed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string line in lines)
if (PackageOf(line, news, out _) is { } name) listed.Add(name);
var updated = new List<string>(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;
}

/// <summary>
/// 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;
Expand Down
91 changes: 91 additions & 0 deletions src/OrthoStreamXP/Setup/Packages/LegacyPackageNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using OrthoStreamXP.Scenery;

namespace OrthoStreamXP.Setup;

/// <summary>
/// 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 <see cref="SceneryPacks.DownloadedPrefix"/> names:
/// <list type="bullet">
/// <item>the packages, in the packages folder, with the hidden download folder of an install that was stopped;</item>
/// <item>their textures in the cache (each setup's folder holds one folder per package);</item>
/// <item>their entries in X-Plane's scenery_packs.ini, in place, so their order and whether they are disabled stay.</item>
/// </list>
/// 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.
/// </summary>
internal static class LegacyPackageNames
{
public const string OldPrefix = "OSXP_";
private const string OldWorkFolderName = ".osxp-download";

/// <summary>Each downloaded package's old name and its new one: the regions' and the overlays'.</summary>
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))];

/// <summary>Renames the packages in <paramref name="packagesFolder"/>, their textures in <paramref name="cache"/> and their entries in the scenery_packs.ini of the X-Plane in <paramref name="xplane"/>.</summary>
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.
}
}

/// <summary>Renames the packages OrthoStreamXP installed in <paramref name="packagesFolder"/>, and its download folder; returns the old names left (in use, or a folder of the new name already there).</summary>
public static IReadOnlyList<string> MovePackages(string packagesFolder)
{
var left = new List<string>();
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;
}

/// <summary>The folder of <paramref name="package"/> under its old name, when it is still there.</summary>
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));

/// <summary>Whether <paramref name="from"/> is gone: moved now, or there was none.</summary>
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
}
}
}
12 changes: 6 additions & 6 deletions src/OrthoStreamXP/Setup/Packages/MeshRegion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ internal sealed record MeshRegion(string Id, string Release, IReadOnlyList<MeshA
/// <summary>The regions in the order they are listed, with their names and their packages' names.</summary>
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);
Expand Down
2 changes: 1 addition & 1 deletion src/OrthoStreamXP/Setup/Packages/OverlayPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace OrthoStreamXP.Setup;
/// </summary>
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);

Expand Down
24 changes: 21 additions & 3 deletions src/OrthoStreamXP/Setup/Packages/RegionInstaller.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.ExceptionServices;
using System.Text.Json;
using OrthoStreamXP.Scenery;

namespace OrthoStreamXP.Setup;

Expand All @@ -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 (<see cref="MeshRegion.PackageName"/>) beside the person's other packages, its overlays in the shared overlays package.
/// <para>
/// Everything is prepared in the hidden <c>.osxp-download</c> folder there, which is no package (it has no Earth nav data): a
/// Everything is prepared in the hidden <c>.orthostream-download</c> 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.
Expand All @@ -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";
/// <summary>Unpacked, a region takes about this much more than its zips (DSFs grow, each small .ter takes a whole disk block).</summary>
private const double UnpackedRatio = 1.7;
private const long SpareBytes = 2L << 30;
Expand All @@ -44,7 +45,9 @@ public IReadOnlyDictionary<string, InstalledRegion> Installed()
var found = new Dictionary<string, InstalledRegion>();
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<InstalledRegion>(File.ReadAllText(marker)) is { } region)
Expand Down Expand Up @@ -75,6 +78,7 @@ public long AlreadyDone(MeshRegion region, bool withOverlays)
public async Task InstallAsync(MeshRegion region, bool withOverlays, IProgress<InstallProgress> progress, CancellationToken cancel)
{
CheckRegion(region);
MoveOldNames();
string work = WorkOf(region);
PrepareWork(region, work);
State state = LoadState(work);
Expand Down Expand Up @@ -196,8 +200,10 @@ private static Task<IReadOnlyList<string>> UnpackPartsAsync(IReadOnlyList<string
}

/// <summary>Deletes an installed region and its overlays.</summary>
/// <exception cref="InstallException">A package still under its old name could not be renamed.</exception>
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);
Expand Down Expand Up @@ -234,6 +240,18 @@ private void PutInPlace(MeshRegion region, string work, IReadOnlyList<string>? o
DeleteIfEmpty(WorkRoot);
}

/// <summary>
/// Renames what an earlier version installed (<see cref="LegacyPackageNames"/>) first: installed or deleted under the new
/// name alone, a region would be in the packages folder twice, or not deleted.
/// </summary>
private void MoveOldNames()
{
IReadOnlyList<string> 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);
Expand Down
Loading
Loading