feat: secure trade with other players - wire, RuntimeTradeState, the
authored gmSecureTradeUI window, and both retail open paths
Three-lane research first (docs/research/2026-08-14-trade-lane{A,B,C}):
retail gmSecureTradeUI decode, the byte-exact ACE/decomp/holtburger
three-way wire agreement, and the acdream seam map (which found both
open paths ALREADY classified by the ported policy - OpenSecureTrade on
Use-a-player, StartSecureTrade on drag-item-onto-player with the
DragItemOnPlayerOpensSecureTrade option - dead-ending at a stub toast).
- Core.Net: TradeRequests builders (0x1F6-0x204, retail's CM_Trade
senders byte-checked against ACE's readers; the ACE-discarded
AcceptTrade echo carries zero-count item lists - AD-94), corrected +
completed inbound parsers (0x1FD-0x208; the old AddToTrade parser
missed the SIDE dword, TradeFailure missed the reason), delegate-hole
registrars, six WorldSession sends. 10 golden-byte tests.
- Runtime: RuntimeTradeState, the third sibling J-owner (fellowship/
allegiance shape): session-scoped, clears at generation reset (new
stage Trade=14), staged teardown stage 11 (Identity/EntityObjects
shift 12/13, TeardownStageCount 14 - the FA2-era per-stage-flag test
caught the mapping exactly as designed), combined ownership ledger,
event routing with ACE's wrong-initiator RegisterTrade landmine
honored (partner = whichever guid is not mine). 7 conformance tests.
- App: SecureTradeUiController binds the dedicated authored LayoutDesc
0x2100000D (root 0x1000007A - gmSecureTradeUI::PostInit's exact ids):
partner name/status/count/grid, the authored 'Trade' accept toggle
(accept <-> decline withdraw), 'Clear All' (ACE clears BOTH sides -
surfaced honestly), the X close, drop-on-your-grid staging, per-mode
accept cues (partner icon's authored Highlight state + Trade button
Selected latch). Mounted via the vendor recipe (nine-slice chrome,
hidden until RegisterTrade). ItemInteractionController's two policy
arms now raise SecureTradeRequested instead of the stub toast; the
drag path queues the dragged item until the window registers
(ClientTradeSystem::AttemptToTradeItem @0x0056DF80's shape).
Register: AD-94 (accept-echo zero-count lists), AD-95 (numeric-only
count texts pending template verification).
Suites: App 4,990/3, Core.Net 905, Runtime 1,626 - all green. The
panel itself is user-gate acceptance (two-client connected trade), the
#372-class lesson: fixture-green alone is not acceptance for a mount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bee38b0746
commit
067cbea8a5
26 changed files with 2682 additions and 42 deletions
|
|
@ -37,8 +37,10 @@ public enum GameRuntimeTeardownStage
|
|||
// Campaign FA slice FA2 (2026-08-12): the two sibling J-owners.
|
||||
FellowshipDisposed = 1 << 9,
|
||||
AllegianceDisposed = 1 << 10,
|
||||
IdentityDisposed = 1 << 11,
|
||||
EntityObjectsDisposed = 1 << 12,
|
||||
// Secure trade (2026-08-14): third sibling J-owner, same shape.
|
||||
TradeDisposed = 1 << 11,
|
||||
IdentityDisposed = 1 << 12,
|
||||
EntityObjectsDisposed = 1 << 13,
|
||||
Complete =
|
||||
HostLeasesReleased
|
||||
| EventsDetached
|
||||
|
|
@ -51,6 +53,7 @@ public enum GameRuntimeTeardownStage
|
|||
| CommunicationDisposed
|
||||
| FellowshipDisposed
|
||||
| AllegianceDisposed
|
||||
| TradeDisposed
|
||||
| IdentityDisposed
|
||||
| EntityObjectsDisposed,
|
||||
}
|
||||
|
|
@ -94,6 +97,7 @@ internal enum GameRuntimeConstructionPoint
|
|||
CommunicationCreated,
|
||||
FellowshipCreated,
|
||||
AllegianceCreated,
|
||||
TradeCreated,
|
||||
MovementCreated,
|
||||
ActionsCreated,
|
||||
EnvironmentCreated,
|
||||
|
|
@ -111,6 +115,7 @@ internal sealed class GameRuntimeConstructionContext
|
|||
public RuntimeCommunicationState? Communication { get; set; }
|
||||
public RuntimeFellowshipState? Fellowship { get; set; }
|
||||
public RuntimeAllegianceState? Allegiance { get; set; }
|
||||
public RuntimeTradeState? Trade { get; set; }
|
||||
public RuntimeLocalPlayerMovementState? Movement { get; set; }
|
||||
public RuntimeActionState? Actions { get; set; }
|
||||
public GameRuntimeEventHub? Events { get; set; }
|
||||
|
|
@ -126,7 +131,7 @@ public sealed class GameRuntime
|
|||
IRuntimeEventSource,
|
||||
IDisposable
|
||||
{
|
||||
private const int TeardownStageCount = 13;
|
||||
private const int TeardownStageCount = 14;
|
||||
|
||||
private readonly object _lifetimeGate = new();
|
||||
private readonly Dictionary<long, string> _hostLeases = [];
|
||||
|
|
@ -248,6 +253,16 @@ public sealed class GameRuntime
|
|||
context,
|
||||
faultInjection);
|
||||
|
||||
// Secure trade (2026-08-14): third sibling J-owner —
|
||||
// session-scoped like fellowship (a disconnect closes the trade
|
||||
// server-side), clears at every generation reset.
|
||||
context.Trade = new RuntimeTradeState();
|
||||
construction.Own(context.Trade);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.TradeCreated,
|
||||
context,
|
||||
faultInjection);
|
||||
|
||||
context.Movement = new RuntimeLocalPlayerMovementState();
|
||||
// Campaign CH slice CH2: local jump refusals (CommenceJump/
|
||||
// DoJump's WeenieError family — research doc §4.2/§6.4) reach
|
||||
|
|
@ -302,7 +317,8 @@ public sealed class GameRuntime
|
|||
context.Character,
|
||||
context.PlayerIdentity,
|
||||
context.Fellowship,
|
||||
context.Allegiance);
|
||||
context.Allegiance,
|
||||
context.Trade);
|
||||
|
||||
context.Movement.AttachPhysicsPublication(
|
||||
new RuntimeLocalPlayerPhysicsPublicationState(
|
||||
|
|
@ -357,6 +373,7 @@ public sealed class GameRuntime
|
|||
CommunicationOwner = context.Communication;
|
||||
FellowshipOwner = context.Fellowship;
|
||||
AllegianceOwner = context.Allegiance;
|
||||
TradeOwner = context.Trade;
|
||||
MovementOwner = context.Movement;
|
||||
ActionOwner = context.Actions;
|
||||
EnvironmentOwner = environment;
|
||||
|
|
@ -462,6 +479,9 @@ public sealed class GameRuntime
|
|||
public RuntimeCommunicationState CommunicationOwner { get; }
|
||||
public RuntimeFellowshipState FellowshipOwner { get; }
|
||||
public RuntimeAllegianceState AllegianceOwner { get; }
|
||||
|
||||
/// <summary>Secure trade (2026-08-14): third sibling J-owner.</summary>
|
||||
public RuntimeTradeState TradeOwner { get; }
|
||||
public RuntimeActionState ActionOwner { get; }
|
||||
public RuntimeLocalPlayerMovementState MovementOwner { get; }
|
||||
internal RuntimeLocalPlayerPhysicsPublicationState
|
||||
|
|
@ -507,6 +527,8 @@ public sealed class GameRuntime
|
|||
public IRuntimeChatView Chat => CommunicationOwner.View;
|
||||
public IRuntimeFellowshipView Fellowship => FellowshipOwner.View;
|
||||
public IRuntimeAllegianceView Allegiance => AllegianceOwner.View;
|
||||
|
||||
public IRuntimeTradeView Trade => TradeOwner.View;
|
||||
public IRuntimeActionView Actions => ActionOwner.View;
|
||||
public IRuntimeMovementView Movement => MovementOwner.View;
|
||||
public IRuntimeWorldEnvironmentView Environment => EnvironmentOwner;
|
||||
|
|
@ -607,7 +629,8 @@ public sealed class GameRuntime
|
|||
ActionOwner,
|
||||
MovementOwner,
|
||||
FellowshipOwner,
|
||||
AllegianceOwner),
|
||||
AllegianceOwner,
|
||||
TradeOwner),
|
||||
EnvironmentOwner.CaptureOwnership(),
|
||||
TransitOwner.CaptureOwnership(),
|
||||
GenerationReset.CaptureSnapshot(),
|
||||
|
|
@ -733,16 +756,22 @@ public sealed class GameRuntime
|
|||
9 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.FellowshipDisposed
|
||||
& ~GameRuntimeTeardownStage.AllegianceDisposed
|
||||
& ~GameRuntimeTeardownStage.TradeDisposed
|
||||
& ~GameRuntimeTeardownStage.IdentityDisposed
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
10 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.AllegianceDisposed
|
||||
& ~GameRuntimeTeardownStage.TradeDisposed
|
||||
& ~GameRuntimeTeardownStage.IdentityDisposed
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
11 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.TradeDisposed
|
||||
& ~GameRuntimeTeardownStage.IdentityDisposed
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
12 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.IdentityDisposed
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
13 => GameRuntimeTeardownStage.Complete
|
||||
& ~GameRuntimeTeardownStage.EntityObjectsDisposed,
|
||||
_ => GameRuntimeTeardownStage.Complete,
|
||||
};
|
||||
|
|
@ -794,9 +823,12 @@ public sealed class GameRuntime
|
|||
AllegianceOwner.Dispose();
|
||||
return AllegianceOwner.CaptureOwnership().IsConverged;
|
||||
case 11:
|
||||
TradeOwner.Dispose();
|
||||
return TradeOwner.CaptureOwnership().IsConverged;
|
||||
case 12:
|
||||
PlayerIdentity.Dispose();
|
||||
return PlayerIdentity.CaptureOwnership().IsConverged;
|
||||
case 12:
|
||||
case 13:
|
||||
EntityObjects.Dispose();
|
||||
return EntityObjects.CaptureOwnership().IsConverged
|
||||
&& EntityObjects.Physics.CaptureOwnership().IsConverged;
|
||||
|
|
@ -819,8 +851,9 @@ public sealed class GameRuntime
|
|||
8 => CommunicationOwner.CaptureOwnership().IsConverged,
|
||||
9 => FellowshipOwner.CaptureOwnership().IsConverged,
|
||||
10 => AllegianceOwner.CaptureOwnership().IsConverged,
|
||||
11 => PlayerIdentity.CaptureOwnership().IsConverged,
|
||||
12 => EntityObjects.CaptureOwnership().IsConverged
|
||||
11 => TradeOwner.CaptureOwnership().IsConverged,
|
||||
12 => PlayerIdentity.CaptureOwnership().IsConverged,
|
||||
13 => EntityObjects.CaptureOwnership().IsConverged
|
||||
&& EntityObjects.Physics.CaptureOwnership().IsConverged,
|
||||
_ => true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ public readonly record struct RuntimeGameplayOwnershipSnapshot(
|
|||
RuntimeCommunicationOwnershipSnapshot Communication,
|
||||
RuntimeActionOwnershipSnapshot Actions,
|
||||
RuntimeLocalMovementOwnershipSnapshot Movement,
|
||||
// Campaign FA slice FA2 (2026-08-12): the two sibling J-owners.
|
||||
// Campaign FA slice FA2 (2026-08-12): the two sibling J-owners;
|
||||
// secure trade (2026-08-14) is the third.
|
||||
RuntimeFellowshipOwnershipSnapshot Fellowship,
|
||||
RuntimeAllegianceOwnershipSnapshot Allegiance)
|
||||
RuntimeAllegianceOwnershipSnapshot Allegiance,
|
||||
RuntimeTradeOwnershipSnapshot Trade)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
Inventory.IsConverged
|
||||
|
|
@ -22,7 +24,8 @@ public readonly record struct RuntimeGameplayOwnershipSnapshot(
|
|||
&& Actions.IsConverged
|
||||
&& Movement.IsConverged
|
||||
&& Fellowship.IsConverged
|
||||
&& Allegiance.IsConverged;
|
||||
&& Allegiance.IsConverged
|
||||
&& Trade.IsConverged;
|
||||
}
|
||||
|
||||
public static class RuntimeGameplayOwnership
|
||||
|
|
@ -34,7 +37,8 @@ public static class RuntimeGameplayOwnership
|
|||
RuntimeActionState actions,
|
||||
RuntimeLocalPlayerMovementState movement,
|
||||
RuntimeFellowshipState fellowship,
|
||||
RuntimeAllegianceState allegiance)
|
||||
RuntimeAllegianceState allegiance,
|
||||
RuntimeTradeState trade)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inventory);
|
||||
ArgumentNullException.ThrowIfNull(character);
|
||||
|
|
@ -43,6 +47,7 @@ public static class RuntimeGameplayOwnership
|
|||
ArgumentNullException.ThrowIfNull(movement);
|
||||
ArgumentNullException.ThrowIfNull(fellowship);
|
||||
ArgumentNullException.ThrowIfNull(allegiance);
|
||||
ArgumentNullException.ThrowIfNull(trade);
|
||||
return new RuntimeGameplayOwnershipSnapshot(
|
||||
inventory.CaptureOwnership(),
|
||||
character.CaptureOwnership(),
|
||||
|
|
@ -50,6 +55,7 @@ public static class RuntimeGameplayOwnership
|
|||
actions.CaptureOwnership(),
|
||||
movement.CaptureOwnership(),
|
||||
fellowship.CaptureOwnership(),
|
||||
allegiance.CaptureOwnership());
|
||||
allegiance.CaptureOwnership(),
|
||||
trade.CaptureOwnership());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
307
src/AcDream.Runtime/Gameplay/RuntimeTradeState.cs
Normal file
307
src/AcDream.Runtime/Gameplay/RuntimeTradeState.cs
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public readonly record struct RuntimeTradeOwnershipSnapshot(
|
||||
bool IsDisposed,
|
||||
bool IsOpen,
|
||||
int StagedItemCount)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !IsOpen
|
||||
&& StagedItemCount == 0;
|
||||
}
|
||||
|
||||
/// <summary>One player's staged-item side of the trade window.</summary>
|
||||
public enum RuntimeTradeSide : uint
|
||||
{
|
||||
Self = 1u,
|
||||
Partner = 2u,
|
||||
}
|
||||
|
||||
/// <summary>Immutable poll snapshot of the whole trade.</summary>
|
||||
public readonly record struct RuntimeTradeSnapshot(
|
||||
long Revision,
|
||||
bool IsOpen,
|
||||
uint PartnerGuid,
|
||||
bool SelfAccepted,
|
||||
bool PartnerAccepted,
|
||||
int SelfItemCount,
|
||||
int PartnerItemCount,
|
||||
uint LastFailureItemGuid,
|
||||
uint LastFailureReason);
|
||||
|
||||
public interface IRuntimeTradeView
|
||||
{
|
||||
RuntimeTradeSnapshot Snapshot { get; }
|
||||
|
||||
/// <summary>Materialized staged-item guids for one side, in stage order.</summary>
|
||||
IReadOnlyList<uint> GetItems(RuntimeTradeSide side);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical presentation-independent owner for the secure-trade window —
|
||||
/// retail's <c>ClientTradeSystem</c>/<c>Trade</c> state, session-scoped like
|
||||
/// <see cref="RuntimeFellowshipState"/> (a disconnect closes the trade
|
||||
/// server-side, so this clears at every generation reset).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wire SSOT: <c>docs/research/2026-08-14-trade-laneB-wire.md</c>; retail UI
|
||||
/// truth: <c>docs/research/2026-08-14-trade-laneA-ui.md</c>. Assembled from
|
||||
/// the 0x01FD–0x0208 event family. ACE landmines honored here:
|
||||
/// RegisterTrade's initiator/partner fields BOTH carry the non-self player's
|
||||
/// guid (the true initiator is never on the wire — <c>Player_Trade.cs:80,98</c>),
|
||||
/// so <see cref="ApplyRegister"/> derives the partner as "whichever guid is
|
||||
/// not mine, else either"; ResetTrade clears BOTH sides' items regardless of
|
||||
/// who reset. Consumers poll <see cref="View"/>'s monotonic revision — no
|
||||
/// push event, the same D2 discipline as fellowship.
|
||||
/// </remarks>
|
||||
public sealed class RuntimeTradeState : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly List<uint> _selfItems = [];
|
||||
private readonly List<uint> _partnerItems = [];
|
||||
private bool _isOpen;
|
||||
private uint _partnerGuid;
|
||||
private bool _selfAccepted;
|
||||
private bool _partnerAccepted;
|
||||
private uint _lastFailureItemGuid;
|
||||
private uint _lastFailureReason;
|
||||
private long _revision;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeTradeState() => View = new TradeView(this);
|
||||
|
||||
public IRuntimeTradeView View { get; }
|
||||
|
||||
public bool IsDisposed
|
||||
{
|
||||
get { lock (_gate) return _disposed; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0x01FD RegisterTrade — the window opens on BOTH clients
|
||||
/// (<c>Handle_Trade__Recv_RegisterTrade @ 0x0056E050</c>). Clears any
|
||||
/// stale staged state from a previous trade.
|
||||
/// </summary>
|
||||
public void ApplyRegister(GameEvents.RegisterTrade update, uint selfGuid)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
_isOpen = true;
|
||||
_partnerGuid = update.Initiator != selfGuid && update.Initiator != 0u
|
||||
? update.Initiator
|
||||
: update.Partner;
|
||||
_selfItems.Clear();
|
||||
_partnerItems.Clear();
|
||||
_selfAccepted = false;
|
||||
_partnerAccepted = false;
|
||||
_lastFailureItemGuid = 0u;
|
||||
_lastFailureReason = 0u;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x01FF CloseTrade — full teardown on either side's close
|
||||
/// (reason recorded nowhere; retail closes the panel outright).</summary>
|
||||
public void ApplyClose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ClearLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0200 AddToTrade — Side 1 = this client's own offer echo,
|
||||
/// Side 2 = the partner staged an item. ACE's slot is always 0; stage
|
||||
/// order is arrival order (retail's grid does the same against ACE).</summary>
|
||||
public void ApplyAdd(GameEvents.AddToTrade update)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
List<uint> items = update.Side == (uint)RuntimeTradeSide.Partner
|
||||
? _partnerItems
|
||||
: _selfItems;
|
||||
if (!items.Contains(update.ItemGuid))
|
||||
items.Add(update.ItemGuid);
|
||||
// Staging changes invalidate prior acceptance server-side (ACE
|
||||
// re-arms via ClearTradeAcceptance; mirrored defensively here so
|
||||
// a dropped 0x0208 cannot leave a stale green check).
|
||||
_selfAccepted = false;
|
||||
_partnerAccepted = false;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0201 RemoveFromTrade — ACE never emits it; honored
|
||||
/// defensively for the retail handler's shape
|
||||
/// (<c>Handle_Trade__Recv_RemoveFromTrade @ 0x0056DC00</c>).</summary>
|
||||
public void ApplyRemove(GameEvents.RemoveFromTrade update)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
bool removed = _selfItems.Remove(update.ItemGuid);
|
||||
removed |= _partnerItems.Remove(update.ItemGuid);
|
||||
if (removed) Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0202 AcceptTrade — who accepted; compared against the
|
||||
/// local player exactly like retail's dispatch @ 0x006ACDF0.</summary>
|
||||
public void ApplyAccept(uint whoAccepted, uint selfGuid)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
if (whoAccepted == selfGuid) _selfAccepted = true;
|
||||
else _partnerAccepted = true;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0203 DeclineTrade — withdraws that side's acceptance.</summary>
|
||||
public void ApplyDecline(uint whoDeclined, uint selfGuid)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
if (whoDeclined == selfGuid) _selfAccepted = false;
|
||||
else _partnerAccepted = false;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0205 ResetTrade — ACE clears BOTH sides' staged items on
|
||||
/// either player's reset (lane B §quirks), and acceptance with them.
|
||||
/// The window stays open (a completed trade auto-resets this way).</summary>
|
||||
public void ApplyReset()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
_selfItems.Clear();
|
||||
_partnerItems.Clear();
|
||||
_selfAccepted = false;
|
||||
_partnerAccepted = false;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0207 TradeFailure — retail removes the refused item
|
||||
/// locally before showing the notice
|
||||
/// (<c>Handle_Trade__Recv_TradeFailure @ 0x0056D990</c>).</summary>
|
||||
public void ApplyFailure(GameEvents.TradeFailure failure)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
_selfItems.Remove(failure.ItemGuid);
|
||||
_partnerItems.Remove(failure.ItemGuid);
|
||||
_lastFailureItemGuid = failure.ItemGuid;
|
||||
_lastFailureReason = failure.Reason;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>0x0208 ClearTradeAcceptance — both checks come down.</summary>
|
||||
public void ApplyClearAcceptance()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
_selfAccepted = false;
|
||||
_partnerAccepted = false;
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Generation reset / session teardown — full clear.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed) return;
|
||||
ClearLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeTradeOwnershipSnapshot CaptureOwnership()
|
||||
{
|
||||
lock (_gate)
|
||||
return new RuntimeTradeOwnershipSnapshot(
|
||||
_disposed,
|
||||
_isOpen,
|
||||
_selfItems.Count + _partnerItems.Count);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed) return;
|
||||
ClearLocked();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearLocked()
|
||||
{
|
||||
bool changed = _isOpen
|
||||
|| _selfItems.Count != 0
|
||||
|| _partnerItems.Count != 0
|
||||
|| _selfAccepted
|
||||
|| _partnerAccepted;
|
||||
_isOpen = false;
|
||||
_partnerGuid = 0u;
|
||||
_selfItems.Clear();
|
||||
_partnerItems.Clear();
|
||||
_selfAccepted = false;
|
||||
_partnerAccepted = false;
|
||||
_lastFailureItemGuid = 0u;
|
||||
_lastFailureReason = 0u;
|
||||
if (changed) Bump();
|
||||
}
|
||||
|
||||
private void Bump() => _revision++;
|
||||
|
||||
private sealed class TradeView(RuntimeTradeState owner) : IRuntimeTradeView
|
||||
{
|
||||
public RuntimeTradeSnapshot Snapshot
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (owner._gate)
|
||||
return new RuntimeTradeSnapshot(
|
||||
owner._revision,
|
||||
owner._isOpen,
|
||||
owner._partnerGuid,
|
||||
owner._selfAccepted,
|
||||
owner._partnerAccepted,
|
||||
owner._selfItems.Count,
|
||||
owner._partnerItems.Count,
|
||||
owner._lastFailureItemGuid,
|
||||
owner._lastFailureReason);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<uint> GetItems(RuntimeTradeSide side)
|
||||
{
|
||||
lock (owner._gate)
|
||||
return side == RuntimeTradeSide.Partner
|
||||
? [.. owner._partnerItems]
|
||||
: [.. owner._selfItems];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,15 +53,22 @@ public enum RuntimeGenerationResetStage
|
|||
/// from the precedent it named).
|
||||
/// </summary>
|
||||
Allegiance = 13,
|
||||
BeginEntityRetirement = 14,
|
||||
RetireEntities = 15,
|
||||
DrainHostProjection = 16,
|
||||
CompleteCanonicalEntities = 17,
|
||||
CompleteHostProjection = 18,
|
||||
ChatIdentity = 19,
|
||||
PlayerSnapshots = 20,
|
||||
PlayerIdentity = 21,
|
||||
Complete = 22,
|
||||
/// <summary>
|
||||
/// Secure trade (2026-08-14): the trade window is session-scoped — a
|
||||
/// disconnect closes the trade server-side (ACE tears the negotiation
|
||||
/// down with the session), so the third sibling J-owner clears here
|
||||
/// beside its fellowship/allegiance precedents.
|
||||
/// </summary>
|
||||
Trade = 14,
|
||||
BeginEntityRetirement = 15,
|
||||
RetireEntities = 16,
|
||||
DrainHostProjection = 17,
|
||||
CompleteCanonicalEntities = 18,
|
||||
CompleteHostProjection = 19,
|
||||
ChatIdentity = 20,
|
||||
PlayerSnapshots = 21,
|
||||
PlayerIdentity = 22,
|
||||
Complete = 23,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeGenerationResetSnapshot(
|
||||
|
|
@ -111,6 +118,7 @@ public sealed class RuntimeGenerationReset
|
|||
private readonly RuntimeLocalPlayerIdentityState _identity;
|
||||
private readonly RuntimeFellowshipState _fellowship;
|
||||
private readonly RuntimeAllegianceState _allegiance;
|
||||
private readonly RuntimeTradeState _trade;
|
||||
private ResetState? _state;
|
||||
private RuntimeGenerationToken _lastCompletedGeneration;
|
||||
private bool _hasCompletedGeneration;
|
||||
|
|
@ -127,7 +135,8 @@ public sealed class RuntimeGenerationReset
|
|||
RuntimeCharacterState character,
|
||||
RuntimeLocalPlayerIdentityState identity,
|
||||
RuntimeFellowshipState fellowship,
|
||||
RuntimeAllegianceState allegiance)
|
||||
RuntimeAllegianceState allegiance,
|
||||
RuntimeTradeState trade)
|
||||
{
|
||||
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
||||
_communication = communication
|
||||
|
|
@ -147,6 +156,7 @@ public sealed class RuntimeGenerationReset
|
|||
?? throw new ArgumentNullException(nameof(fellowship));
|
||||
_allegiance = allegiance
|
||||
?? throw new ArgumentNullException(nameof(allegiance));
|
||||
_trade = trade ?? throw new ArgumentNullException(nameof(trade));
|
||||
}
|
||||
|
||||
public RuntimeGenerationToken? ActiveRetiringGeneration =>
|
||||
|
|
@ -320,6 +330,9 @@ public sealed class RuntimeGenerationReset
|
|||
case RuntimeGenerationResetStage.Allegiance:
|
||||
Advance(state, _allegiance.ResetSession);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.Trade:
|
||||
Advance(state, _trade.Clear);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.BeginEntityRetirement:
|
||||
_ = _entityObjects.BeginSessionClear();
|
||||
state.Retirements = _entityObjects
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ public static class RuntimeSimulationOwnership
|
|||
RuntimeActionState actions,
|
||||
RuntimeLocalPlayerMovementState movement,
|
||||
RuntimeFellowshipState fellowship,
|
||||
RuntimeAllegianceState allegiance)
|
||||
RuntimeAllegianceState allegiance,
|
||||
RuntimeTradeState trade)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entityObjects);
|
||||
return new RuntimeSimulationOwnershipSnapshot(
|
||||
|
|
@ -43,6 +44,7 @@ public static class RuntimeSimulationOwnership
|
|||
actions,
|
||||
movement,
|
||||
fellowship,
|
||||
allegiance));
|
||||
allegiance,
|
||||
trade));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ public sealed record LiveSocialSessionBindings(
|
|||
// compiles unchanged (docs/research/2026-08-11-fa-acdream-seams.md
|
||||
// §2.4 — "the established compatibility convention").
|
||||
RuntimeFellowshipState? Fellowship = null,
|
||||
RuntimeAllegianceState? Allegiance = null);
|
||||
RuntimeAllegianceState? Allegiance = null,
|
||||
// Secure trade (2026-08-14): the third sibling J-owner, same
|
||||
// trailing/optional compatibility convention.
|
||||
RuntimeTradeState? Trade = null);
|
||||
|
||||
/// <summary>
|
||||
/// Owns every inbound subscription for one exact live session. Domain state
|
||||
|
|
@ -281,6 +284,36 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
? notice => allegianceLogin.ApplyLoginNotification(
|
||||
notice.CharacterGuid,
|
||||
notice.IsLoggedIn)
|
||||
: null,
|
||||
// Secure trade (2026-08-14): same conditional delegate-hole
|
||||
// discipline. Self-vs-partner comparisons use the exact
|
||||
// player guid the fellowship holes above already borrow.
|
||||
onTradeRegister: social.Trade is { } tradeRegister
|
||||
? update => tradeRegister.ApplyRegister(update, inventory.PlayerGuid())
|
||||
: null,
|
||||
onTradeClose: social.Trade is { } tradeClose
|
||||
? _ => tradeClose.ApplyClose()
|
||||
: null,
|
||||
onTradeAdd: social.Trade is { } tradeAdd
|
||||
? tradeAdd.ApplyAdd
|
||||
: null,
|
||||
onTradeRemove: social.Trade is { } tradeRemove
|
||||
? tradeRemove.ApplyRemove
|
||||
: null,
|
||||
onTradeAccept: social.Trade is { } tradeAccept
|
||||
? whoAccepted => tradeAccept.ApplyAccept(whoAccepted, inventory.PlayerGuid())
|
||||
: null,
|
||||
onTradeDecline: social.Trade is { } tradeDecline
|
||||
? whoDeclined => tradeDecline.ApplyDecline(whoDeclined, inventory.PlayerGuid())
|
||||
: null,
|
||||
onTradeReset: social.Trade is { } tradeReset
|
||||
? _ => tradeReset.ApplyReset()
|
||||
: null,
|
||||
onTradeFailure: social.Trade is { } tradeFailure
|
||||
? tradeFailure.ApplyFailure
|
||||
: null,
|
||||
onTradeClearAcceptance: social.Trade is { } tradeClear
|
||||
? tradeClear.ApplyClearAcceptance
|
||||
: null));
|
||||
ConstructionCheckpoint();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue