fix(client): restore retail interaction parity
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -62,10 +62,10 @@ internal sealed class AutoWieldController : IDisposable
|
|||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Action<uint, uint>? _sendWield;
|
||||
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
|
||||
private readonly Action<string>? _toast;
|
||||
private readonly Action<string>? _systemMessage;
|
||||
private readonly CombatState? _combatState;
|
||||
private readonly Action<CombatMode>? _sendChangeCombatMode;
|
||||
private readonly InventoryTransactionState? _transactions;
|
||||
|
||||
private PendingSwitch? _pendingSwitch;
|
||||
private PendingCombatSettlement? _pendingCombatSettlement;
|
||||
|
|
@ -79,19 +79,19 @@ internal sealed class AutoWieldController : IDisposable
|
|||
Func<uint> playerGuid,
|
||||
Action<uint, uint>? sendWield,
|
||||
Action<uint, uint, int>? sendPutItemInContainer,
|
||||
Action<string>? toast,
|
||||
Action<string>? systemMessage = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null)
|
||||
Action<CombatMode>? sendChangeCombatMode = null,
|
||||
InventoryTransactionState? transactions = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_sendWield = sendWield;
|
||||
_sendPutItemInContainer = sendPutItemInContainer;
|
||||
_toast = toast;
|
||||
_systemMessage = systemMessage;
|
||||
_combatState = combatState;
|
||||
_sendChangeCombatMode = sendChangeCombatMode;
|
||||
_transactions = transactions;
|
||||
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.ObjectRemoved += OnObjectRemoved;
|
||||
|
|
@ -238,8 +238,20 @@ internal sealed class AutoWieldController : IDisposable
|
|||
: BestAvailableEquipMask(item);
|
||||
if (mask == EquipMask.None)
|
||||
{
|
||||
_toast?.Invoke("That slot is already in use");
|
||||
return false;
|
||||
// UsingItem calls retail AutoWield with its automatic-unblock flag.
|
||||
// When every compatible slot is occupied, retail chooses the first
|
||||
// compatible slot, moves that blocker to the backpack, and retries
|
||||
// only after RecvNotice_ServerSaysMoveItem confirms the move.
|
||||
// CPlayerSystem::AutoWield @ 0x0056173D-0x0056186E.
|
||||
mask = FirstCompatibleEquipMask(item);
|
||||
ClientObject? blocker = GetEquippedObjectAtLocation(
|
||||
mask, priority: 0, item.ObjectId);
|
||||
return blocker is not null
|
||||
&& BeginWeaponReplacement(
|
||||
item.ObjectId,
|
||||
blocker,
|
||||
mask,
|
||||
combatModeAfterWield: null);
|
||||
}
|
||||
|
||||
return SendWield(item, mask, combatModeAfterWield: null);
|
||||
|
|
@ -252,10 +264,7 @@ internal sealed class AutoWieldController : IDisposable
|
|||
CombatMode? combatModeAfterWield)
|
||||
{
|
||||
if (_sendPutItemInContainer is null)
|
||||
{
|
||||
_toast?.Invoke("That slot is already in use");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint player = _playerGuid();
|
||||
if (player == 0)
|
||||
|
|
@ -272,8 +281,17 @@ internal sealed class AutoWieldController : IDisposable
|
|||
// is the transaction boundary and preserves its stance-specific motion.
|
||||
_systemMessage?.Invoke(
|
||||
$"Moving {blockingItem.GetAppropriateName()} to your backpack");
|
||||
_sendPutItemInContainer(blockingItem.ObjectId, player, 0);
|
||||
return true;
|
||||
bool dispatched = DispatchInventoryRequest(
|
||||
InventoryRequestKind.PutInContainer,
|
||||
blockingItem.ObjectId,
|
||||
() =>
|
||||
{
|
||||
_sendPutItemInContainer(blockingItem.ObjectId, player, 0);
|
||||
return true;
|
||||
});
|
||||
if (!dispatched)
|
||||
_pendingSwitch = null;
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
private bool SendWield(
|
||||
|
|
@ -288,17 +306,26 @@ internal sealed class AutoWieldController : IDisposable
|
|||
BlockingItemId: 0,
|
||||
RequestedMask: mask,
|
||||
CombatModeAfterWield: combatModeAfterWield);
|
||||
if (!_objects.WieldItemOptimistic(item.ObjectId, _playerGuid(), mask))
|
||||
{
|
||||
_pendingSwitch = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retail ACCWeenieObject::UIAttemptWield @ 0x0058D590.
|
||||
_sendWield(item.ObjectId, (uint)mask);
|
||||
return true;
|
||||
bool dispatched = DispatchInventoryRequest(
|
||||
InventoryRequestKind.Wield,
|
||||
item.ObjectId,
|
||||
() =>
|
||||
{
|
||||
_sendWield(item.ObjectId, (uint)mask);
|
||||
return true;
|
||||
});
|
||||
if (!dispatched)
|
||||
_pendingSwitch = null;
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
private bool DispatchInventoryRequest(
|
||||
InventoryRequestKind kind,
|
||||
uint itemId,
|
||||
Func<bool> dispatch)
|
||||
=> _transactions?.TryDispatch(kind, itemId, dispatch) ?? dispatch();
|
||||
|
||||
private void OnObjectMoved(ClientObjectMove move)
|
||||
{
|
||||
if (_pendingSwitch is not { } pending
|
||||
|
|
@ -454,6 +481,14 @@ internal sealed class AutoWieldController : IDisposable
|
|||
return EquipMask.None;
|
||||
}
|
||||
|
||||
private static EquipMask FirstCompatibleEquipMask(ClientObject item)
|
||||
{
|
||||
foreach (EquipMask mask in AutoEquipOrder)
|
||||
if ((item.ValidLocations & mask) != EquipMask.None)
|
||||
return mask;
|
||||
return EquipMask.None;
|
||||
}
|
||||
|
||||
private bool AutoWearIsLegal(
|
||||
ClientObject item,
|
||||
out ClientObject? blocker)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue