fix(items): port retail useability and autowear rejection
Use the exact low USEABLE_NO-bit predicate so reset/zero-valued direct-use items such as Blackmoor's Favor reach the ordinary Use request. Port AutoWear's clothing-priority blocker lookup and exact named system notice through the shared activation owner. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
60387668d0
commit
f71f947475
13 changed files with 289 additions and 57 deletions
|
|
@ -307,13 +307,9 @@ internal sealed class WorldSelectionQuery
|
|||
public bool IsUseable(uint serverGuid)
|
||||
{
|
||||
if (_liveEntities.TryGetSnapshot(serverGuid, out var spawn))
|
||||
{
|
||||
if (spawn.Useability is uint useability)
|
||||
return useability is not 0u and not 1u;
|
||||
if (((spawn.ObjectDescriptionFlags ?? 0u) & LargeUseObjectFlags) != 0u)
|
||||
return true;
|
||||
}
|
||||
return (GetItemType(serverGuid) & ItemType.Creature) != 0;
|
||||
return ItemUseability.IsUseable(
|
||||
spawn.Useability ?? ItemUseability.Undef);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>ItemHolder::DetermineUseResult @ 0x00588460 pickup gate.</summary>
|
||||
|
|
|
|||
|
|
@ -164,6 +164,26 @@ internal sealed class AutoWieldController : IDisposable
|
|||
return SendWield(item, requestedMask, combatModeAfterWield);
|
||||
}
|
||||
|
||||
if (ItemEquipRules.IsAutoWearItem(item))
|
||||
{
|
||||
if (!AutoWearIsLegal(item, out ClientObject? blocker))
|
||||
{
|
||||
if (blocker is not null)
|
||||
{
|
||||
_systemMessage?.Invoke(
|
||||
$"You must remove your {blocker.GetAppropriateName()} to wear that");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// CPlayerSystem::AutoWear @ 0x005601C0 always supplies the complete
|
||||
// PublicWeenieDesc valid-locations mask to UIAttemptWield.
|
||||
return SendWield(
|
||||
item,
|
||||
item.ValidLocations,
|
||||
combatModeAfterWield: null);
|
||||
}
|
||||
|
||||
EquipMask weaponLocation = requestedMask == EquipMask.None
|
||||
? item.ValidLocations & WeaponReadyMask
|
||||
: requestedMask & WeaponReadyMask;
|
||||
|
|
@ -424,9 +444,6 @@ internal sealed class AutoWieldController : IDisposable
|
|||
|
||||
private EquipMask BestAvailableEquipMask(ClientObject item)
|
||||
{
|
||||
if (ItemEquipRules.IsAutoWearItem(item))
|
||||
return AutoWearIsLegal(item) ? item.ValidLocations : EquipMask.None;
|
||||
|
||||
foreach (EquipMask mask in AutoEquipOrder)
|
||||
{
|
||||
if ((item.ValidLocations & mask) == EquipMask.None)
|
||||
|
|
@ -437,16 +454,22 @@ internal sealed class AutoWieldController : IDisposable
|
|||
return EquipMask.None;
|
||||
}
|
||||
|
||||
private bool AutoWearIsLegal(ClientObject item)
|
||||
private bool AutoWearIsLegal(
|
||||
ClientObject item,
|
||||
out ClientObject? blocker)
|
||||
{
|
||||
uint priorityMask = EquippedAutoWearPriorityMask(item.ObjectId);
|
||||
if ((item.Priority & priorityMask) == 0)
|
||||
{
|
||||
blocker = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
EquipMask occupiedLocations = EquippedAutoWearLocationMask(item.ObjectId)
|
||||
& item.ValidLocations;
|
||||
return GetEquippedObjectAtLocation(
|
||||
occupiedLocations, item.Priority, item.ObjectId) is null;
|
||||
blocker = GetEquippedObjectAtLocation(
|
||||
occupiedLocations, item.Priority, item.ObjectId);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool EquipMaskOccupied(EquipMask mask, uint exceptGuid)
|
||||
|
|
@ -455,11 +478,10 @@ internal sealed class AutoWieldController : IDisposable
|
|||
private uint EquippedAutoWearPriorityMask(uint exceptGuid)
|
||||
{
|
||||
uint mask = 0;
|
||||
foreach (ClientObject item in _objects.Objects)
|
||||
foreach (ClientObject item in _objects.GetEquippedBy(_playerGuid()))
|
||||
{
|
||||
if (item.ObjectId == exceptGuid
|
||||
|| (item.CurrentlyEquippedLocation & ItemEquipRules.AutoWearMask) == EquipMask.None
|
||||
|| !IsEquippedByPlayer(item))
|
||||
|| (item.CurrentlyEquippedLocation & ItemEquipRules.AutoWearMask) == EquipMask.None)
|
||||
continue;
|
||||
mask |= item.Priority;
|
||||
}
|
||||
|
|
@ -469,11 +491,10 @@ internal sealed class AutoWieldController : IDisposable
|
|||
private EquipMask EquippedAutoWearLocationMask(uint exceptGuid)
|
||||
{
|
||||
EquipMask mask = EquipMask.None;
|
||||
foreach (ClientObject item in _objects.Objects)
|
||||
foreach (ClientObject item in _objects.GetEquippedBy(_playerGuid()))
|
||||
{
|
||||
if (item.ObjectId == exceptGuid
|
||||
|| (item.CurrentlyEquippedLocation & ItemEquipRules.AutoWearMask) == EquipMask.None
|
||||
|| !IsEquippedByPlayer(item))
|
||||
|| (item.CurrentlyEquippedLocation & ItemEquipRules.AutoWearMask) == EquipMask.None)
|
||||
continue;
|
||||
mask |= item.CurrentlyEquippedLocation;
|
||||
}
|
||||
|
|
@ -488,10 +509,9 @@ internal sealed class AutoWieldController : IDisposable
|
|||
if (locationMask == EquipMask.None)
|
||||
return null;
|
||||
|
||||
foreach (ClientObject item in _objects.Objects)
|
||||
foreach (ClientObject item in _objects.GetEquippedBy(_playerGuid()))
|
||||
{
|
||||
if (item.ObjectId == exceptGuid
|
||||
|| !IsEquippedByPlayer(item)
|
||||
|| (item.CurrentlyEquippedLocation & locationMask) == EquipMask.None)
|
||||
continue;
|
||||
if ((item.Priority & priority) != 0 || priority == 0)
|
||||
|
|
@ -500,13 +520,6 @@ internal sealed class AutoWieldController : IDisposable
|
|||
return null;
|
||||
}
|
||||
|
||||
private bool IsEquippedByPlayer(ClientObject item)
|
||||
{
|
||||
uint player = _playerGuid();
|
||||
return item.CurrentlyEquippedLocation != EquipMask.None
|
||||
&& (item.WielderId == player || item.ContainerId == player);
|
||||
}
|
||||
|
||||
internal static bool BlocksUseOfShield(ClientObject item)
|
||||
{
|
||||
byte combatUse = item.CombatUse ?? 0;
|
||||
|
|
|
|||
|
|
@ -310,6 +310,15 @@ public static class ItemUseability
|
|||
public static uint TargetFlags(uint useability)
|
||||
=> (useability & TargetMask) >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>ItemUses::IsUseable @ 0x004FCCC0</c>. The implementation
|
||||
/// complements the full bitfield and returns its low bit, so only
|
||||
/// <c>USEABLE_NO</c> disables use. In particular, the default
|
||||
/// <c>USEABLE_UNDEF</c> value zero is usable.
|
||||
/// </summary>
|
||||
public static bool IsUseable(uint useability)
|
||||
=> (useability & No) == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>ItemUses::GetLeastLimitedTargetUse</c> (0x004fcd50): the most
|
||||
/// permissive target-use bit present, priority Remote > Viewed >
|
||||
|
|
@ -330,11 +339,7 @@ public static class ItemUseability
|
|||
}
|
||||
|
||||
public static bool IsDirectUseable(uint useability)
|
||||
{
|
||||
uint source = SourceFlags(useability);
|
||||
return !IsTargeted(useability)
|
||||
&& (source & ~(No | NeverWalk)) != Undef;
|
||||
}
|
||||
=> !IsTargeted(useability) && IsUseable(useability);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ public static class ItemInteractionPolicy
|
|||
uint useability)
|
||||
=> combatUse != 0
|
||||
|| (type & ToolbarEquipmentTypes) != 0
|
||||
|| (useability & ItemUseability.No) == 0;
|
||||
|| ItemUseability.IsUseable(useability);
|
||||
|
||||
public static ItemPrimaryUseResult DetermineUseResult(
|
||||
in ItemPolicyObject item,
|
||||
|
|
@ -207,7 +207,7 @@ public static class ItemInteractionPolicy
|
|||
return ItemPrimaryUseResult.BeginGame;
|
||||
}
|
||||
|
||||
if (IsUseable(item.Useability))
|
||||
if (ItemUseability.IsUseable(item.Useability))
|
||||
return ItemPrimaryUseResult.ItemUse;
|
||||
|
||||
if (item.IsPlayer && item.Id != playerId)
|
||||
|
|
@ -261,7 +261,7 @@ public static class ItemInteractionPolicy
|
|||
return Consumed(actions);
|
||||
}
|
||||
|
||||
if (IsUseable(source.Useability))
|
||||
if (ItemUseability.IsUseable(source.Useability))
|
||||
{
|
||||
if ((source.Flags & PublicWeenieFlags.PlayerKillerSwitch) != 0)
|
||||
return Consumed(new ItemPolicyAction(
|
||||
|
|
@ -406,7 +406,7 @@ public static class ItemInteractionPolicy
|
|||
if (item.OwnedByPlayer && item.IsContainer)
|
||||
actions.Add(new ItemPolicyAction(ItemPolicyActionKind.OpenContainedContainer, item.Id));
|
||||
if (!item.OwnedByPlayer && item.IsContainer
|
||||
&& IsUseable(item.Useability)
|
||||
&& ItemUseability.IsUseable(item.Useability)
|
||||
&& !ItemUseability.IsTargeted(item.Useability))
|
||||
actions.Add(new ItemPolicyAction(ItemPolicyActionKind.SetGroundObject, item.Id,
|
||||
groundObjectId));
|
||||
|
|
@ -418,10 +418,6 @@ public static class ItemInteractionPolicy
|
|||
|| ((valid & ClothingLocations) != 0 && (current & ClothingLocations) == 0)
|
||||
|| ((valid & HeldLocations) != 0 && (current & HeldLocations) == 0);
|
||||
|
||||
private static bool IsUseable(uint useability)
|
||||
=> (ItemUseability.SourceFlags(useability)
|
||||
& ~(ItemUseability.No | ItemUseability.NeverWalk)) != 0;
|
||||
|
||||
private static ItemPlacementPolicyDecision PlaceOnGround(
|
||||
in ItemPlacementPolicyInput input)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue