fix(art): re-seed a bundled background when the stored bytes differ

The creator's profile still showed the previous Starting art after the replacement, with
`Art.DefaultVersion = 4` recorded — so the version gate had already declared itself done. It
compared a constant against the embedded resource, and those two were compiled at different
moments (version 4 landed with the OLD image embedded; the corrected image reached the assembly
one build later). A version bump can always be raced that way.

The seed now compares BYTES: `LayoutStore.AssetMatches` hashes the bundled resource and checks the
row the background already points at. Same bytes → leave it untouched (so this is idempotent and
never churns the database). Different bytes → re-stamp. The version constant and its setting are
gone, because a heal that only runs once can silently skip the one launch it was needed for.

`SceneThumbnailTests` planted its own art on a background still flagged as default art — in the
real app that state only ever means stale bundled art, because Browse… sets the path and clears
the flag. The fixture now models a creator's own picture.

Tests: 572 green, 0 warnings. Verified the profile's Starting asset hash (167CDD3B…, the
two-category render) differs from the shipped `starting-background.jpg` (5FC6DD22…), so the next
launch re-seeds it.
This commit is contained in:
2026-11-06 12:21:24 -08:00
parent 3db964e5cf
commit 70e2c48136
5 changed files with 36 additions and 43 deletions
+7 -30
View File
@@ -427,37 +427,9 @@ public partial class MainViewModel
/// <summary>Heals every scene to the locked Background model after a layout
/// load: keeps the correct flavor per scene at index 0 named "Background",
/// drops duplicates/stale rows, re-seeds missing ones.</summary>
/// <summary>Bump when a bundled background image is replaced. Layouts stamped with an older version
/// re-seed their DEFAULT art on next load; a creator's own custom background is never touched.</summary>
private const int CurrentDefaultArtVersion = 4;
/// <summary>Re-stamp the shipped art for scenes still using the default. Without this, a replaced
/// <c>starting-background.jpg</c> reaches new installs only: the asset is hash-keyed into the layout
/// database on first seed, so an existing profile keeps showing the old image forever.</summary>
private void HealDefaultArtVersion()
{
if (_layoutStore.LoadDefaultArtVersion() >= CurrentDefaultArtVersion) return;
// Every scene's background, not AllBackgrounds(): that helper only returns the LIVE scene's
// capture-flavoured rows, which is why version 2 healed nothing (2026-11-06).
var cleared = 0;
foreach (var scene in Scenes)
foreach (var background in scene.Elements.OfType<Source>().Where(element => element.IsBackground))
{
if (!background.UseDefaultBackground && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath))
continue; // the creator chose their own art
background.AssetId = null; // the seed below re-stamps the bundled art
cleared++;
}
AppLog.Write($"Default art version {CurrentDefaultArtVersion}: re-seeding {cleared} background(s)");
_layoutStore.SaveDefaultArtVersion(CurrentDefaultArtVersion);
}
private void HealBackgrounds()
{
NormalizeBackgrounds(Scenes);
HealDefaultArtVersion();
foreach (var scene in Scenes)
{
_sceneGraph.InvalidateBake(scene);
@@ -516,7 +488,6 @@ public partial class MainViewModel
/// carries none. A custom image (Browse…) always wins and is never overwritten.</summary>
private void StampDefaultBackgroundAsset(Source background, string sceneName)
{
if (!string.IsNullOrWhiteSpace(background.AssetId)) return;
if (!background.UseDefaultBackground && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath)) return;
var asset = DefaultBackgroundAsset(sceneName);
if (asset == null) return;
@@ -531,7 +502,13 @@ public partial class MainViewModel
if (stream == null) return;
using var ms = new MemoryStream();
stream.CopyTo(ms);
background.AssetId = AddAsset(ms.ToArray());
var bytes = ms.ToArray();
// Already the shipped art → leave it. Compared by BYTES, not by a version stamp: assets are
// hash-keyed on first seed, so a replaced bundled image never reaches an existing profile on
// its own, and the version constant proved just as stale (2026-11-06).
if (_layoutStore.AssetMatches(background.AssetId!, bytes)) return;
background.AssetId = AddAsset(bytes);
}
catch { /* non-critical — the app works without default art */ }
}