mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f507702b9 | |||
| ee7d614262 |
@@ -215,6 +215,88 @@ public sealed class OwnedAvatarItemsTests : IClassFixture<DorkNetServerFactory>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Owned_items_carry_the_same_name_tooltip_and_rarity_as_their_shop_tile()
|
||||||
|
{
|
||||||
|
using var setup = ApiClient();
|
||||||
|
var player = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||||
|
|
||||||
|
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||||
|
await scope.ServiceProvider.GetRequiredService<DorkNet.Server.Services.ServerSettingsService>()
|
||||||
|
.SetAllAvatarItemsOwnedEnabledAsync(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = ApiClient(player);
|
||||||
|
var owned = await GetJsonAsync(client, "/api/avatar/v4/items");
|
||||||
|
var shop = await GetJsonAsync(client, "/api/storefronts/v3/giftdropstore/3");
|
||||||
|
|
||||||
|
var tiles = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
||||||
|
foreach (var row in shop.GetProperty("StoreItems").EnumerateArray())
|
||||||
|
{
|
||||||
|
var drop = row.GetProperty("GiftDrop");
|
||||||
|
var desc = drop.GetProperty("AvatarItemDesc").GetString();
|
||||||
|
if (!string.IsNullOrEmpty(desc)) tiles.TryAdd(desc, drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
var matched = 0;
|
||||||
|
foreach (var item in owned.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!tiles.TryGetValue(item.GetProperty("AvatarItemDesc").GetString()!, out var tile)) continue;
|
||||||
|
matched++;
|
||||||
|
Assert.Equal(tile.GetProperty("FriendlyName").GetString(), item.GetProperty("FriendlyName").GetString());
|
||||||
|
Assert.Equal(tile.GetProperty("Tooltip").GetString(), item.GetProperty("Tooltip").GetString());
|
||||||
|
Assert.Equal(tile.GetProperty("Rarity").GetInt32(), item.GetProperty("Rarity").GetInt32());
|
||||||
|
}
|
||||||
|
Assert.True(matched > 0, "no owned item matched a shop tile");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await using var scope = _factory.Services.CreateAsyncScope();
|
||||||
|
await scope.ServiceProvider.GetRequiredService<DorkNet.Server.Services.ServerSettingsService>()
|
||||||
|
.SetAllAvatarItemsOwnedEnabledAsync(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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.
|
// Beard_Close: one of the client's defaultUnlockedVariants.
|
||||||
private const string DefaultItemGuid = "8d10cc78-6b00-45f3-affb-205e9cc5b03f";
|
private const string DefaultItemGuid = "8d10cc78-6b00-45f3-affb-205e9cc5b03f";
|
||||||
|
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ public class AvatarItemsController(
|
|||||||
// reverts to the real per-player wardrobe below. Bounded by the same
|
// reverts to the real per-player wardrobe below. Bounded by the same
|
||||||
// maxItems ceiling so a future watch build can't be over-fed.
|
// maxItems ceiling so a future watch build can't be over-fed.
|
||||||
if (await serverSettings.IsAllAvatarItemsOwnedEnabledAsync())
|
if (await serverSettings.IsAllAvatarItemsOwnedEnabledAsync())
|
||||||
return Ok(BuildEntireCatalogOwned(maxItems));
|
return await WithStoreInfoAsync(BuildEntireCatalogOwned(maxItems));
|
||||||
|
|
||||||
var pid = this.CurrentPlayerId();
|
var pid = this.CurrentPlayerId();
|
||||||
if (pid is not long me) return Ok(BuildDefaultUnlocked(maxItems));
|
if (pid is not long me) return await WithStoreInfoAsync(BuildDefaultUnlocked(maxItems));
|
||||||
|
|
||||||
var avatar = await db.Avatars
|
var avatar = await db.Avatars
|
||||||
.Where(a => a.PlayerId == me)
|
.Where(a => a.PlayerId == me)
|
||||||
@@ -134,7 +134,7 @@ public class AvatarItemsController(
|
|||||||
.Where(slug => StoreService.TryGetHairDyePayload(slug, out _))
|
.Where(slug => StoreService.TryGetHairDyePayload(slug, out _))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (owned.Count == 0 && ownedHairDyes.Count == 0) return Ok(BuildDefaultUnlocked(maxItems));
|
if (owned.Count == 0 && ownedHairDyes.Count == 0) return await WithStoreInfoAsync(BuildDefaultUnlocked(maxItems));
|
||||||
|
|
||||||
var catalog = _catalog.Value;
|
var catalog = _catalog.Value;
|
||||||
var equipped = ParseEquippedItemGuids(avatar?.OutfitSelections);
|
var equipped = ParseEquippedItemGuids(avatar?.OutfitSelections);
|
||||||
@@ -167,6 +167,7 @@ public class AvatarItemsController(
|
|||||||
// miss and silently drop every owned colored item (the contest
|
// miss and silently drop every owned colored item (the contest
|
||||||
// hat "I bought it but it never shows in my wardrobe" bug).
|
// hat "I bought it but it never shows in my wardrobe" bug).
|
||||||
if (!catalog.Items.TryGetValue(entry.BaseGuid, out var item)) continue;
|
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;
|
var slot = catalog.SlotByGuid.TryGetValue(entry.BaseGuid, out var value) ? value : -1;
|
||||||
perSlot.TryGetValue(slot, out var count);
|
perSlot.TryGetValue(slot, out var count);
|
||||||
@@ -226,7 +227,34 @@ public class AvatarItemsController(
|
|||||||
IsBaseAvatarItem = false,
|
IsBaseAvatarItem = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Ok(result);
|
return await WithStoreInfoAsync(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Give each owned row the same FriendlyName, Tooltip and
|
||||||
|
/// Rarity its shop tile shows. The client keeps this list's metadata
|
||||||
|
/// per item, and the catalog dump only carries asset names (e.g.
|
||||||
|
/// "(Shirt_Winter)" for colour variants) with no rarity or tooltip, so
|
||||||
|
/// the owned list disagreed with the shop for most items. Rows with no
|
||||||
|
/// shop tile keep the catalog name. Catalog DTOs are shared, so matched
|
||||||
|
/// rows are copied, never mutated.</summary>
|
||||||
|
private async Task<List<UnlockedAvatarItemDto>> WithStoreInfoAsync(List<UnlockedAvatarItemDto> rows)
|
||||||
|
{
|
||||||
|
var info = await StoreService.LoadAvatarItemStoreInfoAsync(db);
|
||||||
|
if (info.Count == 0) return rows;
|
||||||
|
return rows.Select(r => info.TryGetValue(r.AvatarItemDesc, out var s)
|
||||||
|
? new UnlockedAvatarItemDto
|
||||||
|
{
|
||||||
|
AvatarItemType = r.AvatarItemType,
|
||||||
|
AvatarItemDesc = r.AvatarItemDesc,
|
||||||
|
AvatarItemId = r.AvatarItemId,
|
||||||
|
PlatformMask = r.PlatformMask,
|
||||||
|
FriendlyName = string.IsNullOrWhiteSpace(s.FriendlyName) ? r.FriendlyName : s.FriendlyName,
|
||||||
|
Tooltip = s.Tooltip,
|
||||||
|
Rarity = s.Rarity,
|
||||||
|
TagList = r.TagList,
|
||||||
|
IsBaseAvatarItem = r.IsBaseAvatarItem,
|
||||||
|
}
|
||||||
|
: r).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The free items every player owns
|
/// <summary>The free items every player owns
|
||||||
@@ -250,6 +278,8 @@ public class AvatarItemsController(
|
|||||||
foreach (var item in catalog.Items.Values)
|
foreach (var item in catalog.Items.Values)
|
||||||
{
|
{
|
||||||
if (result.Count >= maxItems) return result;
|
if (result.Count >= maxItems) return result;
|
||||||
|
// Custom-clothing templates aren't ownable clothing.
|
||||||
|
if (item.IsBaseAvatarItem) continue;
|
||||||
result.Add(item);
|
result.Add(item);
|
||||||
}
|
}
|
||||||
// Colored variants aren't in the base avatar_item_lookup catalog, so
|
// Colored variants aren't in the base avatar_item_lookup catalog, so
|
||||||
@@ -299,14 +329,33 @@ public class AvatarItemsController(
|
|||||||
return result;
|
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")]
|
[HttpGet("api/avatar/v1/defaultbaseavataritems")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public IActionResult DefaultBaseAvatarItems()
|
public IActionResult DefaultBaseAvatarItems()
|
||||||
{
|
{
|
||||||
var rows = _catalog.Value.Items.Values
|
var items = _catalog.Value.Items;
|
||||||
.OrderBy(i => i.AvatarItemType)
|
var rows = BaseAvatarItemIds.Keys
|
||||||
.ThenBy(i => i.FriendlyName)
|
.Where(items.ContainsKey)
|
||||||
.Take(100)
|
.Select(g => items[g])
|
||||||
.ToList();
|
.ToList();
|
||||||
return Ok(rows);
|
return Ok(rows);
|
||||||
}
|
}
|
||||||
@@ -379,8 +428,8 @@ public class AvatarItemsController(
|
|||||||
.Select(guid => catalog.TryGetValue(guid, out var item) ? item : null)
|
.Select(guid => catalog.TryGetValue(guid, out var item) ? item : null)
|
||||||
.Where(item => item is not null)
|
.Where(item => item is not null)
|
||||||
.Select(item => item!)
|
.Select(item => item!)
|
||||||
.ToArray();
|
.ToList();
|
||||||
return Ok(rows);
|
return Ok(await WithStoreInfoAsync(rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Per-OutfitType slot index → list of GUIDs in that slot.
|
/// <summary>Per-OutfitType slot index → list of GUIDs in that slot.
|
||||||
@@ -580,13 +629,13 @@ public class AvatarItemsController(
|
|||||||
// GUID as a color GUID and log "Can not find hair dye color".
|
// GUID as a color GUID and log "Can not find hair dye color".
|
||||||
AvatarItemType = AvatarItemType.Outfit,
|
AvatarItemType = AvatarItemType.Outfit,
|
||||||
AvatarItemDesc = row.Guid + ",,,",
|
AvatarItemDesc = row.Guid + ",,,",
|
||||||
AvatarItemId = StableAvatarItemId(row.Guid),
|
AvatarItemId = BaseAvatarItemIds.TryGetValue(row.Guid, out var baseId) ? baseId : StableAvatarItemId(row.Guid),
|
||||||
PlatformMask = unchecked((int)0xFFFFFFFF),
|
PlatformMask = unchecked((int)0xFFFFFFFF),
|
||||||
FriendlyName = pretty,
|
FriendlyName = pretty,
|
||||||
Tooltip = string.Empty,
|
Tooltip = string.Empty,
|
||||||
Rarity = 0,
|
Rarity = 0,
|
||||||
TagList = string.Empty,
|
TagList = string.Empty,
|
||||||
IsBaseAvatarItem = false,
|
IsBaseAvatarItem = IsBaseAvatarItemGuid(row.Guid),
|
||||||
};
|
};
|
||||||
if (!bySlot.TryGetValue(row.OutfitType, out var lst))
|
if (!bySlot.TryGetValue(row.OutfitType, out var lst))
|
||||||
bySlot[row.OutfitType] = lst = new List<string>();
|
bySlot[row.OutfitType] = lst = new List<string>();
|
||||||
@@ -599,7 +648,7 @@ public class AvatarItemsController(
|
|||||||
var defaults = dump.DefaultUnlockedVariants
|
var defaults = dump.DefaultUnlockedVariants
|
||||||
.Where(v => string.IsNullOrEmpty(v.Swatch) && string.IsNullOrEmpty(v.Mask) && string.IsNullOrEmpty(v.Decal))
|
.Where(v => string.IsNullOrEmpty(v.Swatch) && string.IsNullOrEmpty(v.Mask) && string.IsNullOrEmpty(v.Decal))
|
||||||
.Select(v => v.Item)
|
.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)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,11 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
|
|||||||
foreach (var row in stale)
|
foreach (var row in stale)
|
||||||
{
|
{
|
||||||
var guid = row.Slug.Substring("wardrobe-".Length);
|
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)
|
if (toDelete.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -690,6 +694,9 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
|
|||||||
var added = false;
|
var added = false;
|
||||||
foreach (var (guid, dto) in catalog)
|
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}";
|
var slug = $"wardrobe-{guid}";
|
||||||
if (existingSlugs.Contains(slug)) continue;
|
if (existingSlugs.Contains(slug)) continue;
|
||||||
existingSlugs.Add(slug);
|
existingSlugs.Add(slug);
|
||||||
@@ -1191,6 +1198,32 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
|
|||||||
/// id-lists (toptoday) resolve against the gift-drop cache.</summary>
|
/// id-lists (toptoday) resolve against the gift-drop cache.</summary>
|
||||||
public static int PurchasableItemIdFor(StoreItemEntity i) => (int)(i.Id & 0x7fffffff);
|
public static int PurchasableItemIdFor(StoreItemEntity i) => (int)(i.Id & 0x7fffffff);
|
||||||
|
|
||||||
|
/// <summary>Shop metadata for an avatar item, keyed by the exact
|
||||||
|
/// AvatarItemDesc a shop tile carries.</summary>
|
||||||
|
public sealed record AvatarItemStoreInfo(string FriendlyName, string Tooltip, int Rarity);
|
||||||
|
|
||||||
|
/// <summary>FriendlyName / Tooltip / Rarity of every avatar-item and
|
||||||
|
/// hair-dye shop tile, keyed by the same AvatarItemDesc
|
||||||
|
/// <see cref="BuildGiftDrop"/> sends (ordinal: colour-variant
|
||||||
|
/// combination ids are case-sensitive). The owned-items list uses it so
|
||||||
|
/// an item reads the same in the wardrobe as in the shop.</summary>
|
||||||
|
public static async Task<Dictionary<string, AvatarItemStoreInfo>> LoadAvatarItemStoreInfoAsync(DorkNetDbContext db)
|
||||||
|
{
|
||||||
|
var rows = await db.StoreItems.AsNoTracking()
|
||||||
|
.Select(i => new { i.Slug, i.DisplayName, i.Description, i.Category })
|
||||||
|
.ToListAsync();
|
||||||
|
var info = new Dictionary<string, AvatarItemStoreInfo>(StringComparer.Ordinal);
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
string desc;
|
||||||
|
if (TryGetHairDyePayload(row.Slug, out _, out var hairColorGuid)) desc = hairColorGuid;
|
||||||
|
else if (!TryGetAvatarItemPayload(row.Slug, out _, out desc)) continue;
|
||||||
|
if (string.IsNullOrEmpty(desc)) continue;
|
||||||
|
info.TryAdd(desc, new AvatarItemStoreInfo(row.DisplayName ?? string.Empty, row.Description ?? string.Empty, MapRarity(row.Category)));
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>GiftManager.GiftRarity wire values:
|
/// <summary>GiftManager.GiftRarity wire values:
|
||||||
/// None=-1, Common=0, Uncommon=10, Rare=20, Epic=30, Legendary=50.</summary>
|
/// None=-1, Common=0, Uncommon=10, Rare=20, Epic=30, Legendary=50.</summary>
|
||||||
private static int MapRarity(string? category) => category?.ToLowerInvariant() switch
|
private static int MapRarity(string? category) => category?.ToLowerInvariant() switch
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ verified against the binary before the change, and the full test suite passes.
|
|||||||
| `GET api/avatar/v1/lockeditems` | 5-key subset: AvatarItemId 0, no TagList/IsBaseAvatarItem | full IFNGBPJGINL owned-item shape |
|
| `GET api/avatar/v1/lockeditems` | 5-key subset: AvatarItemId 0, no TagList/IsBaseAvatarItem | full IFNGBPJGINL owned-item shape |
|
||||||
| `GET api/avatar/v4/items` (colored variants) | a colorway reused its base item's AvatarItemId, so owning a base + colorways collided in the id-keyed wardrobe | id hashed from the full descriptor |
|
| `GET api/avatar/v4/items` (colored variants) | a colorway reused its base item's AvatarItemId, so owning a base + colorways collided in the id-keyed wardrobe | id hashed from the full descriptor |
|
||||||
| `GET api/avatar/v4/items` / `defaultunlocked` / `lockeditems` (default items) | the client REPLACES its unlocked set with each downloaded list (OutfitManager.GFEIDFKKNMO clears field +0x70) and never merges its own defaults, so the free `default_unlocked_variants` items (e.g. Beard Close) showed a price | every list includes the 239 default items; `defaultunlocked` serves them before login; `lockeditems` never reports them |
|
| `GET api/avatar/v4/items` / `defaultunlocked` / `lockeditems` (default items) | the client REPLACES its unlocked set with each downloaded list (OutfitManager.GFEIDFKKNMO clears field +0x70) and never merges its own defaults, so the free `default_unlocked_variants` items (e.g. Beard Close) showed a price | every list includes the 239 default items; `defaultunlocked` serves them before login; `lockeditems` never reports them |
|
||||||
|
| `GET api/avatar/v4/items` / `defaultunlocked` / `lockeditems` (metadata) | names came from the catalog dump (`(Shirt_Winter)` for colour variants), rarity was always 0 and tooltips empty, so 1,864 items disagreed with their shop tile | each owned row takes FriendlyName / Tooltip / Rarity from its shop tile (`StoreService.LoadAvatarItemStoreInfoAsync`), matched on the exact AvatarItemDesc |
|
||||||
|
| `GET api/avatar/v1/defaultbaseavataritems` | returned the first 100 catalog items; the client flags every row as a custom-clothing template (`AvatarItemData.IsBaseAvatarItem`, keyed by AvatarItemId) and skips templates in the owned rebuild (`OutfitManager.MBPOFMEHDIJ`), so those 100 ordinary items (80sHipHopJacket, Beard Close, ...) showed a price even when owned and equipped | only the UGC templates `UGCTee_Shirt` (AvatarItemId 2184) and `UGCPulloverHoodie_Shirt` (2918), `IsBaseAvatarItem: true`; they're no longer sold or listed as owned clothing |
|
||||||
| `GET api/customAvatarItems/v2/fromCreator/{id}` | bare array; client reads {Results,TotalResults} | paged container |
|
| `GET api/customAvatarItems/v2/fromCreator/{id}` | bare array; client reads {Results,TotalResults} | paged container |
|
||||||
| `POST econ/customAvatarItems/v1/{id}/purchase` | 404 — route only existed under api/ | econ/ alias added |
|
| `POST econ/customAvatarItems/v1/{id}/purchase` | 404 — route only existed under api/ | econ/ alias added |
|
||||||
| `GET api/ugcPurchasables/v1/items/room/{roomId}` | 404 — only the ?roomId= form existed | path form added |
|
| `GET api/ugcPurchasables/v1/items/room/{roomId}` | 404 — only the ?roomId= form existed | path form added |
|
||||||
@@ -1348,7 +1350,7 @@ Verified all 37 real client HTTP calls against the DorkNet server source (every
|
|||||||
|
|
||||||
| Verb | Route | Request | Response |
|
| Verb | Route | Request | Response |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| GET | `api/avatar/v1/defaultbaseavataritems` | none | JSON array of the same UnlockedAvatarItem shape as v4/items |
|
| GET | `api/avatar/v1/defaultbaseavataritems` | none | JSON array of the same UnlockedAvatarItem shape as v4/items: only the custom-clothing templates (UGCTee_Shirt 2184, UGCPulloverHoodie_Shirt 2918) with `IsBaseAvatarItem: true`. Every row becomes a template client-side, so never list ordinary clothing here |
|
||||||
| GET | `api/avatar/v1/defaultunlocked` | none | Same array shape as v4/items (server aliases it to the same handler; logged out it returns the `default_unlocked_variants` items, logged in the full owned set including them) |
|
| GET | `api/avatar/v1/defaultunlocked` | none | Same array shape as v4/items (server aliases it to the same handler; logged out it returns the `default_unlocked_variants` items, logged in the full owned set including them) |
|
||||||
| GET | `api/avatar/v1/lockeditems` | Query: repeated key "desc" carrying avatar-item desc strings (list added via helper 0x181F2E250 with key literal "desc") | JSON array of List<IFNGBPJGINL> — the same element as v4/items (CLIANCNHNIE reader: AvatarItemType, AvatarItemDesc, FriendlyName, Tooltip, Rarity, TagList, AvatarItemId, IsBaseAvatarItem) for each requested desc the caller does NOT own |
|
| GET | `api/avatar/v1/lockeditems` | Query: repeated key "desc" carrying avatar-item desc strings (list added via helper 0x181F2E250 with key literal "desc") | JSON array of List<IFNGBPJGINL> — the same element as v4/items (CLIANCNHNIE reader: AvatarItemType, AvatarItemDesc, FriendlyName, Tooltip, Rarity, TagList, AvatarItemId, IsBaseAvatarItem) for each requested desc the caller does NOT own |
|
||||||
| GET | `api/avatar/v2` | none | Object with keys (exact literals in reader EKHJKDNHOPB, case-tolerant Pascal/camel/lower): OutfitSelections(string), OutfitSelectionsV2(string), FaceFeatures(string), SkinColor(string), HairColor(string), CustomAvatarIte |
|
| GET | `api/avatar/v2` | none | Object with keys (exact literals in reader EKHJKDNHOPB, case-tolerant Pascal/camel/lower): OutfitSelections(string), OutfitSelectionsV2(string), FaceFeatures(string), SkinColor(string), HairColor(string), CustomAvatarIte |
|
||||||
|
|||||||
Reference in New Issue
Block a user