mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
175 lines
8.1 KiB
C#
175 lines
8.1 KiB
C#
using System.Text.Json;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// The curated level-up pool: the deny-list that must never be overridden, and the shipped
|
|
/// allow-list that decides what the box may contain.
|
|
/// </summary>
|
|
public class LevelUpRewardPoolTests
|
|
{
|
|
[Theory]
|
|
// The brief's named exclusions, as they appear in the 2023 catalogue.
|
|
[InlineData("White Hat", "A hat given to the ethical hackers of Rec Room", null)] // bug bounty / security
|
|
[InlineData("Top Hat (Gold)", "", null)] // payment milestone
|
|
[InlineData("Rec Con Hoodie (2021)", "", null)] // convention reward
|
|
[InlineData("Winged Headphones (Purple)", "RecCon 2021 exclusive", null)]
|
|
[InlineData("Early Access Tee", "", null)] // launch exclusive
|
|
[InlineData("Laser Tag Alpha Shirt", "", null)] // alpha reward
|
|
[InlineData("MrBeast T-Shirt", "", null)] // licensed collaboration
|
|
[InlineData("Space Visor (Orion)", "Unlocked during Xbox launch", null)]
|
|
[InlineData("Royal Cape (Creators Contest 2)", "", null)] // contest reward
|
|
[InlineData("SFVRCC Promo Shirt", "", null)] // promotional
|
|
[InlineData("Notes Headphones (Gold)", "", null)] // payment milestone
|
|
[InlineData("Class of 2020 Shirt", "", null)] // educational programme
|
|
[InlineData("New Years Hat Hat", "", null)] // event dressing
|
|
[InlineData("Plain Tee", "", "halloween")] // event tag alone
|
|
[InlineData("Plain Tee", "", null, true)] // control: this one is fine
|
|
public void TheDenyListRefusesTheItemsTheBriefNames(string name, string tooltip, string? tag, bool allowed = false)
|
|
{
|
|
var denied = LevelUpRewardPool.IsAlwaysDenied(name, tooltip, tag);
|
|
Assert.Equal(!allowed, denied);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheDenyListIsCaseInsensitiveAndChecksTheTooltipAsWellAsTheName()
|
|
{
|
|
Assert.True(LevelUpRewardPool.IsAlwaysDenied("Halloween Sweater", ""));
|
|
Assert.True(LevelUpRewardPool.IsAlwaysDenied("Sweater", "HALLOWEEN SWEATER"));
|
|
Assert.False(LevelUpRewardPool.IsAlwaysDenied("Stripey Sweater", "A cosy sweater"));
|
|
}
|
|
|
|
[Fact]
|
|
public void OwnershipKeysMatchRegardlessOfSpacingOrCase()
|
|
{
|
|
Assert.Equal(
|
|
LevelUpRewardPool.OwnershipKey("AAAA-BBBB,CCCC,"),
|
|
LevelUpRewardPool.OwnershipKey("aaaa-bbbb,cccc,"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnEmptyPoolAwardsNothingButDoesNotThrow()
|
|
{
|
|
var pool = new LevelUpRewardPool.Pool();
|
|
var catalogue = new List<AvatarItem>
|
|
{
|
|
new() { AvatarItemDesc = "x", FriendlyName = "Hat", Rarity = (int)Rarity.Uncommon }
|
|
};
|
|
|
|
Assert.Null(LevelUpRewardPool.PickAvatarItem(catalogue, Rarity.Uncommon, new HashSet<string>(), new Random(1), pool));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------
|
|
// The shipped artefacts
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Walks up from the test's own output directory to the repository, so this can check the real
|
|
/// files rather than a copy. Returns null when the tree is not there (a published test run).
|
|
/// </summary>
|
|
private static string? RepositoryRoot()
|
|
{
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (directory != null)
|
|
{
|
|
if (File.Exists(Path.Join(directory.FullName, "Server", "RadicadeServer-Config", LevelUpRewardPool.ConfigFileName)))
|
|
return directory.FullName;
|
|
directory = directory.Parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private sealed class PoolFile
|
|
{
|
|
public List<string> AvatarItems { get; set; } = new();
|
|
public List<string> Consumables { get; set; } = new();
|
|
}
|
|
|
|
[Fact]
|
|
public void TheShippedPoolNamesOnlyOrdinaryCatalogueItemsFromThe2023Client()
|
|
{
|
|
var root = RepositoryRoot();
|
|
if (root == null) return;
|
|
|
|
var config = Path.Join(root, "Server", "RadicadeServer-Config");
|
|
var pool = JsonSerializer.Deserialize<PoolFile>(
|
|
File.ReadAllText(Path.Join(config, LevelUpRewardPool.ConfigFileName)),
|
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
|
var catalogue = JsonSerializer.Deserialize<List<AvatarItem>>(File.ReadAllText(Path.Join(config, "2022-avatar.json")))!;
|
|
var resolvable = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var lookup = JsonDocument.Parse(File.ReadAllText(Path.Join(config, "avatar-item-lookup.json"))))
|
|
foreach (var description in lookup.RootElement.GetProperty("descriptions").EnumerateArray())
|
|
resolvable.Add(description.GetString() ?? "");
|
|
|
|
var byDesc = catalogue
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc))
|
|
.GroupBy(item => item.AvatarItemDesc, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
Assert.NotEmpty(pool.AvatarItems);
|
|
Assert.NotEmpty(pool.Consumables);
|
|
|
|
var denied = new List<string>();
|
|
var missing = new List<string>();
|
|
var notResolvable = new List<string>();
|
|
var wrongTier = new List<string>();
|
|
var baseItems = new List<string>();
|
|
|
|
foreach (var desc in pool.AvatarItems)
|
|
{
|
|
if (!byDesc.TryGetValue(desc, out var item)) { missing.Add(desc); continue; }
|
|
if (LevelUpRewardPool.IsAlwaysDenied(item)) denied.Add($"{item.FriendlyName} ({desc})");
|
|
if (!resolvable.Contains(desc)) notResolvable.Add($"{item.FriendlyName} ({desc})");
|
|
if (item.Rarity is not ((int)Rarity.Uncommon or (int)Rarity.Rare or (int)Rarity.Epic or (int)Rarity.Legendary))
|
|
wrongTier.Add($"{item.FriendlyName} rarity={item.Rarity}");
|
|
if (item.IsBaseAvatarItem) baseItems.Add(item.FriendlyName);
|
|
}
|
|
|
|
// Each of these is a way the generated list could have gone wrong, and each is a hard
|
|
// failure rather than a warning: the pool decides what a real player receives.
|
|
Assert.Empty(denied);
|
|
Assert.Empty(missing);
|
|
Assert.Empty(notResolvable);
|
|
Assert.Empty(wrongTier);
|
|
Assert.Empty(baseItems);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryStarTierHasSomethingToGive()
|
|
{
|
|
var root = RepositoryRoot();
|
|
if (root == null) return;
|
|
|
|
var config = Path.Join(root, "Server", "RadicadeServer-Config");
|
|
var file = JsonSerializer.Deserialize<PoolFile>(
|
|
File.ReadAllText(Path.Join(config, LevelUpRewardPool.ConfigFileName)),
|
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
|
var catalogue = JsonSerializer.Deserialize<List<AvatarItem>>(File.ReadAllText(Path.Join(config, "2022-avatar.json")))!;
|
|
|
|
// Selection is driven by the pool as an object here rather than through the file loader,
|
|
// which would need the process's own working directory pointed at the repository.
|
|
var pool = new LevelUpRewardPool.Pool
|
|
{
|
|
AvatarItems = file.AvatarItems,
|
|
Consumables = file.Consumables
|
|
};
|
|
|
|
foreach (var rarity in new[] { Rarity.Uncommon, Rarity.Rare, Rarity.Epic, Rarity.Legendary })
|
|
{
|
|
var pick = LevelUpRewardPool.PickAvatarItem(catalogue, rarity, new HashSet<string>(), new Random(1), pool);
|
|
Assert.NotNull(pick);
|
|
Assert.Equal((int)rarity, pick!.Rarity);
|
|
}
|
|
|
|
var consumable = LevelUpRewardPool.PickConsumable(
|
|
JsonSerializer.Deserialize<List<ConsumableItem>>(
|
|
File.ReadAllText(Path.Join(config, "consumables.json")))!,
|
|
new HashSet<string>(), new Random(1), pool);
|
|
Assert.NotNull(consumable);
|
|
}
|
|
}
|