using System.Text.Json; using System.Text.Json.Serialization; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Store; using Xunit; namespace RadicadeServer.Tests; /// /// The shipped consumable and equipment-skin catalogs come from DorkNet's extraction of the /// 2023 client's own asset configs (tools/import_dorknet_catalogs.py). These tests keep the two /// halves of each row honest: a consumable that has no client token spawns as /// "Unable to find consumable", and a skin without an exact prefab + guid pair does not resolve /// at all. /// public class DorkNetCatalogTests { private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name); private static List ShippedConsumables() { var path = Fixture("consumables.json"); Assert.True(File.Exists(path), $"missing fixture {path}"); // Rarity is written as a name in the catalog, which is what the admin panel's own // options (JsonStringEnumConverter) accept. return JsonSerializer.Deserialize>( File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter() } }) ?? new List(); } private static List ShippedSkins() { var path = Fixture("equipment_skins.json"); Assert.True(File.Exists(path), $"missing fixture {path}"); return EquipmentSkinCatalog.Parse(File.ReadAllText(path)); } [Fact] public void ShippedConsumablesAllResolveToAClientToken() { var rows = ShippedConsumables(); Assert.True(rows.Count >= 53, $"only {rows.Count} consumable(s) shipped"); Assert.DoesNotContain(rows, row => string.IsNullOrWhiteSpace(row.ConsumableItemDesc)); // ToClientDesc leaves unknown names alone, so a row that comes back unchanged would be // served as its friendly name - exactly the bug that made gift boxes fail to spawn. var unresolved = rows .Where(row => ConsumableCatalog.ToClientDesc(row.ConsumableItemDesc) == row.ConsumableItemDesc) .Select(row => row.ConsumableItemDesc) .ToList(); Assert.Empty(unresolved); } [Fact] public void ShippedConsumablesAreDistinctAndCarryAFriendlyName() { var rows = ShippedConsumables(); Assert.Equal( rows.Count, rows.Select(row => row.ConsumableItemDesc).Distinct(StringComparer.OrdinalIgnoreCase).Count()); Assert.DoesNotContain(rows, row => string.IsNullOrWhiteSpace(row.FriendlyName)); Assert.Contains(rows, row => row.Rarity != Rarity.Common); } [Fact] public void ShippedSkinsAllCarryThePairTheClientResolvesThemFrom() { var skins = ShippedSkins(); Assert.True(skins.Count >= 194, $"only {skins.Count} skin(s) shipped"); Assert.DoesNotContain(skins, skin => string.IsNullOrWhiteSpace(skin.PrefabName)); Assert.DoesNotContain(skins, skin => string.IsNullOrWhiteSpace(skin.ModificationGuid)); Assert.DoesNotContain(skins, skin => string.IsNullOrWhiteSpace(skin.DisplayName)); Assert.Equal( skins.Count, skins.Select(skin => skin.ModificationGuid).Distinct(StringComparer.OrdinalIgnoreCase).Count()); } [Fact] public void SkinsBecomeEquipmentRowsNamedFromTheCatalog() { var skin = new EquipmentSkinItem { PrefabName = "[PaintballGun]", ModificationGuid = "00000000-0000-0000-0000-000000000001", DisplayName = "Paintball Gun Skin Test", Tooltip = "Equipment skin for PaintballGun.", Rarity = Rarity.Uncommon }; var equipment = Assert.Single(EquipmentSkinCatalog.ToEquipment([skin])); Assert.Equal("[PaintballGun]", equipment.PrefabName); Assert.Equal(skin.ModificationGuid, equipment.ModificationGuid); Assert.Equal("Paintball Gun Skin Test", equipment.FriendlyName); Assert.Equal("Equipment skin for PaintballGun.", equipment.Tooltip); Assert.Equal(Rarity.Uncommon, equipment.Rarity); } [Fact] public void SkinsWithoutAPrefabOrGuidAreDropped() { var parsed = EquipmentSkinCatalog.Parse(""" { "items": [ { "prefab_name": "[Arrow]", "modification_guid": "aaa", "display_name": "Arrow Skin" }, { "prefab_name": "", "modification_guid": "bbb", "display_name": "No prefab" }, { "prefab_name": "[Basketball]", "modification_guid": "", "display_name": "No guid" } ] } """); Assert.Equal("[Arrow]", Assert.Single(parsed).PrefabName); Assert.Empty(EquipmentSkinCatalog.Parse("not json at all")); Assert.Empty(EquipmentSkinCatalog.Parse(null)); } [Fact] public void MergingSkinsAddsOnlyThePairsMissingFromTheCatalog() { const string equipmentJson = """ [ { "PrefabName": "[Arrow]", "ModificationGuid": "b862383e-0de3-492d-8f72-25e756287687", "FriendlyName": "[Arrow]", "Tooltip": "", "Rarity": 0 } ] """; const string skinsJson = """ { "items": [ { "prefab_name": "[Arrow]", "modification_guid": "b862383e-0de3-492d-8f72-25e756287687", "display_name": "Arrow Skin Vitton", "rarity": 10 }, { "prefab_name": "[Arrow]", "modification_guid": "aaaaaaaa-0000-0000-0000-000000000000", "display_name": "Arrow Skin Fresh", "rarity": 10 } ] } """; var merged = JsonSerializer.Deserialize>( EquipmentSkinCatalog.MergeInto(equipmentJson, skinsJson))!; // The existing row keeps its place: a skin contributes only pairs the catalog lacks. Assert.Equal(2, merged.Count); Assert.Equal("[Arrow]", merged[0].FriendlyName); Assert.Equal("Arrow Skin Fresh", merged[1].FriendlyName); Assert.Equal(Rarity.Uncommon, merged[1].Rarity); // Idempotent: merging the merged catalog again adds nothing. var again = JsonSerializer.Deserialize>( EquipmentSkinCatalog.MergeInto( EquipmentSkinCatalog.MergeInto(equipmentJson, skinsJson), skinsJson))!; Assert.Equal(2, again.Count); } [Fact] public void MergingAnUnreadableCatalogLeavesItAlone() { const string broken = "{ this is not an equipment list }"; Assert.Equal(broken, EquipmentSkinCatalog.MergeInto(broken, """{ "items": [ { "prefab_name": "[Arrow]", "modification_guid": "aaa" } ] }""")); Assert.Equal("[]", EquipmentSkinCatalog.MergeInto("[]", "{}")); Assert.Equal("[]", EquipmentSkinCatalog.MergeInto(null, null)); } /// How many rows repeat a prefab/guid pair another row already carries. private static int DuplicatePairs(List rows) => rows.Count - rows .Select(row => $"{row.PrefabName}\u001f{row.ModificationGuid}") .Distinct(StringComparer.OrdinalIgnoreCase) .Count(); [Fact] public void ShippedSkinsSurviveAMergeIntoTheShippedEquipmentCatalog() { var equipmentPath = Fixture("equipment.json"); var skinsPath = Fixture("equipment_skins.json"); if (!File.Exists(equipmentPath) || !File.Exists(skinsPath)) { // Both fixtures are optional here; the shipped files themselves are what matters. return; } var equipment = File.ReadAllText(equipmentPath); var merged = JsonSerializer.Deserialize>( EquipmentSkinCatalog.MergeInto(equipment, File.ReadAllText(skinsPath)))!; var before = JsonSerializer.Deserialize>(equipment)!; Assert.True(merged.Count >= before.Count); // The shipped equipment.json already repeats some prefab/guid pairs, so the invariant is // that merging a skin never adds another row for a pair the catalog already had. Assert.Equal(DuplicatePairs(before), DuplicatePairs(merged)); foreach (var skin in ShippedSkins()) { Assert.Contains(merged, row => string.Equals(row.PrefabName, skin.PrefabName, StringComparison.OrdinalIgnoreCase) && string.Equals(row.ModificationGuid, skin.ModificationGuid, StringComparison.OrdinalIgnoreCase)); } } }