Files
CapySR/tests/CapySR.Tests/GameDataTests.cs
T

300 lines
9.5 KiB
C#
Raw Normal View History

2026-09-07 21:28:11 -07:00
using CapySR.Common;
using CapySR.Data;
using CapySR.Data.Excel;
using Xunit;
using Xunit.Abstractions;
namespace CapySR.Tests;
public class GameDataTests(ITestOutputHelper output)
{
private static GameData? TryLoad(ITestOutputHelper output)
{
var sources = new DataConfig().ResolvedSources.Where(Directory.Exists).ToList();
if (sources.Count == 0)
{
output.WriteLine("skipping: no game data sources found");
return null;
}
return GameData.Load(sources, output.WriteLine);
}
[Fact]
public void LoadsEveryTable()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
Assert.True(data.Avatars.Count > 80, $"only {data.Avatars.Count} avatars");
Assert.True(data.Stages.Count > 20_000, $"only {data.Stages.Count} stages");
Assert.NotEmpty(data.Monsters);
Assert.NotEmpty(data.Equipment);
}
2026-09-08 17:35:42 -07:00
// an empty banner leaves the client's gacha red dot throwing once a frame
[Fact]
public void ReadsStandardBannerCeiling()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
Assert.NotEmpty(data.GachaCeilingAvatars);
Assert.Contains(1003u, data.GachaCeilingAvatars);
}
2026-09-07 21:28:11 -07:00
[Fact]
public void ParsesMarch7th()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
var march = data.GetAvatar(1001);
Assert.NotNull(march);
Assert.Equal("Ice", march.DamageType);
Assert.Equal("Knight", march.AvatarBaseType);
Assert.Equal(120u, march.SpMax);
Assert.Equal(6, march.RankIDList.Count);
var tree = data.MaxedSkillTree(1001).ToList();
Assert.True(tree.Count > 10, $"only {tree.Count} trace points");
Assert.All(tree, p => Assert.True(p.Level > 0));
}
[Fact]
public void ParsesStageMonsterWaves()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
var stage = data.GetStage(1022010);
Assert.NotNull(stage);
Assert.Equal("Cocoon", stage.StageType);
Assert.Equal(16u, stage.Level);
var wave = Assert.Single(stage.MonsterList);
Assert.Equal([8001010, 8001020, 8001010, 8001020], wave);
// obfuscated key names, matched by the '_'-prefixed value
Assert.Equal("1", stage.StageConfigData["_Wave"]);
}
[Fact]
public void BetaOverlayAddsNewCharacters()
{
var data = TryLoad(output);
if (data is null || data.Sources.Count < 2)
{
return;
}
// 1503 Pearl ships in the 4.6 beta dump only, not in prod 4.5.0
var pearl = data.GetAvatar(1503);
Assert.NotNull(pearl);
Assert.Equal("Ice", pearl.DamageType);
Assert.Equal("Elation", pearl.AvatarBaseType);
Assert.True(data.MaxedSkillTree(1503).Count() >= 10);
// prod rows still present and not clobbered by the overlay
Assert.NotNull(data.GetAvatar(1001));
Assert.Equal(100101u, data.DefaultMazeBuffs[1001].BuffId);
}
// 101 is the Space Anchor; the Chap00 boxes are the first MP/HP orbs the game shows
[Fact]
public void PropsAreClassifiedByEntityPath()
{
var data = TryLoad(output);
if (data is null || data.Props.Count == 0)
{
return;
}
Assert.True(data.Props[101].IsSpring);
Assert.True(data.Props[100025].IsMpRecover);
Assert.False(data.Props[100025].IsHpRecover);
Assert.True(data.Props[100024].IsHpRecover);
Assert.True(data.Props[100026].IsDestructible);
Assert.False(data.Props[100026].IsMpRecover || data.Props[100026].IsHpRecover);
Assert.Contains(100026u, data.DestructiblePropIds);
Assert.DoesNotContain(101u, data.DestructiblePropIds);
}
[Fact]
public void OverworldEventsResolveToStages()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
// 30001011 is MOC floor 1's encounter; its event is also its stage
Assert.Equal(30001011u, data.ResolveStageForEvent(30001011, 6));
Assert.Equal(0u, data.ResolveStageForEvent(0, 6));
// a real overworld monster from the start scene resolves at every world level
var monster = data.GetScene(new GameServerConfig().StartEntryId)!.Monsters.First(m => m.EventId != 0);
for (var level = 0u; level <= 6; level++)
{
var stage = data.ResolveStageForEvent(monster.EventId, level);
Assert.NotEqual(0u, stage);
Assert.NotNull(data.GetStage(stage));
}
// calyx and farm rows fall back to the closest lower world level
Assert.NotNull(data.FindCocoon(1001, 6));
Assert.NotNull(data.FindCocoon(1001, 0));
Assert.Null(data.FindCocoon(999999, 6));
Assert.NotNull(data.FindFarmElement(1101, 6));
}
[Fact]
public void ChallengeTablesLoad()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
Assert.True(data.Challenges.Count > 600, $"only {data.Challenges.Count} challenge floors");
Assert.NotEmpty(data.MapEntrances);
Assert.NotEmpty(data.MazePlanes);
var kinds = data.Challenges.Values.GroupBy(c => c.Kind).ToDictionary(g => g.Key, g => g.Count());
foreach (var (kind, count) in kinds)
{
output.WriteLine($"{kind}: {count}");
}
Assert.All(Enum.GetValues<ChallengeKind>(), k => Assert.True(kinds.ContainsKey(k), $"no {k} floors"));
}
// dimbreath's ChallengePeakConfig has no MapEntranceID column at all; the beta dump does.
// whole-row merging would drop it and leave AA with nowhere to teleport to.
[Fact]
public void FieldMergeRecoversBetaOnlyColumns()
{
var data = TryLoad(output);
if (data is null || data.Sources.Count < 2)
{
return;
}
var playable = data.ChallengePeaks.Values.Where(p => p.IsPlayable).ToList();
output.WriteLine($"{playable.Count}/{data.ChallengePeaks.Count} AA stages have scene fields");
Assert.NotEmpty(playable);
// 101 exists in both sources; only the beta one carries MapEntranceID
var first = data.ChallengePeaks[101];
Assert.NotEqual(0u, first.MapEntranceID);
Assert.NotEmpty(first.NpcMonsterIDList);
Assert.NotEmpty(first.TagList);
}
[Fact]
public void EntranceResolvesToAPlane()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
var challenge = data.Challenges.Values.First(c => c.MapEntranceID > 0);
var resolved = data.ResolveEntrance(challenge.MapEntranceID);
Assert.NotNull(resolved);
Assert.NotEqual(0u, resolved.Value.Entrance.FloorID);
Assert.NotNull(resolved.Value.Plane);
Assert.NotEqual(0u, resolved.Value.Plane!.WorldID);
}
[Fact]
public void ResolvesTechniqueAndGlobalBuffs()
{
var data = TryLoad(output);
if (data is null)
{
return;
}
var march = data.DefaultMazeBuffs[1001];
Assert.Equal(100101u, march.BuffId);
Assert.Equal(2u, march.SkillIndex);
// castorice and silver wolf are the only two with a global buff
Assert.Equal(2, data.GlobalBuffs.Count);
Assert.Contains(data.GlobalBuffs, b => b.AvatarID == 1407);
Assert.Contains(data.GlobalBuffs, b => b.AvatarID == 1506);
}
}
public class PathSkillTreeTests(ITestOutputHelper output)
{
// AvatarPathData.point_id is the anchor index ("Point01" -> 1), not the raw point id.
// sending raw point ids gives the client something it cannot resolve.
[Fact]
public void AnchorIndexParsesFromAnchorType()
{
Assert.Equal(1u, new AvatarSkillTreeExcel { AnchorType = "Point01" }.AnchorIndex);
Assert.Equal(12u, new AvatarSkillTreeExcel { AnchorType = "Point12" }.AnchorIndex);
Assert.Equal(0u, new AvatarSkillTreeExcel { AnchorType = "" }.AnchorIndex);
Assert.Equal(0u, new AvatarSkillTreeExcel { AnchorType = "Point" }.AnchorIndex);
}
[Fact]
public void EveryPlayableAvatarHasAnchoredTracePoints()
{
var sources = new DataConfig().ResolvedSources.Where(Directory.Exists).ToList();
if (sources.Count == 0)
{
return;
}
var data = GameData.Load(sources);
var playable = data.Avatars.Keys.Where(id => id is (> 1000 and < 2000) or (>= 8001 and <= 8010)).ToList();
var missing = playable.Where(id => !data.MaxedPathSkillTree(id).Any()).ToList();
output.WriteLine($"{playable.Count} playable avatars, {missing.Count} without anchored trace points");
Assert.Empty(missing);
// anchors are small indices, never raw point ids
foreach (var (anchor, _) in data.MaxedPathSkillTree(1001))
{
Assert.InRange(anchor, 1u, 100u);
}
}
[Fact]
public void MultiPathAvatarsAreKnown()
{
var sources = new DataConfig().ResolvedSources.Where(Directory.Exists).ToList();
if (sources.Count == 0)
{
return;
}
var data = GameData.Load(sources);
Assert.Contains(8001u, data.MultiPathAvatarIds);
Assert.Contains(1001u, data.MultiPathAvatarIds);
Assert.Contains(1224u, data.MultiPathAvatarIds);
Assert.DoesNotContain(1003u, data.MultiPathAvatarIds);
}
}