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>
254 lines
9.6 KiB
C#
254 lines
9.6 KiB
C#
using AcDream.Core.Chat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Social;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime;
|
|
|
|
namespace AcDream.Runtime.Tests.Gameplay;
|
|
|
|
public sealed class RuntimeGameplayOwnershipTests
|
|
{
|
|
[Fact]
|
|
public void J5LedgerConvergesEntityPhysicsAndGameplayAsOneLifetime()
|
|
{
|
|
var entities = new RuntimeEntityObjectLifetime();
|
|
var inventory = new RuntimeInventoryState(entities);
|
|
var character = new RuntimeCharacterState();
|
|
var communication = new RuntimeCommunicationState();
|
|
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
|
var movement = new RuntimeLocalPlayerMovementState();
|
|
var fellowship = new RuntimeFellowshipState();
|
|
var allegiance = new RuntimeAllegianceState();
|
|
var trade = new RuntimeTradeState();
|
|
|
|
movement.Execute(RuntimeMovementCommand.ToggleRunLock);
|
|
inventory.Transactions.IncrementBusyCount();
|
|
actions.Selection.Select(
|
|
0x70000001u,
|
|
AcDream.Core.Selection.SelectionChangeSource.System);
|
|
character.Spellbook.OnSpellLearned(42u);
|
|
communication.Chat.OnSystemMessage("runtime-only", 0u);
|
|
|
|
RuntimeSimulationOwnershipSnapshot populated =
|
|
RuntimeSimulationOwnership.Capture(
|
|
entities,
|
|
inventory,
|
|
character,
|
|
communication,
|
|
actions,
|
|
movement,
|
|
fellowship,
|
|
allegiance,
|
|
trade);
|
|
Assert.False(populated.IsConverged);
|
|
|
|
movement.Dispose();
|
|
actions.Dispose();
|
|
communication.Dispose();
|
|
character.Dispose();
|
|
inventory.Dispose();
|
|
fellowship.Dispose();
|
|
allegiance.Dispose();
|
|
trade.Dispose();
|
|
entities.Dispose();
|
|
|
|
RuntimeSimulationOwnershipSnapshot retired =
|
|
RuntimeSimulationOwnership.Capture(
|
|
entities,
|
|
inventory,
|
|
character,
|
|
communication,
|
|
actions,
|
|
movement,
|
|
fellowship,
|
|
allegiance,
|
|
trade);
|
|
Assert.True(retired.IsConverged);
|
|
Assert.True(retired.EntityObjects.IsDisposed);
|
|
Assert.True(retired.Physics.IsDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public void CombinedLedgerConvergesEveryGameplayOwnerAndSubscription()
|
|
{
|
|
using var entities = new RuntimeEntityObjectLifetime();
|
|
var inventory = new RuntimeInventoryState(entities);
|
|
var character = new RuntimeCharacterState();
|
|
var communication = new RuntimeCommunicationState();
|
|
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
|
var movement = new RuntimeLocalPlayerMovementState();
|
|
var fellowship = new RuntimeFellowshipState();
|
|
var allegiance = new RuntimeAllegianceState();
|
|
var trade = new RuntimeTradeState();
|
|
movement.Execute(RuntimeMovementCommand.ToggleRunLock);
|
|
inventory.Shortcuts.Changed += static () => { };
|
|
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
|
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
|
|
inventory.ExternalContainers.RequestOpen(3u);
|
|
inventory.ExternalContainers.ApplyViewContents(3u);
|
|
inventory.Transactions.IncrementBusyCount();
|
|
|
|
character.Spellbook.OnSpellLearned(4u);
|
|
character.Spellbook.SetFavorite(0, 0, 4u);
|
|
character.Spellbook.SetDesiredComponent(5u, 6u);
|
|
character.LocalPlayer.OnVitalUpdate(7u, 1u, 2u, 3u, 4u);
|
|
character.LocalPlayer.OnAttributeUpdate(1u, 2u, 3u, 4u);
|
|
character.LocalPlayer.OnSkillUpdate(
|
|
6u, 1u, 2u, 3u, 4u, 5u, 6d, 7u);
|
|
character.Options.Replace(1u, 2u);
|
|
character.MovementSkills.Update(3, 4);
|
|
|
|
communication.Chat.OnTellReceived(
|
|
"Sender",
|
|
"hello",
|
|
0x50000001u,
|
|
logTextType: 0x03u);
|
|
communication.Chat.OnSelfSent(
|
|
ChatKind.Tell,
|
|
"reply",
|
|
logTextType: 0x04u,
|
|
targetOrChannel: "Recipient");
|
|
communication.TurbineChat.OnChannelsReceived(
|
|
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
|
communication.Friends.Apply(new FriendsUpdate(
|
|
FriendsUpdateType.Full,
|
|
[new FriendEntry(1u, "Friend", true, false, [], [])]));
|
|
communication.Squelch.Replace(new SquelchDatabase(
|
|
new Dictionary<string, uint> { ["account"] = 1u },
|
|
new Dictionary<uint, SquelchInfo>
|
|
{
|
|
[2u] = new SquelchInfo(
|
|
"Character",
|
|
false,
|
|
new HashSet<uint> { 3u }),
|
|
},
|
|
new SquelchInfo(
|
|
string.Empty,
|
|
false,
|
|
new HashSet<uint> { 4u })));
|
|
IDisposable subscription =
|
|
communication.Events.Subscribe(new NoopObserver());
|
|
actions.Selection.Select(
|
|
0x50000002u,
|
|
AcDream.Core.Selection.SelectionChangeSource.World);
|
|
actions.Combat.SetCombatMode(AcDream.Core.Combat.CombatMode.Melee);
|
|
actions.Combat.OnUpdateHealth(0x50000002u, 0.75f);
|
|
actions.Interaction.EnterUse();
|
|
|
|
RuntimeGameplayOwnershipSnapshot populated =
|
|
RuntimeGameplayOwnership.Capture(
|
|
inventory,
|
|
character,
|
|
communication,
|
|
actions,
|
|
movement,
|
|
fellowship,
|
|
allegiance,
|
|
trade);
|
|
|
|
Assert.False(populated.IsConverged);
|
|
Assert.Equal(1, populated.Inventory.ShortcutCount);
|
|
Assert.Equal(1, populated.Character.LearnedSpellCount);
|
|
Assert.Equal(1, populated.Communication.StreamSubscriberCount);
|
|
Assert.True(populated.Communication.HasReplyTarget);
|
|
Assert.True(populated.Communication.HasRetellTarget);
|
|
Assert.Equal(0x50000002u, populated.Actions.SelectedObjectId);
|
|
Assert.Equal(
|
|
AcDream.Core.Combat.CombatMode.Melee,
|
|
populated.Actions.CombatMode);
|
|
Assert.Equal(1, populated.Actions.TrackedTargetHealthCount);
|
|
Assert.True(populated.Movement.AutoRunActive);
|
|
|
|
movement.Dispose();
|
|
actions.Dispose();
|
|
communication.Dispose();
|
|
character.Dispose();
|
|
inventory.Dispose();
|
|
fellowship.Dispose();
|
|
allegiance.Dispose();
|
|
trade.Dispose();
|
|
subscription.Dispose();
|
|
|
|
RuntimeGameplayOwnershipSnapshot retired =
|
|
RuntimeGameplayOwnership.Capture(
|
|
inventory,
|
|
character,
|
|
communication,
|
|
actions,
|
|
movement,
|
|
fellowship,
|
|
allegiance,
|
|
trade);
|
|
|
|
Assert.True(retired.IsConverged);
|
|
Assert.Equal(0, retired.Inventory.ShortcutSubscriberCount);
|
|
Assert.False(retired.Character.InternalSubscriptionsAttached);
|
|
Assert.Equal(0, retired.Communication.StreamSubscriberCount);
|
|
Assert.Equal(0, retired.Communication.PendingDispatchCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void BorrowerFailuresCannotStrandAnyGameplayOwnerDuringTerminalDisposal()
|
|
{
|
|
using var entities = new RuntimeEntityObjectLifetime();
|
|
var inventory = new RuntimeInventoryState(entities);
|
|
var character = new RuntimeCharacterState();
|
|
var communication = new RuntimeCommunicationState();
|
|
var actions = RuntimeActionTestFactory.Create(inventory.Transactions);
|
|
var movement = new RuntimeLocalPlayerMovementState();
|
|
var fellowship = new RuntimeFellowshipState();
|
|
var allegiance = new RuntimeAllegianceState();
|
|
var trade = new RuntimeTradeState();
|
|
|
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
|
inventory.ExternalContainers.Changed += static _ =>
|
|
throw new InvalidOperationException("inventory observer");
|
|
|
|
character.Spellbook.OnSpellLearned(42u);
|
|
character.LocalPlayer.OnVitalUpdate(7u, 1u, 2u, 3u, 4u);
|
|
character.Spellbook.SpellbookChanged += static () =>
|
|
throw new InvalidOperationException("spellbook observer");
|
|
character.LocalPlayer.CharacterChanged += static () =>
|
|
throw new InvalidOperationException("character observer");
|
|
|
|
_ = communication.Events.Subscribe(new ThrowingObserver());
|
|
communication.Chat.OnSystemMessage("failure-isolated event", 0u);
|
|
Assert.Equal(1, communication.DispatchFailureCount);
|
|
|
|
movement.Dispose();
|
|
actions.Dispose();
|
|
Assert.Throws<AggregateException>(inventory.Dispose);
|
|
Assert.Throws<AggregateException>(character.Dispose);
|
|
communication.Dispose();
|
|
fellowship.Dispose();
|
|
allegiance.Dispose();
|
|
trade.Dispose();
|
|
|
|
RuntimeGameplayOwnershipSnapshot retired =
|
|
RuntimeGameplayOwnership.Capture(
|
|
inventory,
|
|
character,
|
|
communication,
|
|
actions,
|
|
movement,
|
|
fellowship,
|
|
allegiance,
|
|
trade);
|
|
|
|
Assert.True(retired.IsConverged);
|
|
Assert.Equal(1, retired.Communication.DispatchFailureCount);
|
|
}
|
|
|
|
private sealed class NoopObserver : IRuntimeCommunicationObserver
|
|
{
|
|
public void OnChat(in RuntimeCommunicationEvent delta) { }
|
|
}
|
|
|
|
private sealed class ThrowingObserver : IRuntimeCommunicationObserver
|
|
{
|
|
public void OnChat(in RuntimeCommunicationEvent delta) =>
|
|
throw new InvalidOperationException("communication observer");
|
|
}
|
|
}
|