feat(net): Map/House panel — slice 4a, House wire parsing groundwork
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>
This commit is contained in:
parent
6b0fa4ff0d
commit
dd6c7e09f0
6 changed files with 381 additions and 1 deletions
|
|
@ -124,7 +124,14 @@ public static class GameEventWiring
|
|||
Action<uint /*whoDeclined*/>? onTradeDecline = null,
|
||||
Action<uint /*whoReset*/>? onTradeReset = null,
|
||||
Action<GameEvents.TradeFailure>? onTradeFailure = null,
|
||||
Action? onTradeClearAcceptance = null)
|
||||
Action? onTradeClearAcceptance = null,
|
||||
// House panel (Batch C, Map/House toolbar panel, 2026-08-17): the
|
||||
// same Runtime-owned delegate-hole shape as trade above —
|
||||
// RuntimeHouseState (or a lighter equivalent) is the consumer.
|
||||
Action<GameEvents.HouseData>? onHouseData = null,
|
||||
Action<uint /*weenieError*/>? onHouseStatus = null,
|
||||
Action<uint /*rentTime*/>? onHouseUpdateRentTime = null,
|
||||
Action<IReadOnlyList<GameEvents.HousePayment>>? onHouseUpdateRentPayment = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
|
@ -402,6 +409,43 @@ public static class GameEventWiring
|
|||
onTradeClearAcceptance());
|
||||
}
|
||||
|
||||
// ── House panel (0x0225–0x0228) ───────────────────────────
|
||||
// Batch C (Map/House toolbar panel, 2026-08-17). gmHouseUI::
|
||||
// PostInit registers all four; consumers are optional so every
|
||||
// existing caller compiles unchanged.
|
||||
if (onHouseData is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.HouseData, e =>
|
||||
{
|
||||
var p = GameEvents.ParseHouseData(e.Payload.Span);
|
||||
if (p is not null) onHouseData(p.Value);
|
||||
});
|
||||
}
|
||||
if (onHouseStatus is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.HouseStatus, e =>
|
||||
{
|
||||
var p = GameEvents.ParseHouseStatus(e.Payload.Span);
|
||||
if (p is not null) onHouseStatus(p.Value);
|
||||
});
|
||||
}
|
||||
if (onHouseUpdateRentTime is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.UpdateRentTime, e =>
|
||||
{
|
||||
var p = GameEvents.ParseUpdateRentTime(e.Payload.Span);
|
||||
if (p is not null) onHouseUpdateRentTime(p.Value);
|
||||
});
|
||||
}
|
||||
if (onHouseUpdateRentPayment is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.UpdateRentPayment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseUpdateRentPayment(e.Payload.Span);
|
||||
if (p is not null) onHouseUpdateRentPayment(p);
|
||||
});
|
||||
}
|
||||
|
||||
if (onConfirmationRequest is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.CharacterConfirmationRequest, e =>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,17 @@ public static class ClientCommandRequests
|
|||
public const uint AddPlayerPermissionOpcode = 0x0219u;
|
||||
public const uint RemovePlayerPermissionOpcode = 0x021Au;
|
||||
public const uint AbandonHouseOpcode = 0x021Fu;
|
||||
// Batch C (Map/House toolbar panel, 2026-08-17): the query the House
|
||||
// tab needs to populate. ACE GameActionHouseQuery.cs: [GameAction(
|
||||
// GameActionType.HouseQuery)] (0x021E), Handle reads no payload and
|
||||
// calls session.Player.HandleActionQueryHouse() — which replies with
|
||||
// either GameEventHouseStatus (0x0226, no house owned) or
|
||||
// GameEventHouseData (0x0225, house owned). No known retail client
|
||||
// call site was found in this session's decomp reading (gmHouseUI::
|
||||
// PostInit never sends it) — HousePageController fires it when the
|
||||
// House tab becomes visible, an acdream convention, not a ported
|
||||
// retail trigger.
|
||||
public const uint HouseQueryOpcode = 0x021Eu;
|
||||
|
||||
// Named-retail anchors:
|
||||
// CM_Character::Event_TeleToMarketplace @ 0x006A1C20
|
||||
|
|
@ -282,6 +293,11 @@ public static class ClientCommandRequests
|
|||
public static byte[] BuildAbandonHouse(uint sequence) =>
|
||||
BuildParameterless(sequence, AbandonHouseOpcode);
|
||||
|
||||
// Queries the local player's house info (owned house data, or a
|
||||
// no-house status) — GameActionHouseQuery.Handle: no payload read.
|
||||
public static byte[] BuildHouseQuery(uint sequence) =>
|
||||
BuildParameterless(sequence, HouseQueryOpcode);
|
||||
|
||||
private static byte[] BuildParameterless(uint sequence, uint opcode)
|
||||
{
|
||||
byte[] body = new byte[12];
|
||||
|
|
|
|||
|
|
@ -1003,6 +1003,119 @@ public static class GameEvents
|
|||
Guests: guests));
|
||||
}
|
||||
|
||||
// ── House panel (Batch C, Map/House toolbar panel, 2026-08-17) ─────────
|
||||
// gmHouseUI::PostInit @0x004a2710 registers notice handlers for wire
|
||||
// opcodes 0x0225-0x0228; the recon doc (docs/research/2026-08-17-map-
|
||||
// house-recon.md) is the SSOT for the retail-side call sites and the
|
||||
// two ACE writer stubs (UpdateRentTime always writes 0u; UpdateRentPayment
|
||||
// always writes an empty list — captured verbatim below, not guessed).
|
||||
|
||||
/// <summary>One house purchase/maintenance line item. ACE
|
||||
/// HousePaymentExtensions.Write: Num(int) + Paid(int) + WeenieID(uint) +
|
||||
/// Name(String16L) + PluralName(String16L).</summary>
|
||||
public readonly record struct HousePayment(
|
||||
int Num, int Paid, uint WeenieID, string Name, string PluralName);
|
||||
|
||||
/// <summary>0x0225 HouseData: the owned-house panel snapshot. ACE
|
||||
/// HouseDataExtensions.Write: BuyTime(uint) + RentTime(uint) +
|
||||
/// Type(uint HouseType enum) + MaintenanceFree(uint bool) +
|
||||
/// Buy(List<HousePayment>) + Rent(List<HousePayment>) +
|
||||
/// Position (the same Cell+Pos.XYZ+Rotation.WXYZ 32-byte shape
|
||||
/// <see cref="CreateObject.ServerPosition"/> already parses
|
||||
/// elsewhere).</summary>
|
||||
public readonly record struct HouseData(
|
||||
uint BuyTime,
|
||||
uint RentTime,
|
||||
uint Type,
|
||||
bool MaintenanceFree,
|
||||
IReadOnlyList<HousePayment> Buy,
|
||||
IReadOnlyList<HousePayment> Rent,
|
||||
CreateObject.ServerPosition Position);
|
||||
|
||||
public static HouseData? ParseHouseData(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
int pos = 0;
|
||||
if (payload.Length - pos < 16) return null;
|
||||
uint buyTime = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint rentTime = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint type = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
bool maintenanceFree = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)) != 0; pos += 4;
|
||||
|
||||
List<HousePayment>? buy = ReadHousePaymentList(payload, ref pos);
|
||||
if (buy is null) return null;
|
||||
List<HousePayment>? rent = ReadHousePaymentList(payload, ref pos);
|
||||
if (rent is null) return null;
|
||||
|
||||
if (payload.Length - pos < 32) return null;
|
||||
var position = new CreateObject.ServerPosition(
|
||||
LandblockId: BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos + 0)),
|
||||
PositionX: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 4)),
|
||||
PositionY: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 8)),
|
||||
PositionZ: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 12)),
|
||||
RotationW: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 16)),
|
||||
RotationX: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 20)),
|
||||
RotationY: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 24)),
|
||||
RotationZ: BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos + 28)));
|
||||
|
||||
return new HouseData(buyTime, rentTime, type, maintenanceFree, buy, rent, position);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static List<HousePayment>? ReadHousePaymentList(ReadOnlySpan<byte> payload, ref int pos)
|
||||
{
|
||||
if (payload.Length - pos < 4) return null;
|
||||
uint count = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
var list = new List<HousePayment>((int)Math.Min(count, 4096));
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
if (payload.Length - pos < 8) return null;
|
||||
int num = BinaryPrimitives.ReadInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
int paid = BinaryPrimitives.ReadInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
if (payload.Length - pos < 4) return null;
|
||||
uint weenieId = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
string name = ReadString16L(payload, ref pos);
|
||||
string pluralName = ReadString16L(payload, ref pos);
|
||||
list.Add(new HousePayment(num, paid, weenieId, name, pluralName));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>0x0226 HouseStatus: a single WeenieError u32 — retail's
|
||||
/// <c>RecvNotice_FailedHouseTransaction</c> family (also the "no house
|
||||
/// owned" reply to a HouseQuery — ACE Player_House.cs
|
||||
/// HandleActionQueryHouse's <c>new GameEventHouseStatus(Session)</c>
|
||||
/// defaults to WeenieError.None, not a "failure").</summary>
|
||||
public static uint? ParseHouseStatus(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0227 UpdateRentTime: a single uint (when the current
|
||||
/// maintenance period began, Unix timestamp). ACE
|
||||
/// GameEventHouseUpdateRentTime.cs is a STUB that always writes
|
||||
/// <c>0u</c> — captured here for completeness, not exercised by any
|
||||
/// live ACE install today.</summary>
|
||||
public static uint? ParseUpdateRentTime(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>0x0228 UpdateRentPayment: a List<HousePayment> (the
|
||||
/// rent items and how much of each has been paid this period). ACE
|
||||
/// GameEventHouseUpdateRentPayment.cs is a STUB that always writes an
|
||||
/// EMPTY list — captured here for completeness, not exercised by any
|
||||
/// live ACE install today.</summary>
|
||||
public static IReadOnlyList<HousePayment>? ParseUpdateRentPayment(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
return ReadHousePaymentList(payload, ref pos);
|
||||
}
|
||||
|
||||
// ── Shared string reader (matches LoginRequest.ReadString16L) ───────────
|
||||
|
||||
private static string ReadString16L(ReadOnlySpan<byte> source, ref int pos)
|
||||
|
|
|
|||
|
|
@ -2517,6 +2517,15 @@ public sealed class WorldSession : IDisposable
|
|||
SendGameAction(ClientCommandRequests.BuildMansionRecall(seq));
|
||||
}
|
||||
|
||||
/// <summary>Query the local player's house info — either owned house
|
||||
/// data (0x0225) or a no-house status (0x0226) comes back
|
||||
/// (0x021E).</summary>
|
||||
public void SendHouseQuery()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildHouseQuery(seq));
|
||||
}
|
||||
|
||||
/// <summary>Query the local character's played time (0x01C2).</summary>
|
||||
public void SendQueryAge()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1771,6 +1771,48 @@ public sealed class GameEventWiringTests
|
|||
Assert.True(observed.Value.IsLoggedIn);
|
||||
}
|
||||
|
||||
/// <summary>Batch C (Map/House toolbar panel, 2026-08-17): all four
|
||||
/// house wire opcodes (0x0225-0x0228) reach their registered
|
||||
/// callbacks.</summary>
|
||||
[Fact]
|
||||
public void WireAll_HouseFamily_ReachesTheirCallbacks()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
GameEvents.HouseData? data = null;
|
||||
uint? status = null;
|
||||
uint? rentTime = null;
|
||||
IReadOnlyList<GameEvents.HousePayment>? rentPayment = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onHouseData: d => data = d,
|
||||
onHouseStatus: code => status = code,
|
||||
onHouseUpdateRentTime: t => rentTime = t,
|
||||
onHouseUpdateRentPayment: p => rentPayment = p);
|
||||
|
||||
byte[] houseDataWire = new AceWireWriter()
|
||||
.Write(0u).Write(0u).Write(0u).Write(0u)
|
||||
.Write(0).Write(0)
|
||||
.Write(0x00120001u)
|
||||
.Write(0f).Write(0f).Write(0f)
|
||||
.Write(1f).Write(0f).Write(0f).Write(0f)
|
||||
.ToArray();
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.HouseData, houseDataWire))!.Value);
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(WrapEnvelope(
|
||||
GameEventType.HouseStatus, new AceWireWriter().Write(0u).ToArray()))!.Value);
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(WrapEnvelope(
|
||||
GameEventType.UpdateRentTime, new AceWireWriter().Write(1_700_000_000u).ToArray()))!.Value);
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(WrapEnvelope(
|
||||
GameEventType.UpdateRentPayment, new AceWireWriter().Write(0).ToArray()))!.Value);
|
||||
|
||||
Assert.NotNull(data);
|
||||
Assert.Equal(0x00120001u, data!.Value.Position.LandblockId);
|
||||
Assert.Equal(0u, status);
|
||||
Assert.Equal(1_700_000_000u, rentTime);
|
||||
Assert.NotNull(rentPayment);
|
||||
Assert.Empty(rentPayment);
|
||||
}
|
||||
|
||||
private static byte[] BuildEnchantment(
|
||||
ushort spellId,
|
||||
ushort layer,
|
||||
|
|
|
|||
156
tests/AcDream.Core.Net.Tests/Messages/HouseEventsTests.cs
Normal file
156
tests/AcDream.Core.Net.Tests/Messages/HouseEventsTests.cs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue