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 ee7d614262
commit 7f507702b9
4 changed files with 79 additions and 9 deletions
@@ -257,6 +257,46 @@ public sealed class OwnedAvatarItemsTests : IClassFixture<DorkNetServerFactory>
}
}
[Fact]
public async Task Base_avatar_items_are_only_the_custom_clothing_templates()
{
// The client flags every defaultbaseavataritems row as a custom-
// clothing template and leaves it out of the owned rebuild, so a
// normal item listed here showed a price even when owned.
using var anonymous = ApiClient();
var rows = await GetJsonAsync(anonymous, "/api/avatar/v1/defaultbaseavataritems");
var byDesc = rows.EnumerateArray().ToDictionary(
r => r.GetProperty("AvatarItemDesc").GetString()!.Split(',')[0],
r => r);
Assert.Equal(2, byDesc.Count);
Assert.Equal(2184, byDesc["c5d70cb4-71dd-4fe4-b719-34fe2073c611"].GetProperty("AvatarItemId").GetInt32());
Assert.Equal(2918, byDesc["95a519de-f2cb-429c-b014-508477f20d42"].GetProperty("AvatarItemId").GetInt32());
Assert.All(byDesc.Values, r => Assert.True(r.GetProperty("IsBaseAvatarItem").GetBoolean()));
// Templates aren't sold or listed as owned clothing.
var shop = await GetJsonAsync(anonymous, "/api/storefronts/v3/giftdropstore/3");
var shopDescs = shop.GetProperty("StoreItems").EnumerateArray()
.Select(r => r.GetProperty("GiftDrop").GetProperty("AvatarItemDesc").GetString() ?? "");
Assert.DoesNotContain(shopDescs, d => byDesc.ContainsKey(d.Split(',')[0]));
await using (var scope = _factory.Services.CreateAsyncScope())
await scope.ServiceProvider.GetRequiredService<DorkNet.Server.Services.ServerSettingsService>()
.SetAllAvatarItemsOwnedEnabledAsync(true);
try
{
var owned = await GetJsonAsync(anonymous, "/api/avatar/v1/defaultunlocked");
var ownedDescs = owned.EnumerateArray().Select(r => r.GetProperty("AvatarItemDesc").GetString()!.Split(',')[0]).ToList();
Assert.DoesNotContain(ownedDescs, byDesc.ContainsKey);
Assert.Contains("3c2ed30c-ca2a-4f9f-9bfa-ffe561c4a686", ownedDescs); // 80sHipHopJacket
}
finally
{
await using var scope = _factory.Services.CreateAsyncScope();
await scope.ServiceProvider.GetRequiredService<DorkNet.Server.Services.ServerSettingsService>()
.SetAllAvatarItemsOwnedEnabledAsync(false);
}
}
// Beard_Close: one of the client's defaultUnlockedVariants.
private const string DefaultItemGuid = "8d10cc78-6b00-45f3-affb-205e9cc5b03f";