Adds the outbound HouseQuery action (0x021E, ClientCommandRequests. BuildHouseQuery / WorldSession.SendHouseQuery — ACE GameActionHouseQuery. Handle reads no payload) and inbound parsers for all four House wire opcodes GameEventType already defined (0x0225-0x0228, gmHouseUI::PostInit's registered notice handlers): GameEvents.ParseHouseData (BuyTime/RentTime/ Type/MaintenanceFree/Buy list/Rent list/Position — the Position field reuses CreateObject.ServerPosition's existing 32-byte Cell+Pos.XYZ+ Rotation.WXYZ shape rather than a new type), ParseHouseStatus (WeenieError u32), ParseUpdateRentTime, ParseUpdateRentPayment. Wire shapes verified against ACE's HouseDataExtensions/HousePaymentExtensions (references/ACE/ Source/ACE.Server/Network/Structure/HouseData.cs, HousePayment.cs) — noted that ACE's own UpdateRentTime/UpdateRentPayment writers are stubs (always 0u / always an empty list), captured as such rather than assumed live. GameEventWiring.WireAll gets four new optional delegate holes (onHouseData/onHouseStatus/onHouseUpdateRentTime/onHouseUpdateRentPayment) following the exact trade-family precedent — registered only when non-null, every existing caller compiles unchanged. This is the "enum/parser groundwork" half of Slice 4's pre-authorized fallback. NOT included (filed as an ISSUES entry): a RuntimeHouseState GameRuntime owner (construction-transaction ceremony, fault-injection points, disposal/convergence tracking — the same weight as RuntimeTradeState's integration, judged disproportionate for tonight alongside the completed Map tab), HousePageController's real Lines/ OnShown wiring, the DisplayPurchaseTimeText port, and the six other Display* line builders. The House tab currently mounts with genuinely empty content, matching retail's own PostInit (verified via MapHousePanelSlotProbeTests' live-DAT probe, not assumed). 9 new HouseEventsTests (parser round-trips + truncation), 1 new GameEventWiringTests case (all four opcodes reach their callbacks). Core.Net.Tests: 1004/1004 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
156 lines
5.5 KiB
C#
156 lines
5.5 KiB
C#
using AcDream.Core.Net.Messages;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
/// <summary>
|
|
/// Batch C (overnight hover/UI round, Map/House toolbar panel, 2026-08-17):
|
|
/// golden-byte coverage for the House panel's four inbound events
|
|
/// (0x0225-0x0228, gmHouseUI::PostInit's registered notice handlers) and the
|
|
/// outbound HouseQuery action (0x021E). Wire shapes cross-checked against
|
|
/// ACE's HouseDataExtensions/HousePaymentExtensions
|
|
/// (references/ACE/Source/ACE.Server/Network/Structure/HouseData.cs,
|
|
/// HousePayment.cs) — see docs/research/2026-08-17-map-house-recon.md.
|
|
/// </summary>
|
|
public sealed class HouseEventsTests
|
|
{
|
|
[Fact]
|
|
public void BuildHouseQuery_WritesEnvelopeSequenceOpcodeOnly()
|
|
{
|
|
byte[] body = ClientCommandRequests.BuildHouseQuery(9);
|
|
|
|
Assert.Equal(12, body.Length);
|
|
Assert.Equal(ClientCommandRequests.HouseQueryOpcode,
|
|
System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHouseStatus_ReadsWeenieError()
|
|
{
|
|
byte[] wire = new AceWireWriter().Write(0u).ToArray();
|
|
|
|
Assert.Equal(0u, GameEvents.ParseHouseStatus(wire));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHouseStatus_TruncatedPayload_ReturnsNull()
|
|
{
|
|
Assert.Null(GameEvents.ParseHouseStatus(System.Array.Empty<byte>()));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseUpdateRentTime_ReadsTimestamp()
|
|
{
|
|
// The real ACE writer always sends 0u (a stub) — parsed at whatever
|
|
// value arrives, not hardcoded to that stub's output.
|
|
byte[] wire = new AceWireWriter().Write(1_700_000_000u).ToArray();
|
|
|
|
Assert.Equal(1_700_000_000u, GameEvents.ParseUpdateRentTime(wire));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseUpdateRentPayment_EmptyList_RoundTrips()
|
|
{
|
|
// The real ACE writer always sends an empty list (a stub).
|
|
byte[] wire = new AceWireWriter().Write(0).ToArray();
|
|
|
|
var payments = GameEvents.ParseUpdateRentPayment(wire);
|
|
|
|
Assert.NotNull(payments);
|
|
Assert.Empty(payments);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseUpdateRentPayment_OneEntry_RoundTrips()
|
|
{
|
|
byte[] wire = new AceWireWriter()
|
|
.Write(1)
|
|
.Write(400) // Num
|
|
.Write(150) // Paid
|
|
.Write(273u) // WeenieID (pyreal)
|
|
.WriteString16L("Pyreal")
|
|
.WriteString16L("Pyreals")
|
|
.ToArray();
|
|
|
|
var payments = GameEvents.ParseUpdateRentPayment(wire);
|
|
|
|
Assert.NotNull(payments);
|
|
GameEvents.HousePayment payment = Assert.Single(payments);
|
|
Assert.Equal(400, payment.Num);
|
|
Assert.Equal(150, payment.Paid);
|
|
Assert.Equal(273u, payment.WeenieID);
|
|
Assert.Equal("Pyreal", payment.Name);
|
|
Assert.Equal("Pyreals", payment.PluralName);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHouseData_NoHouseOwned_EmptyListsAndZeroFields()
|
|
{
|
|
byte[] wire = new AceWireWriter()
|
|
.Write(0u) // BuyTime
|
|
.Write(0u) // RentTime
|
|
.Write(0u) // Type (Undef)
|
|
.Write(0u) // MaintenanceFree
|
|
.Write(0) // Buy.Count
|
|
.Write(0) // Rent.Count
|
|
// Position: Cell + Pos.XYZ + Rotation.WXYZ
|
|
.Write(0x00120001u)
|
|
.Write(10f).Write(20f).Write(30f)
|
|
.Write(1f).Write(0f).Write(0f).Write(0f)
|
|
.ToArray();
|
|
|
|
GameEvents.HouseData? data = GameEvents.ParseHouseData(wire);
|
|
|
|
Assert.NotNull(data);
|
|
Assert.Equal(0u, data!.Value.BuyTime);
|
|
Assert.Empty(data.Value.Buy);
|
|
Assert.Empty(data.Value.Rent);
|
|
Assert.Equal(0x00120001u, data.Value.Position.LandblockId);
|
|
Assert.Equal(10f, data.Value.Position.PositionX);
|
|
Assert.Equal(30f, data.Value.Position.PositionZ);
|
|
Assert.Equal(1f, data.Value.Position.RotationW);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHouseData_OwnedHouse_ReadsBuyAndRentLists()
|
|
{
|
|
byte[] wire = new AceWireWriter()
|
|
.Write(1_650_000_000u) // BuyTime
|
|
.Write(1_699_000_000u) // RentTime
|
|
.Write(1u) // Type (Cottage)
|
|
.Write(0u) // MaintenanceFree = false
|
|
.Write(1) // Buy.Count
|
|
.Write(1).Write(1).Write(273u)
|
|
.WriteString16L("Pyreal").WriteString16L("Pyreals")
|
|
.Write(2) // Rent.Count
|
|
.Write(300).Write(300).Write(273u)
|
|
.WriteString16L("Pyreal").WriteString16L("Pyreals")
|
|
.Write(1).Write(0).Write(1049u)
|
|
.WriteString16L("Writ of the Chosen").WriteString16L("Writs of the Chosen")
|
|
// Position
|
|
.Write(0x00340002u)
|
|
.Write(-15f).Write(45f).Write(0f)
|
|
.Write(0.7071f).Write(0f).Write(0f).Write(0.7071f)
|
|
.ToArray();
|
|
|
|
GameEvents.HouseData? data = GameEvents.ParseHouseData(wire);
|
|
|
|
Assert.NotNull(data);
|
|
Assert.Equal(1_650_000_000u, data!.Value.BuyTime);
|
|
Assert.Equal(1_699_000_000u, data.Value.RentTime);
|
|
Assert.Equal(1u, data.Value.Type);
|
|
Assert.False(data.Value.MaintenanceFree);
|
|
Assert.Single(data.Value.Buy);
|
|
Assert.Equal(2, data.Value.Rent.Count);
|
|
Assert.Equal("Writ of the Chosen", data.Value.Rent[1].Name);
|
|
Assert.Equal(0x00340002u, data.Value.Position.LandblockId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHouseData_TruncatedPayload_ReturnsNull()
|
|
{
|
|
byte[] wire = new AceWireWriter().Write(0u).Write(0u).ToArray();
|
|
|
|
Assert.Null(GameEvents.ParseHouseData(wire));
|
|
}
|
|
}
|