Files
Radicade/ServerTests/AvatarItemDescriptionTests.cs
T
2026-10-16 23:33:13 -07:00

98 lines
4.7 KiB
C#

using RadicadeServer.RecNet.Avatar;
using Xunit;
namespace RadicadeServer.Tests;
/// <summary>
/// The catalog descriptions we hand the client must match the shape its own
/// AvatarItemWardrobeRuntimeConfig uses. Live RecNet records only ever carry
/// "guid," (no combination) or "guid,mat1,mat2," (materialised), never empty slots.
/// An empty slot makes the client log "Browsable avatar item not found" and drop the
/// item from the avatar, so these tests pin both the rewrite and its no-op cases.
/// </summary>
public class AvatarItemDescriptionTests
{
[Theory]
// The legacy "no combination" form loses its empty slots.
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")]
[InlineData("_OWVy3z6iU-M3-zbQgSLig,,,", "_OWVy3z6iU-M3-zbQgSLig,")]
// An empty slot in the middle of a material list is dropped, order is preserved.
[InlineData("d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,,0440f08f-ef1d-49d8-942b-523056e8bb45,",
"d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,")]
// Surrounding whitespace is not part of the identifier.
[InlineData(" 8aa79563-ace1-4ba7-ad0c-f3210a78142f,, ", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")]
// Well-formed descriptions are returned byte-for-byte, including the shapes live
// RecNet writes and the trailing comma the client's own serializer omits.
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")]
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c,",
"8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c,")]
[InlineData("d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A",
"d84993dd-4e15-4481-9450-f88a34cdbc82,FnhttSsCoUCSAW8j-stC5A")]
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c",
"8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,05f0ee6e-c824-470e-9178-5ed576c6fe0c")]
public void Normalize_ProducesClientShapedDescriptions(string input, string expected)
{
Assert.Equal(expected, AvatarItemDescription.Normalize(input));
}
[Fact]
public void Normalize_IsIdempotent()
{
var once = AvatarItemDescription.Normalize("8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,");
Assert.Equal(once, AvatarItemDescription.Normalize(once));
}
[Fact]
public void Normalize_LeavesMissingDescriptionsAlone()
{
Assert.Equal("", AvatarItemDescription.Normalize(""));
Assert.Equal("", AvatarItemDescription.Normalize(null));
Assert.Equal("", AvatarItemDescription.Normalize(" "));
// A description with no item GUID at all is not something this can repair.
Assert.Equal(",,,", AvatarItemDescription.Normalize(",,,"));
}
[Theory]
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,", true)]
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", false)]
[InlineData("8aa79563-ace1-4ba7-ad0c-f3210a78142f,95e4cc30-cb68-473d-a395-feadf5b51512,", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void HasEmptyMaterialSlot_DetectsOnlyLegacyRows(string? input, bool expected)
{
Assert.Equal(expected, AvatarItemDescription.HasEmptyMaterialSlot(input));
}
[Fact]
public void CatalogParsingRepairsLegacyRows()
{
var items = AvatarUnlockCatalog.ParseCatalog("""
[{"AvatarItemDesc":"8aa79563-ace1-4ba7-ad0c-f3210a78142f,,,","UnlockedLevel":0,"PlatformMask":-1,
"FriendlyName":"Rec Room Shirt (V-Neck, White)","Rarity":0}]
""");
var item = Assert.Single(items);
Assert.Equal("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", item.AvatarItemDesc);
Assert.Equal("Rec Room Shirt (V-Neck, White)", item.FriendlyName);
}
[Fact]
public void KeepSupportedItemsRepairsLegacyRows()
{
var copy = Assert.Single(AvatarUnlockCatalog.KeepSupportedItems(
[new AvatarItem { AvatarItemDesc = "_qXaNbm4yEeyalo372QbvA,,,", FriendlyName = "Biker Goggles" }]));
Assert.Equal("_qXaNbm4yEeyalo372QbvA,", copy.AvatarItemDesc);
}
[Fact]
public void ShippedCatalogNeverServesEmptyMaterialSlots()
{
var catalogJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json"));
var items = AvatarUnlockCatalog.ParseCatalog(catalogJson);
Assert.NotEmpty(items);
Assert.DoesNotContain(items, item => AvatarItemDescription.HasEmptyMaterialSlot(item.AvatarItemDesc));
}
}