fix(ui): port live experience quality updates (#217)
Parse retail PrivateUpdatePropertyInt64 and route authoritative Total/Available Experience through both local-player projections so Attributes, the level meter, and Skills refresh together. Preserve the existing retail XP curve and right-align the Total XP value. Close the user-confirmed item-give gate for #216 and record the named-retail/ACE/holtburger conformance evidence. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
30d294506c
commit
84b7d2d7cd
15 changed files with 391 additions and 29 deletions
|
|
@ -99,6 +99,23 @@ public sealed class CharacterSheetProviderTests
|
|||
Assert.Equal(90L, sheet.AttributeRaise10Costs[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSheet_AfterLiveInt64Updates_RefreshesBothXpWindowsAndMeter()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.AddPlayerObject(unassignedXp: 0L);
|
||||
|
||||
Assert.True(h.Table.UpdateInt64Property(PlayerGuid, 1u, 200L));
|
||||
Assert.True(h.Table.UpdateInt64Property(PlayerGuid, 2u, 75L));
|
||||
|
||||
var sheet = h.Provider.BuildSheet();
|
||||
|
||||
Assert.Equal(200L, sheet.TotalXp);
|
||||
Assert.Equal(75L, sheet.UnassignedXp);
|
||||
Assert.Equal(50L, sheet.XpToNextLevel);
|
||||
Assert.Equal(2f / 3f, sheet.XpFraction, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSheet_Skills_MapsAdvancementAndCurveCosts()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1366,6 +1366,19 @@ public class CharacterStatControllerTests
|
|||
Assert.False(lbl.RightAligned, "TotalXpLabel must be left-justified (RightAligned=false)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_TotalXpValue_IsRightAligned()
|
||||
{
|
||||
var value = new UiText { Centered = true };
|
||||
var layout = Fake((CharacterStatController.TotalXpId, value));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal((1_250_000_000L).ToString("N0"), value.LinesProvider()[0].Text);
|
||||
Assert.False(value.Centered);
|
||||
Assert.True(value.RightAligned);
|
||||
}
|
||||
|
||||
// ── Polish Commit 1: name white, Infinity!, white footer title ─────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Golden layout from retail
|
||||
/// <c>CM_Qualities::DispatchUI_PrivateUpdateInt64 @ 0x006AEAD0</c>,
|
||||
/// cross-checked against ACE and holtburger.
|
||||
/// </summary>
|
||||
public sealed class PrivateUpdatePropertyInt64Tests
|
||||
{
|
||||
private static byte[] Build(
|
||||
uint property,
|
||||
long value,
|
||||
byte sequence = 1,
|
||||
uint opcode = PrivateUpdatePropertyInt64.Opcode)
|
||||
{
|
||||
var body = new byte[PrivateUpdatePropertyInt64.BodySize];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, opcode);
|
||||
body[4] = sequence;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(5), property);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(body.AsSpan(9), value);
|
||||
return body;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1u, 1_234_567_890L)]
|
||||
[InlineData(2u, 987_654_321L)]
|
||||
[InlineData(6u, -1L)]
|
||||
public void TryParse_RetailBody_ReturnsSignedPropertyValue(uint property, long value)
|
||||
{
|
||||
var parsed = PrivateUpdatePropertyInt64.TryParse(Build(property, value, sequence: 0x7B));
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(property, parsed.Value.Property);
|
||||
Assert.Equal(value, parsed.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_WrongOpcode_ReturnsNull()
|
||||
=> Assert.Null(PrivateUpdatePropertyInt64.TryParse(
|
||||
Build(1u, 100L, opcode: 0x02D0u)));
|
||||
|
||||
[Fact]
|
||||
public void TryParse_Truncated_ReturnsNull()
|
||||
=> Assert.Null(PrivateUpdatePropertyInt64.TryParse(
|
||||
new byte[PrivateUpdatePropertyInt64.BodySize - 1]));
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Player;
|
||||
|
||||
namespace AcDream.Core.Net.Tests;
|
||||
|
||||
|
|
@ -116,6 +117,37 @@ public sealed class ObjectTableWiringTests
|
|||
Assert.Equal(0x50000001u, d.PetOwnerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyPlayerInt64PropertyUpdate_SynchronizesBothPlayerProjections()
|
||||
{
|
||||
const uint playerGuid = 0x50000001u;
|
||||
var table = new ClientObjectTable();
|
||||
var playerObject = new ClientObject { ObjectId = playerGuid };
|
||||
playerObject.Properties.Int64s[2u] = 0L;
|
||||
table.AddOrUpdate(playerObject);
|
||||
|
||||
var localPlayer = new LocalPlayerState();
|
||||
var loginProperties = new PropertyBundle();
|
||||
loginProperties.Int64s[2u] = 0L;
|
||||
localPlayer.OnProperties(loginProperties);
|
||||
|
||||
int tableChanges = 0;
|
||||
int playerChanges = 0;
|
||||
table.ObjectUpdated += _ => tableChanges++;
|
||||
localPlayer.CharacterChanged += () => playerChanges++;
|
||||
|
||||
ObjectTableWiring.ApplyPlayerInt64PropertyUpdate(
|
||||
table,
|
||||
localPlayer,
|
||||
playerGuid,
|
||||
new WorldSession.PlayerInt64PropertyUpdate(2u, 75_000L));
|
||||
|
||||
Assert.Equal(75_000L, table.Get(playerGuid)!.Properties.GetInt64(2u));
|
||||
Assert.Equal(75_000L, localPlayer.Properties.GetInt64(2u));
|
||||
Assert.Equal(1, tableChanges);
|
||||
Assert.Equal(1, playerChanges);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// The handler that ObjectTableWiring.Wire subscribes to session.EntityDeleted.
|
||||
// Testing it directly avoids needing a live WorldSession (UDP socket).
|
||||
|
|
|
|||
|
|
@ -90,6 +90,31 @@ public sealed class PlayerDescriptionParserTests
|
|||
Assert.Empty(p.Value.Attributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_LoginInt64Table_PopulatesTotalAndAvailableExperience()
|
||||
{
|
||||
// ACE GameEventPlayerDescription: flags + weenie type, then the gated
|
||||
// Int64 hash-table (u16 count/u16 buckets + key/i64 pairs), then vector header.
|
||||
byte[] body = new byte[44];
|
||||
int pos = 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(pos), 0x0080u); pos += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(pos), 1u); pos += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(pos), 2); pos += 2;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(pos), 64); pos += 2;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(pos), 1u); pos += 4;
|
||||
BinaryPrimitives.WriteInt64LittleEndian(body.AsSpan(pos), 1_234_567_890L); pos += 8;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(pos), 2u); pos += 4;
|
||||
BinaryPrimitives.WriteInt64LittleEndian(body.AsSpan(pos), 75_000L); pos += 8;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(pos), 0u); pos += 4;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(pos), 0u);
|
||||
|
||||
var parsed = PlayerDescriptionParser.TryParse(body);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(1_234_567_890L, parsed.Value.Properties.GetInt64(1u));
|
||||
Assert.Equal(75_000L, parsed.Value.Properties.GetInt64(2u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_AttributeBlock_PopulatesAllNineEntries()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -262,6 +262,22 @@ public sealed class LocalPlayerStateTests
|
|||
Assert.Equal(1_234_567_890L, s.Properties.GetInt64(1u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnInt64PropertyUpdate_ReplacesValueAndFiresCharacterChanged()
|
||||
{
|
||||
var s = new LocalPlayerState();
|
||||
var props = new PropertyBundle();
|
||||
props.Int64s[1u] = 100L;
|
||||
s.OnProperties(props);
|
||||
int changed = 0;
|
||||
s.CharacterChanged += () => changed++;
|
||||
|
||||
s.OnInt64PropertyUpdate(1u, 1_234_567_890L);
|
||||
|
||||
Assert.Equal(1_234_567_890L, s.Properties.GetInt64(1u));
|
||||
Assert.Equal(1, changed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnSkillUpdate_StoresFormulaAdjustedCurrent()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue