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));
}
}
}