using System.Buffers.Binary;
using System.Text;
using AcDream.Core.Net.Messages;
namespace AcDream.Core.Net.Tests.Messages;
///
/// Campaign CC CC2: byte-exact coverage for the outbound CharacterCreate
/// (0xF656) builder — field order, the 55-slot skill-advancement invariant,
/// and the trailing checksum's exact retail accumulation set (see
/// 's class doc comment for the decompiled
/// source of truth).
///
public sealed class CharacterCreateTests
{
private static uint[] MakeSkills(uint seed = 0)
{
var skills = new uint[CharacterCreate.SkillAdvancementClassCount];
for (int i = 0; i < skills.Length; i++)
skills[i] = seed + (uint)i;
return skills;
}
private static CharacterCreate.Request MakeRequest() => new(
Heritage: 1u,
Gender: 0u,
Appearance: new CharacterCreate.Appearance(
EyesStrip: 2u,
NoseStrip: 3u,
MouthStrip: 4u,
HairColor: 5u,
EyeColor: 6u,
HairStyle: 7u,
HeadgearStyle: 8u,
HeadgearColor: 9u,
ShirtStyle: 10u,
ShirtColor: 11u,
TrousersStyle: 12u,
TrousersColor: 13u,
FootwearStyle: 14u,
FootwearColor: 15u,
SkinShade: 0.1,
HairShade: 0.2,
HeadgearShade: 0.3,
ShirtShade: 0.4,
TrousersShade: 0.5,
FootwearShade: 0.6),
Template: 16u,
Attributes: new CharacterCreate.Attributes(
Strength: 17u,
Endurance: 18u,
Coordination: 19u,
Quickness: 20u,
Focus: 21u,
Self: 22u),
Slot: 0u,
ClassId: 1u,
Name: "Testcdream",
StartArea: 23u,
IsAdmin: false,
IsEnvoy: false);
[Fact]
public void BuildRequestBody_Layout_MatchesRetailCGPackFieldOrder()
{
CharacterCreate.Request request = MakeRequest();
uint[] skills = MakeSkills();
byte[] body = CharacterCreate.BuildRequestBody("testaccount", request, skills);
int pos = 0;
uint ReadU32()
{
uint v = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos));
pos += 4;
return v;
}
double ReadF64()
{
double v = BinaryPrimitives.ReadDoubleLittleEndian(body.AsSpan(pos));
pos += 8;
return v;
}
string ReadString16L()
{
ushort len = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(pos));
pos += 2;
string s = Encoding.ASCII.GetString(body, pos, len);
pos += len;
int recordSize = 2 + len;
int padding = (4 - (recordSize & 3)) & 3;
pos += padding;
return s;
}
Assert.Equal(CharacterCreate.Opcode, ReadU32());
Assert.Equal("testaccount", ReadString16L());
Assert.Equal(1u, ReadU32()); // CG_Pack@0x005c7208 constant
Assert.Equal(request.Heritage, ReadU32());
Assert.Equal(request.Gender, ReadU32());
Assert.Equal(request.Appearance.EyesStrip, ReadU32());
Assert.Equal(request.Appearance.NoseStrip, ReadU32());
Assert.Equal(request.Appearance.MouthStrip, ReadU32());
Assert.Equal(request.Appearance.HairColor, ReadU32());
Assert.Equal(request.Appearance.EyeColor, ReadU32());
Assert.Equal(request.Appearance.HairStyle, ReadU32());
Assert.Equal(request.Appearance.HeadgearStyle, ReadU32());
Assert.Equal(request.Appearance.HeadgearColor, ReadU32());
Assert.Equal(request.Appearance.ShirtStyle, ReadU32());
Assert.Equal(request.Appearance.ShirtColor, ReadU32());
Assert.Equal(request.Appearance.TrousersStyle, ReadU32());
Assert.Equal(request.Appearance.TrousersColor, ReadU32());
Assert.Equal(request.Appearance.FootwearStyle, ReadU32());
Assert.Equal(request.Appearance.FootwearColor, ReadU32());
Assert.Equal(request.Appearance.SkinShade, ReadF64());
Assert.Equal(request.Appearance.HairShade, ReadF64());
Assert.Equal(request.Appearance.HeadgearShade, ReadF64());
Assert.Equal(request.Appearance.ShirtShade, ReadF64());
Assert.Equal(request.Appearance.TrousersShade, ReadF64());
Assert.Equal(request.Appearance.FootwearShade, ReadF64());
Assert.Equal(request.Template, ReadU32());
Assert.Equal(request.Attributes.Strength, ReadU32());
Assert.Equal(request.Attributes.Endurance, ReadU32());
Assert.Equal(request.Attributes.Coordination, ReadU32());
Assert.Equal(request.Attributes.Quickness, ReadU32());
Assert.Equal(request.Attributes.Focus, ReadU32());
Assert.Equal(request.Attributes.Self, ReadU32());
Assert.Equal(request.Slot, ReadU32());
Assert.Equal(request.ClassId, ReadU32());
uint numSkills = ReadU32();
Assert.Equal((uint)CharacterCreate.SkillAdvancementClassCount, numSkills);
for (int i = 0; i < skills.Length; i++)
Assert.Equal(skills[i], ReadU32());
Assert.Equal(request.Name, ReadString16L());
Assert.Equal(request.StartArea, ReadU32());
Assert.Equal(0u, ReadU32()); // isAdmin
Assert.Equal(0u, ReadU32()); // isEnvoy
uint checksum = ReadU32();
Assert.Equal(CharacterCreate.ComputeChecksum(request), checksum);
Assert.Equal(pos, body.Length);
}
[Fact]
public void BuildRequestBody_ExactByteSequence_ShortAccountAndName()
{
// Minimal fixture with distinct short strings, hand-checked padding.
CharacterCreate.Request request = new(
Heritage: 1u,
Gender: 0u,
Appearance: default,
Template: 0u,
Attributes: default,
Slot: 0u,
ClassId: 1u,
Name: "ab",
StartArea: 0u,
IsAdmin: false,
IsEnvoy: false);
uint[] skills = new uint[CharacterCreate.SkillAdvancementClassCount];
byte[] body = CharacterCreate.BuildRequestBody("cd", request, skills);
int pos = 0;
Assert.Equal(CharacterCreate.Opcode, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4;
// String16L("cd") = u16(2) + 2 bytes, already 4-byte aligned.
Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(pos))); pos += 2;
Assert.Equal("cd", Encoding.ASCII.GetString(body, pos, 2)); pos += 2;
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4; // constant
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4; // heritage
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4; // gender
// 14 appearance strip/color u32 fields, all zero (default).
pos += 14 * 4;
// 6 f64 shades, all zero (default).
pos += 6 * 8;
pos += 4; // template
pos += 6 * 4; // attributes
pos += 4; // slot
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4; // classId
Assert.Equal(
(uint)CharacterCreate.SkillAdvancementClassCount,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos)));
pos += 4;
pos += CharacterCreate.SkillAdvancementClassCount * 4;
Assert.Equal(2, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(pos))); pos += 2;
Assert.Equal("ab", Encoding.ASCII.GetString(body, pos, 2)); pos += 2;
pos += 4; // startArea
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4; // isAdmin
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4; // isEnvoy
// Checksum: heritage(1) + gender(0) + 3 strips(0) + hairColor(0) +
// eyeColor(0) + hairStyle(0) + headgearStyle(0) + shirtStyle(0) +
// trousersStyle(0) + footwearStyle(0) + template(0) + 6 attrs(0) = 1.
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos))); pos += 4;
Assert.Equal(pos, body.Length);
}
[Fact]
public void ComputeChecksum_ExactRetailAccumulationSet()
{
// CG_Pack@0x005c7213-0x005c74c3: heritage, gender, the three
// appearance strips, hairColor, eyeColor, hairStyle, headgearStyle,
// shirtStyle, trousersStyle, footwearStyle, template, and the six
// attributes — nineteen terms, u32 wraparound addition.
CharacterCreate.Request request = MakeRequest();
uint expected = unchecked(
request.Heritage
+ request.Gender
+ request.Appearance.EyesStrip
+ request.Appearance.NoseStrip
+ request.Appearance.MouthStrip
+ request.Appearance.HairColor
+ request.Appearance.EyeColor
+ request.Appearance.HairStyle
+ request.Appearance.HeadgearStyle
+ request.Appearance.ShirtStyle
+ request.Appearance.TrousersStyle
+ request.Appearance.FootwearStyle
+ request.Template
+ request.Attributes.Strength
+ request.Attributes.Endurance
+ request.Attributes.Coordination
+ request.Attributes.Quickness
+ request.Attributes.Focus
+ request.Attributes.Self);
Assert.Equal(expected, CharacterCreate.ComputeChecksum(request));
// Concretely: 1+0+2+3+4+5+6+7+8+10+12+14+16+17+18+19+20+21+22 = 205.
Assert.Equal(205u, expected);
}
[Fact]
public void ComputeChecksum_ExcludesColorFieldsShadesSlotAndClassId()
{
// These fields sit adjacent to summed fields on the wire but the
// decompiled CG_Pack accumulation (0x005c7213-0x005c74c3) never
// touches them — mutating only these must not move the checksum.
CharacterCreate.Request baseline = MakeRequest();
uint baselineChecksum = CharacterCreate.ComputeChecksum(baseline);
CharacterCreate.Request mutated = baseline with
{
Appearance = baseline.Appearance with
{
HeadgearColor = baseline.Appearance.HeadgearColor + 1000u,
ShirtColor = baseline.Appearance.ShirtColor + 1000u,
TrousersColor = baseline.Appearance.TrousersColor + 1000u,
FootwearColor = baseline.Appearance.FootwearColor + 1000u,
SkinShade = baseline.Appearance.SkinShade + 5.0,
HairShade = baseline.Appearance.HairShade + 5.0,
},
Slot = baseline.Slot + 7u,
ClassId = baseline.ClassId + 7u,
};
Assert.Equal(baselineChecksum, CharacterCreate.ComputeChecksum(mutated));
}
[Fact]
public void BuildRequestBody_SkillCountOtherThan55_Throws()
{
CharacterCreate.Request request = MakeRequest();
Assert.Throws(() =>
CharacterCreate.BuildRequestBody("testaccount", request, MakeSkills().AsSpan(0, 54)));
Assert.Throws(() =>
CharacterCreate.BuildRequestBody("testaccount", request, new uint[56]));
Assert.Throws(() =>
CharacterCreate.BuildRequestBody("testaccount", request, ReadOnlySpan.Empty));
}
[Fact]
public void BuildRequestBody_NullAccountName_Throws()
{
CharacterCreate.Request request = MakeRequest();
Assert.Throws(() =>
CharacterCreate.BuildRequestBody(null!, request, MakeSkills()));
}
[Fact]
public void BuildRequestBody_NullCharacterName_Throws()
{
CharacterCreate.Request request = MakeRequest() with { Name = null! };
Assert.Throws(() =>
CharacterCreate.BuildRequestBody("testaccount", request, MakeSkills()));
}
[Fact]
public void BuildRequestBody_AdminAndEnvoyFlags_EncodeAsOneOrZero()
{
CharacterCreate.Request request = MakeRequest() with { IsAdmin = true, IsEnvoy = true };
byte[] body = CharacterCreate.BuildRequestBody("testaccount", request, MakeSkills());
// isAdmin and isEnvoy are the two u32s immediately before the
// trailing checksum.
uint isEnvoy = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(body.Length - 8));
uint isAdmin = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(body.Length - 12));
Assert.Equal(1u, isAdmin);
Assert.Equal(1u, isEnvoy);
}
}