using RadicadeServer.RecNet.Avatar; using Xunit; namespace RadicadeServer.Tests; /// /// 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. /// 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)); } }