From f3315309740e868726e2b9baa7909abeb9ce7a18 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Wed, 27 May 2026 10:43:44 +0300 Subject: [PATCH] avatar: raise unlocked-items defaults so new purchases survive relog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/avatar/v4/items truncated owned items by maxItems (was 30) and maxPerSlot (was 4). The per-slot cap silently dropped any newly-bought wardrobe item once a slot held 4+ entries — the 'I bought it but it's gone after relog' symptom. Defaults raised to 100 / 20. Clamp ranges left at [1,120] / [1,20] (prior maintainer note locked these to watch render-budget testing — raising the ceiling without retesting risks BrowsableAvatarItem / DynamicAvatarItemImposter thrash inside the watch's wardrobe drawer). Sort tiebreaker added: ThenByDescending(OriginalIndex) so when truncation has to drop entries, it drops the OLDEST (starter seed) items rather than the just-purchased one. GrantWardrobeAsync and the gift-consume path both append new GUIDs to the end of InventoryJson, so list index == purchase order. --- .../API/Avatar/V4/AvatarItemsController.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/DorkNet.Server/Controllers/API/Avatar/V4/AvatarItemsController.cs b/DorkNet.Server/Controllers/API/Avatar/V4/AvatarItemsController.cs index 09e2418..9e071be 100644 --- a/DorkNet.Server/Controllers/API/Avatar/V4/AvatarItemsController.cs +++ b/DorkNet.Server/Controllers/API/Avatar/V4/AvatarItemsController.cs @@ -85,8 +85,16 @@ public class AvatarItemsController(DorkNetDbContext db, IConfiguration config) : if (!config.GetValue("Avatar:EnableUnlockedItems", true)) return Ok(new List()); - var maxItems = Math.Clamp(config.GetValue("Avatar:MaxUnlockedItems", 30), 1, 120); - var maxPerSlot = Math.Clamp(config.GetValue("Avatar:MaxUnlockedItemsPerSlot", 4), 1, 20); + // Defaults raised (30 → 100, 4 → 20) — the old 4-per-slot silently + // dropped any newly-purchased item once a slot held 4+ entries (the + // "I bought it but it's gone after relog" symptom). The clamp + // ranges stay at [1,120] / [1,20] — those reflect empirical watch + // testing on render-budget headroom (per the prior maintainer note + // about BrowsableAvatarItem / DynamicAvatarItemImposter thrash + // above this count); raising the clamp ceiling without retesting + // could crash the watch's wardrobe drawer. + var maxItems = Math.Clamp(config.GetValue("Avatar:MaxUnlockedItems", 100), 1, 120); + var maxPerSlot = Math.Clamp(config.GetValue("Avatar:MaxUnlockedItemsPerSlot", 20), 1, 20); var pid = this.CurrentPlayerId(); if (pid is not long me) return Ok(new List()); @@ -109,10 +117,17 @@ public class AvatarItemsController(DorkNetDbContext db, IConfiguration config) : var catalog = _catalog.Value; var equipped = ParseEquippedItemGuids(avatar?.OutfitSelections); + // Recency tiebreaker: GrantWardrobeAsync / gift-consume APPEND new + // GUIDs to the end of InventoryJson, so the original index is also + // the purchase order. When per-slot truncation has to drop entries, + // we want it to drop the OLDEST (starter seed) items rather than the + // one the user just bought. var ordered = owned - .OrderByDescending(guid => equipped.Contains(guid)) - .ThenBy(guid => catalog.SlotByGuid.TryGetValue(guid, out var slot) ? slot : int.MaxValue) - .ThenBy(guid => guid, StringComparer.OrdinalIgnoreCase); + .Select((guid, idx) => (Guid: guid, OriginalIndex: idx)) + .OrderByDescending(x => equipped.Contains(x.Guid)) + .ThenBy(x => catalog.SlotByGuid.TryGetValue(x.Guid, out var slot) ? slot : int.MaxValue) + .ThenByDescending(x => x.OriginalIndex) + .Select(x => x.Guid); var maxOutfitItems = Math.Max(0, maxItems - ownedHairDyes.Count); var perSlot = new Dictionary();