diff --git a/src/AcDream.Core.Net/Messages/CharacterActions.cs b/src/AcDream.Core.Net/Messages/CharacterActions.cs
index 4abbbc39..96a86670 100644
--- a/src/AcDream.Core.Net/Messages/CharacterActions.cs
+++ b/src/AcDream.Core.Net/Messages/CharacterActions.cs
@@ -16,9 +16,9 @@ public static class CharacterActions
{
public const uint GameActionEnvelope = 0xF7B1u;
- public const uint RaiseAttributeOpcode = 0x0045u; // u32 attr, u64 xpSpent
- public const uint RaiseVitalOpcode = 0x0044u; // u32 vital, u64 xpSpent
- public const uint RaiseSkillOpcode = 0x0046u; // u32 skillId, u64 xpSpent
+ public const uint RaiseAttributeOpcode = 0x0045u; // u32 attr, u32 xpSpent
+ public const uint RaiseVitalOpcode = 0x0044u; // u32 vital, u32 xpSpent
+ public const uint RaiseSkillOpcode = 0x0046u; // u32 skillId, u32 xpSpent
public const uint TrainSkillOpcode = 0x0047u; // u32 skillId, u32 credits
public const uint ChangeCombatModeOpcode = 0x0053u; // u32 combatMode
@@ -74,14 +74,31 @@ public static class CharacterActions
return body;
}
+ ///
+ /// Envelope + sequence + sub-opcode + id + 32-bit xpSpent.
+ ///
+ /// The xpSpent field is a dword on the wire, not a qword. ACE's
+ /// GameAction/Actions/GameActionRaiseAttribute.cs (and its Vital and
+ /// Skill siblings) read message.Payload.ReadUInt32(), and
+ /// holtburger's RaiseAttributeData declares xp_spent: u32
+ /// and advances the offset by four. We were writing eight, making the
+ /// message 24 bytes where the server expects 20 and leaving four bytes of
+ /// tail the server never reads.
+ ///
+ /// The parameter stays ulong because the cost originates from
+ /// 64-bit server XP tables several layers up; narrowing that chain end to
+ /// end is a separate change. No value is lost here: a cost that does not
+ /// fit in a dword was never expressible on this wire in the first
+ /// place.
+ ///
private static byte[] BuildAttrOrVital(uint seq, uint sub, uint id, ulong xp)
{
- byte[] body = new byte[24];
+ byte[] body = new byte[20];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), sub);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), id);
- BinaryPrimitives.WriteUInt64LittleEndian(body.AsSpan(16), xp);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), (uint)xp);
return body;
}
}
diff --git a/tests/AcDream.Core.Net.Tests/Messages/CharacterActionsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CharacterActionsTests.cs
index 94194619..065f411f 100644
--- a/tests/AcDream.Core.Net.Tests/Messages/CharacterActionsTests.cs
+++ b/tests/AcDream.Core.Net.Tests/Messages/CharacterActionsTests.cs
@@ -7,17 +7,40 @@ namespace AcDream.Core.Net.Tests.Messages;
public sealed class CharacterActionsTests
{
+ ///
+ /// xpSpent is a dword, not a qword: ACE's GameActionRaiseAttribute reads
+ /// ReadUInt32() and holtburger's RaiseAttributeData declares
+ /// xp_spent: u32. This test previously asserted the 24-byte,
+ /// 64-bit shape — note that its sibling BuildTrainSkill_U32CreditsNotU64
+ /// had already been corrected to 20 bytes for the same class of mistake.
+ ///
[Fact]
- public void BuildRaiseAttribute_HasOpcode0x0045AndXp64()
+ public void BuildRaiseAttribute_HasOpcode0x0045AndU32Xp()
{
byte[] body = CharacterActions.BuildRaiseAttribute(seq: 1, attrId: 5, xpSpent: 12345678);
- Assert.Equal(24, body.Length);
+ Assert.Equal(20, body.Length);
Assert.Equal(CharacterActions.RaiseAttributeOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
Assert.Equal(5u,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
Assert.Equal(12345678u,
- BinaryPrimitives.ReadUInt64LittleEndian(body.AsSpan(16)));
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
+ }
+
+ [Theory]
+ [InlineData(0u)]
+ [InlineData(1u)]
+ [InlineData(12345678u)]
+ [InlineData(uint.MaxValue)]
+ public void RaiseVitalAndSkill_AlsoWriteU32Xp(uint xp)
+ {
+ byte[] vital = CharacterActions.BuildRaiseVital(seq: 1, vitalId: 1, xpSpent: xp);
+ byte[] skill = CharacterActions.BuildRaiseSkill(seq: 1, skillId: 8, xpSpent: xp);
+
+ Assert.Equal(20, vital.Length);
+ Assert.Equal(20, skill.Length);
+ Assert.Equal(xp, BinaryPrimitives.ReadUInt32LittleEndian(vital.AsSpan(16)));
+ Assert.Equal(xp, BinaryPrimitives.ReadUInt32LittleEndian(skill.AsSpan(16)));
}
[Fact]