character_stats: the full character frame, five seconds after login and every ten minutes

Fields, order and types as the original sent them. Values follow the
original's client library where it matters: race and gender are the player's
string properties, birth the creation time as a US date (explicit pattern, so
no newer-culture space quirk), burden the units over 150 times effective
strength truncated to a percent, capacity with the carrying augmentation, and
properties the player's integer properties minus the original's blacklist.
Attributes, vitals and skills carry unbuffed values, creation being the value
before experience was spent. Titles, the allegiance tree and luminance total
come through seams that the plugin API additions fill; until then they are
null, as the original sent them before the matching server message arrived.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-25 12:33:15 +02:00
parent 2e5d768ca9
commit 5b3179dd75
4 changed files with 346 additions and 0 deletions

View file

@ -0,0 +1,99 @@
using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.Backend;
using OpenAC.MosswartMassacre.Streams;
namespace OpenAC.MosswartMassacre.Tests.Streams;
/// <summary>The character_stats frame as the original's CharacterStats.CollectAndSend wrote it.</summary>
public sealed class CharacterStatsTests
{
private const string Stamp = "2026-09-25T08:30:15.1230000Z";
private static PluginHarness Arranged()
{
var harness = new PluginHarness("Mossy");
var c = harness.Host.Automation.Character;
c.Level = 126;
c.Attributes =
[
new PluginAttributeInfo(0, "Strength", 300u) { Base = 290u, Ranks = 190u },
new PluginAttributeInfo(1, "Endurance", 200u) { Base = 200u, Ranks = 100u },
];
c.Vitals = [new PluginVitalInfo(0, "Health", 300u, 341u) { Base = 331u }];
c.Skills.Add(new PluginSkillInfo(34u, "War Magic", PluginSkillTraining.Specialized, 540u) { Base = 533u });
c.Skills.Add(new PluginSkillInfo(6u, "Melee Defense", PluginSkillTraining.Trained, 400u) { Base = 388u });
harness.Host.Automation.Objects.Properties[0x50000001] = new PluginItemProperties(
new Dictionary<uint, int>
{
[5] = 9000, // burden units (blacklisted from properties)
[24] = 3, // skill credits (blacklisted)
[43] = 12, // deaths (blacklisted)
[98] = 1_000_000_000, // creation time (blacklisted)
[230] = 2, // carrying-capacity augmentations
[218] = 1, // an augmentation: kept
},
new Dictionary<uint, long> { [1] = 123_456_789_012L, [2] = 5_000L, [6] = 500_000L },
new Dictionary<uint, bool>(),
new Dictionary<uint, double>(),
new Dictionary<uint, string> { [3] = "Male", [4] = "Aluvian" },
new Dictionary<uint, uint>(),
new Dictionary<uint, uint>());
harness.Login();
harness.Frames.Clear();
return harness;
}
[Fact]
public void The_frame_keeps_the_originals_fields_order_and_types()
{
// CharacterStats.cs:252-383. burden = (int)((float)9000 / (300*150) * 100) = 20;
// capacity = 150*300 + 2*30*300 = 63000; birth = 1970 + 1e9 s in en-US.
using var harness = Arranged();
var stream = harness.Feature<CharacterStatsStream>();
stream.CurrentTitle = () => 42;
stream.Titles = () => ["Adventurer", "Sawato Resident"];
stream.Allegiance = () => new AllegianceSummary(
"Mossy Club", new AllegianceMember("King", 1, 8, 1), null, 3, 17);
string json = Wire.Serialize(stream.Build());
Assert.Equal(
"{\"type\":\"character_stats\",\"timestamp\":\"" + Stamp + "\",\"character_name\":\"Mossy\","
+ "\"level\":126,\"race\":\"Aluvian\",\"gender\":\"Male\",\"birth\":\"9/9/2001 1:46:40 AM\","
+ "\"total_xp\":123456789012,\"unassigned_xp\":5000,\"skill_credits\":3,\"deaths\":12,"
+ "\"burden\":20,\"burden_units\":9000,\"encumbrance_capacity\":63000,"
+ "\"luminance_earned\":500000,\"luminance_total\":null,\"current_title\":42,"
+ "\"attributes\":{\"strength\":{\"base\":290,\"creation\":100},\"endurance\":{\"base\":200,\"creation\":100}},"
+ "\"vitals\":{\"health\":{\"base\":331}},"
+ "\"skills\":{\"melee_defense\":{\"base\":388,\"training\":\"Trained\"},\"war_magic\":{\"base\":533,\"training\":\"Specialized\"}},"
+ "\"allegiance\":{\"name\":\"Mossy Club\",\"monarch\":{\"name\":\"King\",\"race\":1,\"rank\":8,\"gender\":1},\"patron\":null,\"rank\":3,\"followers\":17},"
+ "\"properties\":{\"230\":2,\"218\":1},"
+ "\"titles\":[\"Adventurer\",\"Sawato Resident\"]}",
json);
}
[Fact]
public void Unknown_title_and_allegiance_are_null()
{
using var harness = Arranged();
string json = Wire.Serialize(harness.Feature<CharacterStatsStream>().Build());
Assert.Contains("\"current_title\":null,", json, StringComparison.Ordinal);
Assert.Contains("\"allegiance\":null,", json, StringComparison.Ordinal);
Assert.EndsWith("\"titles\":null}", json, StringComparison.Ordinal);
}
[Fact]
public void Sent_five_seconds_after_login_then_every_ten_minutes()
{
using var harness = Arranged();
harness.Run(4.5, 0.5);
Assert.Empty(harness.FramesOfType("character_stats"));
harness.Run(0.6, 0.1);
Assert.Single(harness.FramesOfType("character_stats"));
harness.Run(600, 5);
Assert.Equal(2, harness.FramesOfType("character_stats").Count());
}
}