diff --git a/src/OrthoStreamXP/Scenery/FileSystem/SceneryOverlay.cs b/src/OrthoStreamXP/Scenery/FileSystem/SceneryOverlay.cs
index 4a13313..e50ea5a 100644
--- a/src/OrthoStreamXP/Scenery/FileSystem/SceneryOverlay.cs
+++ b/src/OrthoStreamXP/Scenery/FileSystem/SceneryOverlay.cs
@@ -50,8 +50,9 @@ public SceneryOverlay(SceneryRoots roots, TextureScheduler? textures, bool lazyT
///
/// What X-Plane gets for a path. A tile's terrain definition is made in memory. With streaming, a photo texture
- /// is streamed level by level unless it is already complete in the cache or the package has its own copy (both
- /// are served as they are); everything else, and every texture without streaming, is a backing file from
+ /// is streamed level by level unless it is already complete in the cache or the lazy store has a copy (both
+ /// are served as they are; the package's own photo copies are ignored); everything else, and every texture
+ /// without streaming, is a backing file from
/// .
///
public OverlayEntry Open(VirtualPath path)
@@ -188,7 +189,8 @@ private void ReportTextureRequest(VirtualPath path, RequestOutcome outcome, Time
///
/// Merged listing of a directory: source entries, replaced by same-named cache entries. The textures
- /// directory merges the source's textures\ and _textures\; _textures itself is not shown.
+ /// directory merges the source's textures\ and _textures\, without the package's own photo textures;
+ /// _textures itself is not shown.
///
public IReadOnlyList List(VirtualPath directory)
{
@@ -196,17 +198,22 @@ public IReadOnlyList List(VirtualPath directory)
return []; // DSF references terrain definitions by path; X-Plane opens them directly.
var entries = new Dictionary(StringComparer.OrdinalIgnoreCase);
// Lowest priority first, so later folders replace same-named entries.
- foreach (string path in _roots.SourceFolders(directory).Reverse().Append(_roots.CachePath(directory)))
+ foreach (string path in _roots.SourceFolders(directory).Reverse())
+ AddEntries(entries, directory, path, fromSource: true);
+ AddEntries(entries, directory, _roots.CachePath(directory), fromSource: false);
+ return [.. entries.Values.OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase)];
+ }
+
+ private static void AddEntries(Dictionary entries, VirtualPath directory, string path, bool fromSource)
+ {
+ var folder = new DirectoryInfo(path);
+ if (!folder.Exists) return;
+ foreach (FileSystemInfo child in folder.EnumerateFileSystemInfos())
{
- var folder = new DirectoryInfo(path);
- if (!folder.Exists) continue;
- foreach (FileSystemInfo child in folder.EnumerateFileSystemInfos())
- {
- if (directory.IsRoot && HiddenRootEntries.Contains(child.Name, StringComparer.OrdinalIgnoreCase)) continue;
- entries[child.Name] = child;
- }
+ if (directory.IsRoot && HiddenRootEntries.Contains(child.Name, StringComparer.OrdinalIgnoreCase)) continue;
+ if (fromSource && SceneryRoots.IsPackagePhoto(directory.Child(child.Name))) continue;
+ entries[child.Name] = child;
}
- return [.. entries.Values.OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase)];
}
public (int Terrain, int Textures) RequestCounts()
diff --git a/src/OrthoStreamXP/Scenery/FileSystem/SceneryPaths.cs b/src/OrthoStreamXP/Scenery/FileSystem/SceneryPaths.cs
index f5142d9..6331362 100644
--- a/src/OrthoStreamXP/Scenery/FileSystem/SceneryPaths.cs
+++ b/src/OrthoStreamXP/Scenery/FileSystem/SceneryPaths.cs
@@ -100,9 +100,20 @@ public IEnumerable SourceFolders(VirtualPath path) =>
? SourceTextureFolders.Select(folder => Path.Join(Source, folder, string.Join('\\', path.Parts[1..])))
: [Path.Join(Source, string.Join('\\', path.Parts))];
- /// The existing source file or directory behind a virtual path, looking in textures\ then _textures\.
+ ///
+ /// The existing source file or directory behind a virtual path, looking in textures\ then _textures\.
+ /// The package's own photo textures are never found (see ).
+ ///
public string? FindInSource(VirtualPath path) =>
- SourceFolders(path).FirstOrDefault(candidate => File.Exists(candidate) || Directory.Exists(candidate));
+ IsPackagePhoto(path) ? null
+ : SourceFolders(path).FirstOrDefault(candidate => File.Exists(candidate) || Directory.Exists(candidate));
+
+ ///
+ /// Whether a path names a photo texture (textures\21904_35824_BI16.dds). The package's own copies of these
+ /// (made by Ortho4XP or similar) are hidden and never served: photos are always streamed or downloaded,
+ /// while the package's masks (PNGs) and other files pass through as they are.
+ ///
+ public static bool IsPackagePhoto(VirtualPath path) => PhotoTexture(path) is not null;
///
/// Where a virtual path is kept in the cache: a photo texture under its cache name (textures\87520_143440_BI18.dds
@@ -176,9 +187,10 @@ public bool DdsMatchesSize(VirtualPath path, string file)
}
///
- /// A file under textures/ that the package itself provides: from the source's textures\ or _textures\,
- /// or from the lazy texture store (which holds a textures folder's contents). For a DDS this is the
- /// local copy that can be served while, or instead of, a download.
+ /// A file under textures/ that the package itself provides: from the source's textures\ or _textures\
+ /// (never a photo texture, see ), or from the lazy texture store (which holds a
+ /// textures folder's contents). For a store DDS this is the local copy that can be served while, or instead
+ /// of, a download.
///
public string? FindTexture(VirtualPath path)
{
diff --git a/tests/OrthoStreamXP.Tests/SceneryOverlayTests.cs b/tests/OrthoStreamXP.Tests/SceneryOverlayTests.cs
index 0774800..493ba32 100644
--- a/tests/OrthoStreamXP.Tests/SceneryOverlayTests.cs
+++ b/tests/OrthoStreamXP.Tests/SceneryOverlayTests.cs
@@ -65,6 +65,34 @@ public void Textures_merges_the_cache_textures_and_underscore_textures_and_hides
Assert.Equal(["Earth nav data", "terrain", "textures"], overlay.List(VirtualPath.Parse(@"\")).Select(e => e.Name));
}
+ [Theory]
+ [InlineData("textures")]
+ [InlineData("_textures")]
+ public void The_packages_own_photo_textures_are_hidden_and_never_served_but_its_pngs_are(string folder)
+ {
+ _tree.WriteText($"source/{folder}/21904_35824_BI16.dds", "Ortho4XP photo");
+ string mask = _tree.WritePngHeader($"source/{folder}/21904_35824_ZL16.png", 1024);
+ string transition = _tree.WritePngHeader($"source/{folder}/water_transition.png", 64);
+ string other = _tree.WriteText($"source/{folder}/custom.dds", "not a photo texture");
+ SceneryOverlay overlay = Overlay();
+
+ Assert.Equal(["21904_35824_ZL16.png", "custom.dds", "water_transition.png"],
+ overlay.List(VirtualPath.Of("textures")).Select(e => e.Name));
+ var notFound = Assert.Throws(() => overlay.Resolve(@"\textures\21904_35824_BI16.dds"));
+ Assert.Equal(NtStatus.ObjectNameNotFound, notFound.Status);
+ Assert.Equal(mask, overlay.Resolve(@"\textures\21904_35824_ZL16.png"));
+ Assert.Equal(transition, overlay.Resolve(@"\textures\water_transition.png"));
+ Assert.Equal(other, overlay.Resolve(@"\textures\custom.dds"));
+ }
+
+ [Fact]
+ public void A_downloaded_photo_texture_is_served_over_the_packages_own_copy()
+ {
+ _tree.WriteText("source/textures/87520_143440_BI18.dds", "Ortho4XP photo");
+ string cached = _tree.WriteText("cache/textures/10940_17930_PX15.dds", "DDS "); // Overlay() is at level -3
+ Assert.Equal(cached, Overlay().Resolve(@"\textures\87520_143440_BI18.dds"));
+ }
+
[Fact]
public void A_lazy_store_dds_is_served_in_place_without_copying()
{