mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The loader prefers the fuller 2022-avatar.json and keeps avatar.json as a fallback, and it
|
||||
/// only backfills metadata when the chosen catalog is missing it. These tests pin that
|
||||
/// preference order so the served avatar list cannot silently fall back.
|
||||
/// </summary>
|
||||
public class AvatarCatalogSourceTests
|
||||
{
|
||||
[Fact]
|
||||
public void NewerCatalogIsPreferredOverTheFallback()
|
||||
{
|
||||
Assert.Equal("2022-avatar.json", DBConfig.AvatarCatalogFileNames[0]);
|
||||
Assert.Contains("avatar.json", DBConfig.AvatarCatalogFileNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CandidatePathsTryThePreferredFileInEveryDirectoryFirst()
|
||||
{
|
||||
var paths = DBConfig.AvatarCatalogCandidatePaths().ToArray();
|
||||
|
||||
Assert.NotEmpty(paths);
|
||||
var firstFallback = Array.FindIndex(paths, path => path.EndsWith("avatar.json", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.EndsWith("2022-avatar.json", StringComparison.OrdinalIgnoreCase));
|
||||
var lastPreferred = Array.FindLastIndex(paths, path => path.EndsWith("2022-avatar.json", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
Assert.True(lastPreferred >= 0, "2022-avatar.json should be among the candidate paths.");
|
||||
Assert.True(firstFallback > lastPreferred,
|
||||
"every 2022-avatar.json location must be tried before any avatar.json location.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteCatalogNeedsNoEnrichment()
|
||||
{
|
||||
var catalogJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json"));
|
||||
var items = AvatarUnlockCatalog.ParseCatalog(catalogJson);
|
||||
|
||||
// The compact avatar.json carries no item ids, so it is enriched from merged-avatar.json.
|
||||
Assert.True(AvatarUnlockCatalog.NeedsMetadataEnrichment(items));
|
||||
Assert.False(AvatarUnlockCatalog.NeedsMetadataEnrichment(null));
|
||||
Assert.False(AvatarUnlockCatalog.NeedsMetadataEnrichment(Array.Empty<AvatarItem>()));
|
||||
Assert.False(AvatarUnlockCatalog.NeedsMetadataEnrichment(
|
||||
[new AvatarItem { AvatarItemDesc = "item,", AvatarItemId = 12, ThumbnailImage = "a.png" }]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvableFilterDropsOnlyUnknownItems()
|
||||
{
|
||||
var original = DBConfig.ResolvableAvatarItemDescs;
|
||||
try
|
||||
{
|
||||
DBConfig.ResolvableAvatarItemDescs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"8aa79563-ace1-4ba7-ad0c-f3210a78142f,"
|
||||
};
|
||||
|
||||
var kept = AvatarUnlockCatalog.KeepResolvableItems(
|
||||
[
|
||||
new AvatarItem { AvatarItemDesc = "8aa79563-ace1-4ba7-ad0c-f3210a78142f,,," },
|
||||
new AvatarItem { AvatarItemDesc = "not-in-the-client-database,,," }
|
||||
]);
|
||||
|
||||
var item = Assert.Single(kept);
|
||||
Assert.Equal("8aa79563-ace1-4ba7-ad0c-f3210a78142f,", item.AvatarItemDesc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DBConfig.ResolvableAvatarItemDescs = original;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvableFilterIsANoOpWithoutALookup()
|
||||
{
|
||||
var original = DBConfig.ResolvableAvatarItemDescs;
|
||||
try
|
||||
{
|
||||
DBConfig.ResolvableAvatarItemDescs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var kept = AvatarUnlockCatalog.KeepResolvableItems(
|
||||
[new AvatarItem { AvatarItemDesc = "anything," }]);
|
||||
|
||||
Assert.Equal("anything,", Assert.Single(kept).AvatarItemDesc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DBConfig.ResolvableAvatarItemDescs = original;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The client only ships assets for items released before its build, so a newer row makes it
|
||||
/// log "Unknown Material guid" and throw "Bad outfit string. No prefab found!". Rain Cloud
|
||||
/// Hat (2023-04-07) is the item that actually crashed a boot in the 20230301 build.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ItemsReleasedAfterTheClientBuildAreNotServed()
|
||||
{
|
||||
const string rainCloudHat = "24781418-38ad-411f-925e-2321fe616b51,";
|
||||
const string turkeySweater = "002a0f2f-1a24-4439-b578-470818ef8325,";
|
||||
|
||||
var kept = AvatarUnlockCatalog.KeepClientAvailableItems(
|
||||
[
|
||||
new AvatarItem { AvatarItemDesc = rainCloudHat, CreatedAt = new DateTime(2023, 4, 7, 18, 16, 3, DateTimeKind.Utc) },
|
||||
new AvatarItem { AvatarItemDesc = turkeySweater, CreatedAt = new DateTime(2022, 11, 1, 17, 51, 50, DateTimeKind.Utc) },
|
||||
new AvatarItem { AvatarItemDesc = "no-release-date," },
|
||||
new AvatarItem { AvatarItemDesc = "offsetless,", CreatedAt = new DateTime(2023, 4, 7, 18, 16, 3, DateTimeKind.Unspecified) }
|
||||
]);
|
||||
|
||||
Assert.DoesNotContain(kept, item => item.AvatarItemDesc == rainCloudHat);
|
||||
Assert.DoesNotContain(kept, item => item.AvatarItemDesc == "offsetless,");
|
||||
Assert.Contains(kept, item => item.AvatarItemDesc == turkeySweater);
|
||||
Assert.Contains(kept, item => item.AvatarItemDesc == "no-release-date,");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseCutoffIsTheClientBuildDate()
|
||||
{
|
||||
Assert.Equal(new DateTime(2023, 3, 1, 0, 0, 0, DateTimeKind.Utc), DBConfig.AvatarItemReleaseCutoffUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShippedLookupCoversTheCatalogServedToClients()
|
||||
{
|
||||
var lookupPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar-item-lookup.json");
|
||||
var catalogPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json");
|
||||
if (!File.Exists(lookupPath) || !File.Exists(catalogPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var descriptions = System.Text.Json.JsonDocument.Parse(File.ReadAllText(lookupPath))
|
||||
.RootElement.GetProperty("descriptions")
|
||||
.EnumerateArray()
|
||||
.Select(element => element.GetString()!)
|
||||
.Where(description => !string.IsNullOrWhiteSpace(description))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Assert.True(descriptions.Count > 2000, $"lookup only lists {descriptions.Count} descriptions");
|
||||
|
||||
var original = DBConfig.ResolvableAvatarItemDescs;
|
||||
var originalCatalog = DBConfig.DefaultAvatarCatalog;
|
||||
var originalFallback = DBConfig.DefaultAvatarUnlocks;
|
||||
try
|
||||
{
|
||||
DBConfig.ResolvableAvatarItemDescs = descriptions;
|
||||
DBConfig.DefaultAvatarCatalog = File.ReadAllText(catalogPath);
|
||||
DBConfig.DefaultAvatarUnlocks = "[]";
|
||||
|
||||
var served = AvatarUnlockCatalog.GetSafeDefaultItems();
|
||||
|
||||
Assert.NotEmpty(served);
|
||||
Assert.Equal(2990, descriptions.Count);
|
||||
Assert.Equal(AvatarUnlockCatalog.ParseCatalog(DBConfig.DefaultAvatarCatalog).Count, served.Count);
|
||||
Assert.DoesNotContain(served, item => !descriptions.Contains(item.AvatarItemDesc));
|
||||
Assert.DoesNotContain(served, item => AvatarItemDescription.HasEmptyMaterialSlot(item.AvatarItemDesc));
|
||||
Assert.DoesNotContain(served, item =>
|
||||
item.CreatedAt != default && item.CreatedAt.ToUniversalTime() >= DBConfig.AvatarItemReleaseCutoffUtc);
|
||||
// This item is present in the September client, unlike the old March lookup.
|
||||
Assert.Contains(served, item =>
|
||||
item.AvatarItemDesc.StartsWith("24781418-38ad-411f-925e-2321fe616b51", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DBConfig.ResolvableAvatarItemDescs = original;
|
||||
DBConfig.DefaultAvatarCatalog = originalCatalog;
|
||||
DBConfig.DefaultAvatarUnlocks = originalFallback;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShippedCatalogHasUniqueServerIdsAndRetainsKnownMetadata()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "2022-avatar.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
// The fixture is optional: without it the loader simply falls back to avatar.json.
|
||||
return;
|
||||
}
|
||||
|
||||
var items = AvatarUnlockCatalog.ParseCatalog(File.ReadAllText(path));
|
||||
|
||||
Assert.NotEmpty(items);
|
||||
Assert.DoesNotContain(items, item => AvatarItemDescription.HasEmptyMaterialSlot(item.AvatarItemDesc));
|
||||
Assert.DoesNotContain(items, item => item.AvatarItemId <= 0);
|
||||
Assert.Equal(items.Count, items.Select(item => item.AvatarItemId).Distinct().Count());
|
||||
Assert.DoesNotContain(items, item => string.IsNullOrWhiteSpace(item.FriendlyName));
|
||||
Assert.Contains(items, item => item.FriendlyName == "NFL Houston Texans Hat"
|
||||
&& !string.IsNullOrWhiteSpace(item.ThumbnailImage));
|
||||
// New client-only entries have no known store thumbnail; do not invent image URLs.
|
||||
Assert.True(AvatarUnlockCatalog.NeedsMetadataEnrichment(items));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user