fix: owned items showed a price because they were served as base avatar items

defaultbaseavataritems returned the first 100 catalog items. The 2023
client flags every row of that list as a custom-clothing template
(AvatarItemData.IsBaseAvatarItem, keyed by AvatarItemId) and skips
templates when it rebuilds the owned outfit-menu entries, so those 100
ordinary items (80sHipHopJacket, Beard Close, ...) showed a price even
when owned and equipped, while the cart (which checks the owned set)
said "owned".

- defaultbaseavataritems returns only the UGC templates, UGCTee_Shirt
  (AvatarItemId 2184) and UGCPulloverHoodie_Shirt (2918), as the real
  service served them
- those two keep the same ids in every list and carry IsBaseAvatarItem
- they're no longer sold (existing shop rows are removed on seed) or
  listed as owned clothing
This commit is contained in:
Alexa
2026-10-15 22:07:00 +03:00
parent f4b4836375
commit 2dbd03b9ae
4 changed files with 79 additions and 9 deletions
@@ -167,6 +167,7 @@ public class AvatarItemsController(
// miss and silently drop every owned colored item (the contest
// hat "I bought it but it never shows in my wardrobe" bug).
if (!catalog.Items.TryGetValue(entry.BaseGuid, out var item)) continue;
if (item.IsBaseAvatarItem) continue; // custom-clothing template, not clothing
var slot = catalog.SlotByGuid.TryGetValue(entry.BaseGuid, out var value) ? value : -1;
perSlot.TryGetValue(slot, out var count);
@@ -277,6 +278,8 @@ public class AvatarItemsController(
foreach (var item in catalog.Items.Values)
{
if (result.Count >= maxItems) return result;
// Custom-clothing templates aren't ownable clothing.
if (item.IsBaseAvatarItem) continue;
result.Add(item);
}
// Colored variants aren't in the base avatar_item_lookup catalog, so
@@ -326,14 +329,33 @@ public class AvatarItemsController(
return result;
}
/// <summary>The custom-clothing templates ("base avatar items") and
/// their RecNet AvatarItemIds, as the real service served them. The
/// client flags every row of <c>defaultbaseavataritems</c> as a base
/// item (AvatarItemData.IsBaseAvatarItem, keyed by AvatarItemId in
/// OutfitManager.AODPBDNAGII) and leaves base items out of the owned
/// rebuild (MBPOFMEHDIJ), so they never read as owned clothing. This
/// endpoint used to return the first 100 catalog items, which turned
/// ordinary items (80sHipHopJacket, Beard Close, ...) into templates
/// that showed a price even when owned and equipped.</summary>
public static readonly IReadOnlyDictionary<string, int> BaseAvatarItemIds =
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["c5d70cb4-71dd-4fe4-b719-34fe2073c611"] = 2184, // UGCTee_Shirt
["95a519de-f2cb-429c-b014-508477f20d42"] = 2918, // UGCPulloverHoodie_Shirt
};
public static bool IsBaseAvatarItemGuid(string? guid) =>
!string.IsNullOrEmpty(guid) && BaseAvatarItemIds.ContainsKey(guid);
[HttpGet("api/avatar/v1/defaultbaseavataritems")]
[AllowAnonymous]
public IActionResult DefaultBaseAvatarItems()
{
var rows = _catalog.Value.Items.Values
.OrderBy(i => i.AvatarItemType)
.ThenBy(i => i.FriendlyName)
.Take(100)
var items = _catalog.Value.Items;
var rows = BaseAvatarItemIds.Keys
.Where(items.ContainsKey)
.Select(g => items[g])
.ToList();
return Ok(rows);
}
@@ -607,13 +629,13 @@ public class AvatarItemsController(
// GUID as a color GUID and log "Can not find hair dye color".
AvatarItemType = AvatarItemType.Outfit,
AvatarItemDesc = row.Guid + ",,,",
AvatarItemId = StableAvatarItemId(row.Guid),
AvatarItemId = BaseAvatarItemIds.TryGetValue(row.Guid, out var baseId) ? baseId : StableAvatarItemId(row.Guid),
PlatformMask = unchecked((int)0xFFFFFFFF),
FriendlyName = pretty,
Tooltip = string.Empty,
Rarity = 0,
TagList = string.Empty,
IsBaseAvatarItem = false,
IsBaseAvatarItem = IsBaseAvatarItemGuid(row.Guid),
};
if (!bySlot.TryGetValue(row.OutfitType, out var lst))
bySlot[row.OutfitType] = lst = new List<string>();
@@ -626,7 +648,7 @@ public class AvatarItemsController(
var defaults = dump.DefaultUnlockedVariants
.Where(v => string.IsNullOrEmpty(v.Swatch) && string.IsNullOrEmpty(v.Mask) && string.IsNullOrEmpty(v.Decal))
.Select(v => v.Item)
.Where(g => !string.IsNullOrEmpty(g) && items.ContainsKey(g))
.Where(g => !string.IsNullOrEmpty(g) && items.ContainsKey(g) && !IsBaseAvatarItemGuid(g))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
+8 -1
View File
@@ -179,7 +179,11 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
foreach (var row in stale)
{
var guid = row.Slug.Substring("wardrobe-".Length);
if (!safe.Contains(guid)) toDelete.Add(row.Id);
// Custom-clothing templates aren't sold; drop rows seeded
// before they were excluded.
if (!safe.Contains(guid) ||
DorkNet.Server.Controllers.API.Avatar.V4.AvatarItemsController.IsBaseAvatarItemGuid(guid))
toDelete.Add(row.Id);
}
if (toDelete.Count > 0)
{
@@ -690,6 +694,9 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
var added = false;
foreach (var (guid, dto) in catalog)
{
// The UGC templates are the base of custom clothing, not shop
// items (the real store never listed them).
if (dto.IsBaseAvatarItem) continue;
var slug = $"wardrobe-{guid}";
if (existingSlugs.Contains(slug)) continue;
existingSlugs.Add(slug);