feat(ui): port retail item interaction policy
This commit is contained in:
parent
0cf780478a
commit
a8da4fd05a
20 changed files with 1110 additions and 104 deletions
|
|
@ -178,6 +178,14 @@ public sealed class ClientObject
|
|||
public uint Priority { get; set; } // ClothingPriority / CoverageMask layer order
|
||||
public uint? Useability { get; set; } // ITEM_USEABLE from PublicWeenieDesc
|
||||
public uint? TargetType { get; set; } // ITEM_TYPE mask for targeted-use compatibility
|
||||
/// <summary>Retail <c>PublicWeenieDesc._bitfield</c>; null until CreateObject supplies it.</summary>
|
||||
public uint? PublicWeenieBitfield { get; set; }
|
||||
/// <summary>Retail <c>PublicWeenieDesc._combatUse</c>; null when its header flag was absent.</summary>
|
||||
public byte? CombatUse { get; set; }
|
||||
/// <summary>Client-side trade state used by ItemHolder legality gates.</summary>
|
||||
public int TradeState { get; set; }
|
||||
/// <summary>Resolved membership in SpellComponentTable (retail IsComponentPack).</summary>
|
||||
public bool IsComponentPack { get; set; }
|
||||
/// <summary>Retail PublicWeenieDesc <c>_blipColor</c>; null until supplied by CreateObject.</summary>
|
||||
public byte? RadarBlipColor { get; set; }
|
||||
/// <summary>Retail PublicWeenieDesc <c>_radar_enum</c>; null until supplied by CreateObject.</summary>
|
||||
|
|
@ -223,7 +231,9 @@ public readonly record struct WeenieData(
|
|||
uint? Useability = null,
|
||||
uint? TargetType = null,
|
||||
byte? RadarBlipColor = null,
|
||||
byte? RadarBehavior = null);
|
||||
byte? RadarBehavior = null,
|
||||
uint? PublicWeenieBitfield = null,
|
||||
byte? CombatUse = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
||||
|
|
@ -257,6 +267,18 @@ public static class ItemUseability
|
|||
public static uint SourceFlags(uint useability)
|
||||
=> useability & SourceMask;
|
||||
|
||||
/// <summary>Retail <c>ItemUses::GetLeastLimitedSourceUse</c>.</summary>
|
||||
public static uint LeastLimitedSourceUse(uint useability)
|
||||
{
|
||||
uint s = SourceFlags(useability);
|
||||
if ((s & Remote) != 0) return Remote;
|
||||
if ((s & Viewed) != 0) return Viewed;
|
||||
if ((s & Contained) != 0) return Contained;
|
||||
if ((s & Wielded) != 0) return Wielded;
|
||||
if ((s & Self) != 0) return Self;
|
||||
return Undef;
|
||||
}
|
||||
|
||||
public static uint TargetFlags(uint useability)
|
||||
=> (useability & TargetMask) >> 16;
|
||||
|
||||
|
|
|
|||
|
|
@ -386,6 +386,8 @@ public sealed class ClientObjectTable
|
|||
if (d.Priority is { } pr) obj.Priority = pr;
|
||||
if (d.Useability is { } use) obj.Useability = use;
|
||||
if (d.TargetType is { } targetType) obj.TargetType = targetType;
|
||||
if (d.PublicWeenieBitfield is { } bitfield) obj.PublicWeenieBitfield = bitfield;
|
||||
if (d.CombatUse is { } combatUse) obj.CombatUse = combatUse;
|
||||
if (d.RadarBlipColor is { } radarBlipColor) obj.RadarBlipColor = radarBlipColor;
|
||||
if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior;
|
||||
if (d.ItemsCapacity is { } ic) obj.ItemsCapacity = ic;
|
||||
|
|
|
|||
437
src/AcDream.Core/Items/ItemInteractionPolicy.cs
Normal file
437
src/AcDream.Core/Items/ItemInteractionPolicy.cs
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.Core.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>PublicWeenieDesc::BitfieldIndex</c> values used by item interaction.
|
||||
/// Names and values are from named-retail <c>acclient.h:6431</c>; the two damaged
|
||||
/// decompiler expressions were verified in the matching v11.4186 executable at
|
||||
/// <c>0x005884B6</c> and <c>0x00588752</c>.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublicWeenieFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
Openable = 0x00000001,
|
||||
Stuck = 0x00000004,
|
||||
Player = 0x00000008,
|
||||
Attackable = 0x00000010,
|
||||
Vendor = 0x00000200,
|
||||
PlayerKillerSwitch = 0x00000400,
|
||||
NonPlayerKillerSwitch = 0x00000800,
|
||||
Door = 0x00001000,
|
||||
RequiresPackSlot = 0x00800000,
|
||||
VolatileRare = 0x10000000,
|
||||
WieldOnUse = 0x20000000,
|
||||
WieldLeft = 0x40000000,
|
||||
}
|
||||
|
||||
/// <summary>Return values of retail <c>ItemHolder::DetermineUseResult</c>.</summary>
|
||||
public enum ItemPrimaryUseResult : ushort
|
||||
{
|
||||
None = 0,
|
||||
ItemUse = 1,
|
||||
PlaceInBackpack = 2,
|
||||
WieldRight = 3,
|
||||
AutoSort = 4,
|
||||
OpenSecureTrade = 5,
|
||||
OpenSalvage = 6,
|
||||
BeginGame = 7,
|
||||
WieldLeft = 8,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable data needed by the retail item rules. App builds this snapshot from
|
||||
/// the mutable object table; Core never reaches into UI, networking, or physics.
|
||||
/// </summary>
|
||||
public readonly record struct ItemPolicyObject(
|
||||
uint Id,
|
||||
ItemType Type,
|
||||
PublicWeenieFlags Flags,
|
||||
uint ContainerId,
|
||||
uint WielderId,
|
||||
EquipMask ValidLocations,
|
||||
EquipMask CurrentLocation,
|
||||
byte CombatUse,
|
||||
int ItemsCapacity,
|
||||
int ContainersCapacity,
|
||||
uint Useability,
|
||||
uint TargetType,
|
||||
bool OwnedByPlayer,
|
||||
bool IsContainer,
|
||||
bool IsComponentPack,
|
||||
int TradeState,
|
||||
int StackSize,
|
||||
int MaxSplitSize,
|
||||
bool IsIn3DView)
|
||||
{
|
||||
public bool IsPlayer => (Flags & PublicWeenieFlags.Player) != 0;
|
||||
}
|
||||
|
||||
public enum ItemPolicyActionKind
|
||||
{
|
||||
PlaceInBackpack,
|
||||
WieldRight,
|
||||
AutoSort,
|
||||
OpenSecureTrade,
|
||||
OpenSalvage,
|
||||
BeginGame,
|
||||
WieldLeft,
|
||||
OpenContainedContainer,
|
||||
SetGroundObject,
|
||||
EnterTargetMode,
|
||||
SendUse,
|
||||
SendUseWithTarget,
|
||||
IncrementBusy,
|
||||
ConfirmPlayerKillerSwitch,
|
||||
ConfirmNonPlayerKillerSwitch,
|
||||
ConfirmVolatileRare,
|
||||
MergeStack,
|
||||
SellToVendor,
|
||||
StartSecureTrade,
|
||||
GiveToTarget,
|
||||
PlaceInContainer,
|
||||
SplitToWorld,
|
||||
DropToWorld,
|
||||
Reject,
|
||||
}
|
||||
|
||||
public readonly record struct ItemPolicyAction(
|
||||
ItemPolicyActionKind Kind,
|
||||
uint ObjectId = 0,
|
||||
uint TargetId = 0,
|
||||
int Amount = 0,
|
||||
string? Message = null);
|
||||
|
||||
public readonly record struct ItemUsePolicyInput(
|
||||
ItemPolicyObject Source,
|
||||
uint PlayerId,
|
||||
uint GroundObjectId,
|
||||
bool ReadyForInventoryRequest,
|
||||
uint ActiveVendorId,
|
||||
bool BypassClassification,
|
||||
bool UseCurrentSelection,
|
||||
ItemPolicyObject? SelectedTarget,
|
||||
bool ConfirmVolatileRareUses,
|
||||
bool InNonCombatMode);
|
||||
|
||||
public readonly record struct ItemUsePolicyDecision(
|
||||
bool Consumed,
|
||||
IReadOnlyList<ItemPolicyAction> Actions);
|
||||
|
||||
public readonly record struct ItemPlacementPolicyInput(
|
||||
ItemPolicyObject Item,
|
||||
uint PlayerId,
|
||||
uint GroundObjectId,
|
||||
bool ReadyForInventoryRequest,
|
||||
uint TargetId,
|
||||
ItemPolicyObject? Target,
|
||||
bool AllowGroundFallback,
|
||||
bool MergeAccepted,
|
||||
bool DragOnPlayerOpensSecureTrade,
|
||||
bool PlayerOnGround,
|
||||
int SplitSize);
|
||||
|
||||
/// <summary>
|
||||
/// Retail's placement return is deliberately separate from its action list:
|
||||
/// vendor sale and secure-trade attempts return false even though they act.
|
||||
/// </summary>
|
||||
public readonly record struct ItemPlacementPolicyDecision(
|
||||
bool ReturnValue,
|
||||
IReadOnlyList<ItemPolicyAction> Actions);
|
||||
|
||||
/// <summary>
|
||||
/// Pure ports of <c>ItemHolder::DetermineUseResult @ 0x00588460</c>,
|
||||
/// <c>UseObject @ 0x00588A80</c>, and <c>AttemptPlaceIn3D @ 0x00588600</c>.
|
||||
/// </summary>
|
||||
public static class ItemInteractionPolicy
|
||||
{
|
||||
private static readonly EquipMask BodyLocations = (EquipMask)0x00007E00u;
|
||||
private static readonly EquipMask ClothingLocations = (EquipMask)0x080001FFu;
|
||||
private static readonly EquipMask HeldLocations = (EquipMask)0x7C0F8000u;
|
||||
|
||||
public static ItemPrimaryUseResult DetermineUseResult(
|
||||
in ItemPolicyObject item,
|
||||
uint playerId,
|
||||
uint groundObjectId)
|
||||
{
|
||||
bool looseOrViewed = (item.ContainerId == 0
|
||||
&& (item.Flags & PublicWeenieFlags.Stuck) == 0)
|
||||
|| (groundObjectId != 0 && item.ContainerId == groundObjectId);
|
||||
|
||||
if (looseOrViewed && (item.WielderId == 0 || item.WielderId == playerId))
|
||||
{
|
||||
bool ordinaryLooseItem = (item.Flags & PublicWeenieFlags.RequiresPackSlot) == 0
|
||||
&& item.ItemsCapacity == 0
|
||||
&& item.ContainersCapacity == 0;
|
||||
if (ordinaryLooseItem || item.IsComponentPack)
|
||||
return ItemPrimaryUseResult.PlaceInBackpack;
|
||||
}
|
||||
|
||||
if (item.OwnedByPlayer)
|
||||
{
|
||||
bool wieldOnPrimaryUse = item.CombatUse != 0
|
||||
|| (item.Type & ItemType.Caster) != 0
|
||||
|| (item.Flags & PublicWeenieFlags.WieldOnUse) != 0;
|
||||
if (wieldOnPrimaryUse && item.WielderId != playerId)
|
||||
return (item.Flags & PublicWeenieFlags.WieldLeft) != 0
|
||||
? ItemPrimaryUseResult.WieldLeft
|
||||
: ItemPrimaryUseResult.WieldRight;
|
||||
|
||||
if (HasUnusedLocation(item.ValidLocations, item.CurrentLocation))
|
||||
return ItemPrimaryUseResult.AutoSort;
|
||||
|
||||
if ((item.Type & ItemType.TinkeringTool) != 0)
|
||||
return ItemPrimaryUseResult.OpenSalvage;
|
||||
}
|
||||
else if ((item.Type & ItemType.Gameboard) != 0)
|
||||
{
|
||||
return ItemPrimaryUseResult.BeginGame;
|
||||
}
|
||||
|
||||
if (IsUseable(item.Useability))
|
||||
return ItemPrimaryUseResult.ItemUse;
|
||||
|
||||
if (item.IsPlayer && item.Id != playerId)
|
||||
return ItemPrimaryUseResult.OpenSecureTrade;
|
||||
|
||||
return ItemPrimaryUseResult.None;
|
||||
}
|
||||
|
||||
public static ItemUsePolicyDecision DecideUse(in ItemUsePolicyInput input)
|
||||
{
|
||||
var source = input.Source;
|
||||
if (!input.ReadyForInventoryRequest)
|
||||
return Consumed();
|
||||
if (input.ActiveVendorId != 0 && source.ContainerId == input.ActiveVendorId)
|
||||
return Consumed();
|
||||
|
||||
if (!input.BypassClassification)
|
||||
{
|
||||
var primary = DetermineUseResult(source, input.PlayerId, input.GroundObjectId);
|
||||
// Exact retail bound: UseObject classifies 2..7, deliberately excluding 8.
|
||||
if (primary is >= ItemPrimaryUseResult.PlaceInBackpack
|
||||
and <= ItemPrimaryUseResult.BeginGame)
|
||||
return Consumed(BuildUsingItemActions(source, input.PlayerId,
|
||||
input.GroundObjectId, primary));
|
||||
}
|
||||
|
||||
if (source.TradeState == 1)
|
||||
return Reject("You cannot use an item while it is being traded.");
|
||||
|
||||
if (source.CurrentLocation == EquipMask.None
|
||||
&& ItemUseability.LeastLimitedSourceUse(source.Useability) == ItemUseability.Wielded)
|
||||
return Reject("You must wield that item before you can use it.");
|
||||
|
||||
if (ItemUseability.IsTargeted(source.Useability))
|
||||
{
|
||||
if (!input.UseCurrentSelection)
|
||||
return Consumed(new ItemPolicyAction(ItemPolicyActionKind.EnterTargetMode, source.Id));
|
||||
|
||||
if (input.SelectedTarget is not { } target)
|
||||
return Reject("Select a target for this item first.");
|
||||
if (!IsTargetCompatible(source, target, input.PlayerId))
|
||||
return Reject("That is not a valid target for this item.");
|
||||
|
||||
var actions = new List<ItemPolicyAction>
|
||||
{
|
||||
new(ItemPolicyActionKind.SendUseWithTarget, source.Id, target.Id),
|
||||
new(ItemPolicyActionKind.IncrementBusy),
|
||||
};
|
||||
actions.AddRange(BuildUsingItemActions(source, input.PlayerId,
|
||||
input.GroundObjectId, DetermineUseResult(source, input.PlayerId, input.GroundObjectId)));
|
||||
return Consumed(actions);
|
||||
}
|
||||
|
||||
if (IsUseable(source.Useability))
|
||||
{
|
||||
if ((source.Flags & PublicWeenieFlags.PlayerKillerSwitch) != 0)
|
||||
return Consumed(new ItemPolicyAction(
|
||||
ItemPolicyActionKind.ConfirmPlayerKillerSwitch, source.Id));
|
||||
if ((source.Flags & PublicWeenieFlags.NonPlayerKillerSwitch) != 0)
|
||||
return Consumed(new ItemPolicyAction(
|
||||
ItemPolicyActionKind.ConfirmNonPlayerKillerSwitch, source.Id));
|
||||
if ((source.Flags & PublicWeenieFlags.VolatileRare) != 0
|
||||
&& input.ConfirmVolatileRareUses)
|
||||
return Consumed(new ItemPolicyAction(
|
||||
ItemPolicyActionKind.ConfirmVolatileRare, source.Id));
|
||||
|
||||
var actions = new List<ItemPolicyAction>
|
||||
{
|
||||
new(ItemPolicyActionKind.SendUse, source.Id),
|
||||
new(ItemPolicyActionKind.IncrementBusy),
|
||||
};
|
||||
actions.AddRange(BuildUsingItemActions(source, input.PlayerId,
|
||||
input.GroundObjectId, DetermineUseResult(source, input.PlayerId, input.GroundObjectId)));
|
||||
return Consumed(actions);
|
||||
}
|
||||
|
||||
var fallback = BuildUsingItemActions(source, input.PlayerId,
|
||||
input.GroundObjectId, DetermineUseResult(source, input.PlayerId, input.GroundObjectId));
|
||||
if (fallback.Count != 0)
|
||||
return Consumed(fallback);
|
||||
if (source.Id == input.PlayerId)
|
||||
return new ItemUsePolicyDecision(false, Array.Empty<ItemPolicyAction>());
|
||||
if ((source.Flags & PublicWeenieFlags.Door) != 0)
|
||||
return Reject("You cannot open or close that object right now.");
|
||||
if ((source.Flags & PublicWeenieFlags.Attackable) != 0
|
||||
&& input.InNonCombatMode)
|
||||
return Reject("You must switch to a combat mode before attacking that target.");
|
||||
if ((source.Flags & PublicWeenieFlags.Attackable) == 0
|
||||
|| input.InNonCombatMode)
|
||||
return Reject("That object cannot be used.");
|
||||
return new ItemUsePolicyDecision(false, Array.Empty<ItemPolicyAction>());
|
||||
}
|
||||
|
||||
public static bool IsTargetCompatible(
|
||||
in ItemPolicyObject source,
|
||||
in ItemPolicyObject target,
|
||||
uint playerId)
|
||||
{
|
||||
if (source.TradeState == 1)
|
||||
return false;
|
||||
|
||||
uint flags = ItemUseability.TargetFlags(source.Useability);
|
||||
if (!target.OwnedByPlayer)
|
||||
{
|
||||
uint least = ItemUseability.LeastLimitedTargetUse(source.Useability);
|
||||
if ((least & ItemUseability.Contained) != 0)
|
||||
{
|
||||
if (!(target.Id == playerId && (flags & ItemUseability.Self) != 0))
|
||||
return false;
|
||||
}
|
||||
else if ((least & ItemUseability.Wielded) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.Id == playerId && (flags & ItemUseability.Self) == 0)
|
||||
return false;
|
||||
|
||||
return (source.TargetType & (uint)target.Type) != 0;
|
||||
}
|
||||
|
||||
public static ItemPlacementPolicyDecision DecidePlacement(
|
||||
in ItemPlacementPolicyInput input)
|
||||
{
|
||||
if (!input.ReadyForInventoryRequest)
|
||||
return Placement(false);
|
||||
|
||||
if (input.TargetId == input.PlayerId)
|
||||
return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.PlaceInBackpack, input.Item.Id));
|
||||
if (!input.Item.OwnedByPlayer)
|
||||
return Placement(false, RejectAction("You must first pick up that item."));
|
||||
if (input.Item.TradeState != 0)
|
||||
return Placement(false, RejectAction("You cannot move an item while it is being traded."));
|
||||
|
||||
if (input.TargetId == 0)
|
||||
return input.AllowGroundFallback ? PlaceOnGround(input) : Placement(false);
|
||||
if (input.MergeAccepted)
|
||||
return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.MergeStack,
|
||||
input.Item.Id, input.TargetId, input.SplitSize));
|
||||
if (input.Target is not { } target)
|
||||
return input.AllowGroundFallback ? PlaceOnGround(input) : Placement(false);
|
||||
|
||||
if ((target.Flags & PublicWeenieFlags.Vendor) != 0)
|
||||
{
|
||||
if (input.SplitSize >= input.Item.MaxSplitSize)
|
||||
return Placement(false, new ItemPolicyAction(ItemPolicyActionKind.SellToVendor,
|
||||
input.Item.Id, target.Id, input.SplitSize));
|
||||
return Placement(false, RejectAction("Split the stack before selling part of it."));
|
||||
}
|
||||
|
||||
if (input.DragOnPlayerOpensSecureTrade && target.IsPlayer)
|
||||
return Placement(false, new ItemPolicyAction(ItemPolicyActionKind.StartSecureTrade,
|
||||
input.Item.Id, target.Id, input.SplitSize));
|
||||
|
||||
if (target.Type == ItemType.Creature)
|
||||
return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.GiveToTarget,
|
||||
input.Item.Id, target.Id, input.SplitSize));
|
||||
|
||||
if (target.IsContainer)
|
||||
{
|
||||
if ((target.Flags & PublicWeenieFlags.Openable) == 0)
|
||||
return Placement(false, RejectAction("That container is locked."));
|
||||
if (target.Id != input.GroundObjectId)
|
||||
return Placement(false, RejectAction("You must open that container first."));
|
||||
return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.PlaceInContainer,
|
||||
input.Item.Id, target.Id, input.SplitSize));
|
||||
}
|
||||
|
||||
if (input.AllowGroundFallback)
|
||||
return PlaceOnGround(input);
|
||||
return Placement(false, RejectAction("You cannot give that item to this target."));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ItemPolicyAction> BuildUsingItemActions(
|
||||
in ItemPolicyObject item,
|
||||
uint playerId,
|
||||
uint groundObjectId,
|
||||
ItemPrimaryUseResult primary)
|
||||
{
|
||||
var actions = new List<ItemPolicyAction>(3);
|
||||
ItemPolicyActionKind? primaryAction = primary switch
|
||||
{
|
||||
ItemPrimaryUseResult.PlaceInBackpack => ItemPolicyActionKind.PlaceInBackpack,
|
||||
ItemPrimaryUseResult.WieldRight => ItemPolicyActionKind.WieldRight,
|
||||
ItemPrimaryUseResult.AutoSort => ItemPolicyActionKind.AutoSort,
|
||||
ItemPrimaryUseResult.OpenSecureTrade => ItemPolicyActionKind.OpenSecureTrade,
|
||||
ItemPrimaryUseResult.OpenSalvage => ItemPolicyActionKind.OpenSalvage,
|
||||
ItemPrimaryUseResult.BeginGame => ItemPolicyActionKind.BeginGame,
|
||||
ItemPrimaryUseResult.WieldLeft => ItemPolicyActionKind.WieldLeft,
|
||||
_ => null,
|
||||
};
|
||||
if (primaryAction is { } action)
|
||||
actions.Add(new ItemPolicyAction(action, item.Id));
|
||||
|
||||
if (item.OwnedByPlayer && item.IsContainer)
|
||||
actions.Add(new ItemPolicyAction(ItemPolicyActionKind.OpenContainedContainer, item.Id));
|
||||
if (!item.OwnedByPlayer && item.IsContainer
|
||||
&& IsUseable(item.Useability)
|
||||
&& !ItemUseability.IsTargeted(item.Useability))
|
||||
actions.Add(new ItemPolicyAction(ItemPolicyActionKind.SetGroundObject, item.Id,
|
||||
groundObjectId));
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static bool HasUnusedLocation(EquipMask valid, EquipMask current)
|
||||
=> ((valid & BodyLocations) != 0 && (current & BodyLocations) == 0)
|
||||
|| ((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)
|
||||
{
|
||||
if (!input.PlayerOnGround)
|
||||
return Placement(false, RejectAction("You cannot do that in mid air."));
|
||||
if (input.SplitSize < input.Item.MaxSplitSize)
|
||||
return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.SplitToWorld,
|
||||
input.Item.Id, Amount: input.SplitSize));
|
||||
if (!input.Item.IsIn3DView)
|
||||
return Placement(true, new ItemPolicyAction(ItemPolicyActionKind.DropToWorld, input.Item.Id));
|
||||
return Placement(false, RejectAction("Move cancelled."));
|
||||
}
|
||||
|
||||
private static ItemUsePolicyDecision Consumed(params ItemPolicyAction[] actions)
|
||||
=> new(true, actions);
|
||||
|
||||
private static ItemUsePolicyDecision Consumed(IReadOnlyList<ItemPolicyAction> actions)
|
||||
=> new(true, actions);
|
||||
|
||||
private static ItemUsePolicyDecision Reject(string message)
|
||||
=> Consumed(RejectAction(message));
|
||||
|
||||
private static ItemPolicyAction RejectAction(string message)
|
||||
=> new(ItemPolicyActionKind.Reject, Message: message);
|
||||
|
||||
private static ItemPlacementPolicyDecision Placement(
|
||||
bool returnValue,
|
||||
params ItemPolicyAction[] actions)
|
||||
=> new(returnValue, actions);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue