diff --git a/DorkNet.Server/Data/LegacyUpgrades.cs b/DorkNet.Server/Data/LegacyUpgrades.cs
index 7e3becc..55f0c17 100644
--- a/DorkNet.Server/Data/LegacyUpgrades.cs
+++ b/DorkNet.Server/Data/LegacyUpgrades.cs
@@ -34,12 +34,88 @@ public static class LegacyUpgrades
public static async Task RunAsync(DorkNetDbContext db, IConfiguration config, ILogger logger, CancellationToken ct = default)
{
await RenameBloodMoonToCrescendoAsync(db, logger, ct);
+ await BackfillCanonicalRoomImagesAsync(db, logger, ct);
await CoercePhotonRegionAsync(db, config, logger, ct);
// Future data transforms go here, one per release. Keep them
// narrowly scoped — anything that can be expressed as a
// schema change should be a migration instead.
}
+ /// Update existing Rec Room Original rows to the canonical
+ /// Rec.Net CDN blob names used by Data/room_images.json.
+ ///
+ /// Fresh databases get these names from
+ /// seeding, but older production databases can be stuck on
+ /// image_RecCenter.png / image_{Room}.png aliases. Those
+ /// aliases only work when local bundled image files exist; production
+ /// serves RRO thumbnails from S3 under dorknet-content/image/{blob},
+ /// so the DB row itself must point at the blob filename.
+ ///
+ private static async Task BackfillCanonicalRoomImagesAsync(
+ DorkNetDbContext db, ILogger logger, CancellationToken ct)
+ {
+ var canonical = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["RecCenter"] = "50xrbhogfdae0mcgzfkomlghu.png",
+ ["3DCharades"] = "aj7w8q5nrfyt2ssfoovawtz4l.png",
+ ["TheRiseofJumbotron"] = "8x6qaj4nlbudf1412tupxkq9n.png",
+ ["RecRoyaleSolos"] = "8ners1eg89dy974ixbim40sky.png",
+ ["Dodgeball"] = "086ug40lugly2prjest6kw4dz.png",
+ ["Paintball"] = "d1es44q1u6hlhykxpy8uq0lci.png",
+ ["LaserTag"] = "4xy3n0voagf6t9bbfnxft1txt.png",
+ ["Paddleball"] = "61qgagldqpy5opgpbrbg5wxvo.png",
+ ["DiscGolfPropulsion"] = "6wvnxmrmsw98wsttajp0nreci.png",
+ ["DiscGolfLake"] = "86s8y4ihyhx7n9ac41gwahwze.png",
+ ["RecRoyaleSquads"] = "0c1qdk77fbl8c5slphl3k1t8e.png",
+ ["GoldenTrophy"] = "1g7pq04z9xseircwla8sf9py5.png",
+ ["Crescendo"] = "by3mjs9jbozpdvu6g9aje7jgz.png",
+ ["BowlingAlley"] = "8gwqibu0anm4j0rw54xo2y0yd.png",
+ ["CrimsonCauldron"] = "ae6yh8p6y8p3cuyqkc4l8wi4z.png",
+ ["IsleOfLostSkulls"] = "3v6ak7k2ljui4ubj0xh21sjys.png",
+ ["Park"] = "4o5lschc01nani8xeywao622n.png",
+ ["Soccer"] = "apo2sd115tuwueyoprp8jhx7b.png",
+ ["StuntRunner"] = "8j4f516axpnpjgnj342t85vkd.png",
+ };
+
+ var names = canonical.Keys.ToArray();
+ var rooms = await db.Rooms
+ .Where(r => names.Contains(r.Name))
+ .ToListAsync(ct);
+
+ var now = DateTime.UtcNow;
+ var changed = 0;
+ foreach (var room in rooms)
+ {
+ if (!canonical.TryGetValue(room.Name, out var imageName) || room.ImageName == imageName)
+ continue;
+
+ room.ImageName = imageName;
+ room.UpdatedAt = now;
+ changed++;
+ }
+
+ if (changed == 0)
+ {
+ logger.LogInformation(
+ "[legacy-upgrade] BackfillCanonicalRoomImages: skipped (all present RRO rows already use canonical blob names)");
+ }
+ else
+ {
+ await db.SaveChangesAsync(ct);
+ logger.LogInformation(
+ "[legacy-upgrade] BackfillCanonicalRoomImages: applied — updated {Count} room image rows",
+ changed);
+ }
+
+ var missing = names.Except(rooms.Select(r => r.Name), StringComparer.OrdinalIgnoreCase).ToArray();
+ if (missing.Length > 0)
+ {
+ logger.LogInformation(
+ "[legacy-upgrade] BackfillCanonicalRoomImages: {Count} canonical rooms not present yet ({Names})",
+ missing.Length, string.Join(", ", missing));
+ }
+ }
+
/// Coerce every
/// row to the apex's current Photon:CloudRegion.
///