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
27 changes: 17 additions & 10 deletions src/OrthoStreamXP/Scenery/FileSystem/SceneryPacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,42 +47,49 @@ public static bool MoveToEnd(string customScenery, IReadOnlyList<string> package
string file = Path.Join(customScenery, FileName);
string? text = File.Exists(file) ? File.ReadAllText(file) : null;
string[] lines = text is null ? NewFileHeader : text.ReplaceLineEndings("\n").TrimEnd('\n').Split('\n');
List<string> updated = Reorder(lines, packageNames, customScenery);
List<string> updated = Reorder(lines, packageNames);
if (lines.SequenceEqual(updated)) return false;
string newline = text is not null && !text.Contains("\r\n", StringComparison.Ordinal) ? "\n" : "\r\n";
if (text is not null) File.Copy(file, file + ".bak", overwrite: true);
AtomicFile.Write(file, Encoding.UTF8.GetBytes(string.Join(newline, updated) + newline));
return true;
}

/// <summary>The lines without the packages' entries, then one entry per package at the end.</summary>
internal static List<string> Reorder(IReadOnlyList<string> lines, IReadOnlyList<string> packageNames, string customScenery)
/// <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.
/// </summary>
internal static List<string> Reorder(IReadOnlyList<string> lines, IReadOnlyList<string> packageNames)
{
var disabled = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var kept = new List<string>();
foreach (string line in lines)
{
string? package = PackageOf(line, packageNames, customScenery, out bool isDisabled);
string? package = PackageOf(line, packageNames, out bool isDisabled);
if (package is null) kept.Add(line);
else if (isDisabled) disabled.Add(package);
}
while (kept.Count > 0 && kept[^1].Trim().Length == 0) kept.RemoveAt(kept.Count - 1);
foreach (string name in packageNames)
foreach (string name in packageNames.OrderBy(name => IsOverlays(name) ? 0 : 1))
kept.Add($"{(disabled.Contains(name) ? Disabled : Enabled)}Custom Scenery/{name}/");
return kept;
}

/// <summary>Which of the packages a SCENERY_PACK line names (relative to the X-Plane folder, or as a full path).</summary>
private static string? PackageOf(string line, IReadOnlyList<string> packageNames, string customScenery, out bool isDisabled)
/// <summary>
/// Which of the packages a SCENERY_PACK line names, by its folder name wherever the path points: an entry
/// for a package of the same name elsewhere (such as the base packages folder itself) would load it twice.
/// </summary>
private static string? PackageOf(string line, IReadOnlyList<string> packageNames, out bool isDisabled)
{
string trimmed = line.Trim();
isDisabled = trimmed.StartsWith(Disabled, StringComparison.Ordinal);
if (!isDisabled && !trimmed.StartsWith(Enabled, StringComparison.Ordinal)) return null;
string path = Normalize(trimmed[(isDisabled ? Disabled : Enabled).Length..]);
return packageNames.FirstOrDefault(name =>
path.Equals("Custom Scenery/" + name, StringComparison.OrdinalIgnoreCase)
|| path.Equals(Normalize(Path.Join(customScenery, name)), StringComparison.OrdinalIgnoreCase));
string folderName = path[(path.LastIndexOf('/') + 1)..];
return packageNames.FirstOrDefault(name => folderName.Equals(name, StringComparison.OrdinalIgnoreCase));
}

private static bool IsOverlays(string packageName) => packageName.Contains("Overlays", StringComparison.OrdinalIgnoreCase);

private static string Normalize(string path) => path.Trim().Replace('\\', '/').TrimEnd('/');
}
40 changes: 39 additions & 1 deletion tests/OrthoStreamXP.Tests/SceneryPacksTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,48 @@ SCENERY_PACK Custom Scenery/XPME_Asia/
public void An_entry_with_the_full_path_is_recognised()
{
string fullPath = Path.Join(CustomScenery, "XPME_Europe").Replace('\\', '/') + "/";
List<string> lines = SceneryPacks.Reorder(["SCENERY", "", $"SCENERY_PACK {fullPath}", "SCENERY_PACK Custom Scenery/KSEA/"], ["XPME_Europe"], CustomScenery);
List<string> lines = SceneryPacks.Reorder(["SCENERY", "", $"SCENERY_PACK {fullPath}", "SCENERY_PACK Custom Scenery/KSEA/"], ["XPME_Europe"]);
Assert.Equal(["SCENERY", "", "SCENERY_PACK Custom Scenery/KSEA/", "SCENERY_PACK Custom Scenery/XPME_Europe/"], lines);
}

[Fact]
public void An_entry_for_a_package_of_the_same_name_elsewhere_is_replaced()
{
List<string> lines = SceneryPacks.Reorder(
[
"SCENERY", "",
@"SCENERY_PACK C:\OrthoData\XPME\Base Packages\XPME_Africa/",
"SCENERY_PACK Custom Scenery/KSEA/",
@"SCENERY_PACK D:\Other\XPME_Asia",
@"SCENERY_PACK C:\OrthoData\XPME\Base Packages\XPME_Africa_Extra/",
],
["XPME_Africa", "XPME_Asia"]);
Assert.Equal(
[
"SCENERY", "",
"SCENERY_PACK Custom Scenery/KSEA/",
@"SCENERY_PACK C:\OrthoData\XPME\Base Packages\XPME_Africa_Extra/",
"SCENERY_PACK Custom Scenery/XPME_Africa/",
"SCENERY_PACK Custom Scenery/XPME_Asia/",
], lines);
}

[Fact]
public void An_overlays_package_goes_above_the_other_packages()
{
List<string> lines = SceneryPacks.Reorder(
["SCENERY", "", "SCENERY_PACK Custom Scenery/KSEA/", "SCENERY_PACK_DISABLED Custom Scenery/XPME_Overlays/"],
["XPME_Africa", "XPME_Asia", "XPME_Overlays", "XPME_South_America"]);
Assert.Equal(
[
"SCENERY", "", "SCENERY_PACK Custom Scenery/KSEA/",
"SCENERY_PACK_DISABLED Custom Scenery/XPME_Overlays/",
"SCENERY_PACK Custom Scenery/XPME_Africa/",
"SCENERY_PACK Custom Scenery/XPME_Asia/",
"SCENERY_PACK Custom Scenery/XPME_South_America/",
], lines);
}

[Fact]
public void A_missing_file_is_created()
{
Expand Down
Loading