using Newtonsoft.Json.Linq; using Campus_live_rewrite.api.dbs; using Campus_live_rewrite.api.server.Classes.db_class; namespace Campus_live_rewrite.api.server { /// /// Which picture a skin / consumable / plain gift-drop box (the N-Star Unique Boxes) and its /// store tile carry. /// /// The real-store import (2026-09-17..22) gave those store rows Rec Room's own thumbnail names, /// and bought boxes copy them. Almost none of those files exist on this server (0 of 88 skin /// thumbnails, 30 of 168 consumable ones, 0 of 13 box ones) and img.rec.net is gone, so every /// one of them was answered with the fallback room picture - where, with NO thumbnail name, /// the client draws the item's own picture, which is what they showed before the import. /// /// So: a consumable uses the image from the consumables catalog (items/Consumables.json / /// AllUnlockableConsumables.json - 160 of those files are here) when there is one; otherwise /// a thumbnail name is only sent when the file really exists, and is left EMPTY when it does /// not, so the client falls back to the item itself. Avatar items, tokens and XP boxes are /// left alone. /// public static class ItemImages { private static readonly string ImagesRoot = Path.Combine(Environment.CurrentDirectory, "Data", "Images"); // The same folders the /img route looks in (ImagesController.ImgServer). private static readonly string[] Folders = { "", "rro_images", Path.Combine("rro_images", "base_rros"), "player_images", "polaroid_images", "DefaultImgs", "rec_net", "custom_shirt_imgs", "LoadingScreenTips" }; private static readonly System.Collections.Concurrent.ConcurrentDictionary _exists = new(StringComparer.OrdinalIgnoreCase); private static readonly TimeSpan ExistsTtl = TimeSpan.FromMinutes(10); /// Is there a file of this name that /img will serve? public static bool Exists(string? name) { if (string.IsNullOrWhiteSpace(name) || name.IndexOfAny(new[] { '/', '\\' }) >= 0 || name.Contains("..")) return false; if (_exists.TryGetValue(name, out var hit) && DateTime.UtcNow - hit.at < ExistsTtl) return hit.ok; bool ok = false; try { ok = Folders.Any(f => File.Exists(Path.Combine(ImagesRoot, f, name))); } catch { } _exists[name] = (ok, DateTime.UtcNow); return ok; } // ---- consumables catalog ------------------------------------------------------------- private static Dictionary? _consumableImages; private static readonly object _lock = new(); private static Dictionary ConsumableImages() { lock (_lock) { if (_consumableImages != null) return _consumableImages; var map = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var file in new[] { "Consumables.json", "AllUnlockableConsumables.json" }) { try { string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "items", file); if (!File.Exists(path)) continue; foreach (var e in JArray.Parse(File.ReadAllText(path)).OfType()) { string key = PlayerDB.NormalizeConsumableDesc((string?)e["ConsumableItemDesc"]); string? img = (string?)e["ThumbnailImageName"]; if (key.Length > 0 && !string.IsNullOrWhiteSpace(img) && !map.ContainsKey(key)) map[key] = img!; } } catch (Exception ex) { Console.WriteLine($"[item-images] {file} unreadable: {ex.Message}"); } } return _consumableImages = map; } } /// The consumables catalog's image for this consumable, if that file is here. public static string? ConsumableImage(string? consumableItemDesc) { string key = PlayerDB.NormalizeConsumableDesc(consumableItemDesc); return key.Length > 0 && ConsumableImages().TryGetValue(key, out var img) && Exists(img) ? img : null; } /// /// The thumbnail to send for an item. is what the box / row /// carries. Only skins, consumables and item-less gift drops are touched. /// public static string ThumbFor(string? stored, string? consumableDesc, string? equipmentPrefab, string? equipmentGuid, string? avatarDesc, string? customAvatarItemId, bool isCurrencyOrXp) { stored ??= ""; bool avatar = !string.IsNullOrWhiteSpace(avatarDesc) || !string.IsNullOrWhiteSpace(customAvatarItemId); if (avatar || isCurrencyOrXp) return stored; if (!string.IsNullOrWhiteSpace(consumableDesc)) return ConsumableImage(consumableDesc) ?? (Exists(stored) ? stored : ""); // A skin, or a gift drop with no item of its own (the N-Star Unique Boxes). return Exists(stored) ? stored : ""; } public static string ThumbFor(PendingGiftBox g) => ThumbFor(g.ThumbnailImageName, g.ConsumableItemDesc, g.EquipmentPrefabName, g.EquipmentModificationGuid, g.AvatarItemDesc, g.CustomAvatarItemId, (g.Currency > 0 && g.CurrencyType != 0) || GiftBoxes.IsXpOnly(g)); /// Fix a store row's GiftDrop.ThumbnailImageName in place (served stores). public static bool FixStoreRow(JObject row) { if (row["GiftDrop"] is not JObject gd) return false; string? before = (string?)gd["ThumbnailImageName"]; bool currency = (((int?)gd["CurrencyType"]) ?? 0) != 0 && (((long?)gd["Currency"]) ?? 0) > 0; string after = ThumbFor(before, (string?)gd["ConsumableItemDesc"], (string?)gd["EquipmentPrefabName"], (string?)gd["EquipmentModificationGuid"], (string?)gd["AvatarItemDesc"], (string?)gd["CustomAvatarItemId"], currency); if (after == (before ?? "")) return false; gd["ThumbnailImageName"] = after.Length > 0 ? after : null; return true; } } }