feat: port retail magic lifecycle and retained spell UI
Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
7b7ffcd278
commit
07be994d97
84 changed files with 17822 additions and 1051 deletions
197
src/AcDream.App/Spells/MagicRuntime.cs
Normal file
197
src/AcDream.App/Spells/MagicRuntime.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
/// <summary>Immutable presentation metadata for one retail spell component WCID.</summary>
|
||||
public sealed record SpellComponentDescriptor(
|
||||
uint WeenieClassId,
|
||||
string Name,
|
||||
uint Category,
|
||||
uint IconId);
|
||||
|
||||
/// <summary>
|
||||
/// App-layer projection of retail's spell, component, and school-focus DAT tables.
|
||||
/// Keeping this catalog outside <c>GameWindow</c> makes the SCID/WCID boundary and
|
||||
/// rough spell-level calculation one independently testable ownership seam.
|
||||
/// </summary>
|
||||
public sealed class MagicCatalog
|
||||
{
|
||||
private const uint SpellTableDid = 0x0E00000Eu;
|
||||
private const uint SpellComponentTableDid = 0x0E00000Fu;
|
||||
private const uint ComponentIdMapDid = 0x27000002u;
|
||||
|
||||
private readonly IReadOnlyDictionary<uint, SpellFormulaDefinition> _formulaDefinitions;
|
||||
private readonly IReadOnlyDictionary<uint, uint> _wcidByScid;
|
||||
private readonly IReadOnlyDictionary<uint, uint> _magicPackWcidBySchool;
|
||||
private readonly IReadOnlyDictionary<uint, int> _spellLevels;
|
||||
|
||||
private MagicCatalog(
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
||||
IReadOnlyDictionary<uint, SpellFormulaDefinition> formulaDefinitions,
|
||||
IReadOnlyDictionary<uint, uint> wcidByScid,
|
||||
IReadOnlyDictionary<uint, uint> magicPackWcidBySchool,
|
||||
IReadOnlyDictionary<uint, int> spellLevels)
|
||||
{
|
||||
Components = components;
|
||||
_formulaDefinitions = formulaDefinitions;
|
||||
_wcidByScid = wcidByScid;
|
||||
_magicPackWcidBySchool = magicPackWcidBySchool;
|
||||
_spellLevels = spellLevels;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<uint, SpellComponentDescriptor> Components { get; }
|
||||
|
||||
public bool IsComponentPack(uint weenieClassId)
|
||||
=> Components.ContainsKey(weenieClassId);
|
||||
|
||||
public int GetSpellLevel(uint spellId)
|
||||
=> _spellLevels.TryGetValue(spellId, out int level) ? level : 0;
|
||||
|
||||
public SpellComponentRequirementService CreateRequirementService(
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<string> accountName)
|
||||
=> new(
|
||||
objects,
|
||||
playerGuid,
|
||||
accountName,
|
||||
_formulaDefinitions,
|
||||
_wcidByScid,
|
||||
_magicPackWcidBySchool);
|
||||
|
||||
public static MagicCatalog Load(DatCollection dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
|
||||
var components = new Dictionary<uint, SpellComponentDescriptor>();
|
||||
var wcidByScid = new Dictionary<uint, uint>();
|
||||
SpellComponentTable? componentTable = dats.Get<SpellComponentTable>(SpellComponentTableDid);
|
||||
DualEnumIDMap? componentIds = dats.Get<DualEnumIDMap>(ComponentIdMapDid);
|
||||
if (componentTable is not null && componentIds is not null)
|
||||
{
|
||||
foreach (var pair in componentTable.Components)
|
||||
{
|
||||
if (!componentIds.ClientEnumToID.TryGetValue(pair.Key, out uint wcid))
|
||||
continue;
|
||||
|
||||
wcidByScid[pair.Key] = wcid;
|
||||
components[wcid] = new SpellComponentDescriptor(
|
||||
wcid,
|
||||
pair.Value.Name.Value,
|
||||
pair.Value.Category,
|
||||
pair.Value.Icon.DataId);
|
||||
}
|
||||
}
|
||||
|
||||
var formulaDefinitions = new Dictionary<uint, SpellFormulaDefinition>();
|
||||
var spellLevels = new Dictionary<uint, int>();
|
||||
DatReaderWriter.DBObjs.SpellTable? spellTable =
|
||||
dats.Get<DatReaderWriter.DBObjs.SpellTable>(SpellTableDid);
|
||||
if (spellTable is not null)
|
||||
{
|
||||
foreach (var pair in spellTable.Spells)
|
||||
{
|
||||
uint[] formula = pair.Value.Components.Take(8).ToArray();
|
||||
formulaDefinitions[pair.Key] = new SpellFormulaDefinition(
|
||||
pair.Value.FormulaVersion,
|
||||
formula,
|
||||
(uint)pair.Value.School);
|
||||
spellLevels[pair.Key] =
|
||||
RetailSpellFormula.InqSpellLevelByRoughHeuristic(formula);
|
||||
}
|
||||
}
|
||||
|
||||
var magicPackWcidBySchool = new Dictionary<uint, uint>();
|
||||
// Retail IsComponentPack resolves enum key 0x10000001 in category 0x28;
|
||||
// that key is not itself a portal.dat file id.
|
||||
uint magicPackMapDid = AcDream.App.UI.RetailDataIdResolver.Resolve(
|
||||
dats, 0x10000001u, 0x28u);
|
||||
if (magicPackMapDid != 0u
|
||||
&& dats.Portal.TryGet<EnumIDMap>(magicPackMapDid, out EnumIDMap? magicPackMap)
|
||||
&& magicPackMap is not null)
|
||||
{
|
||||
foreach (var pair in magicPackMap.ClientEnumToID)
|
||||
magicPackWcidBySchool[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
return new MagicCatalog(
|
||||
components,
|
||||
formulaDefinitions,
|
||||
wcidByScid,
|
||||
magicPackWcidBySchool,
|
||||
spellLevels);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// App-layer owner for the live retail magic request path. The server owns
|
||||
/// turning, motion, fizzle, mana/component consumption, effects, and results.
|
||||
/// </summary>
|
||||
public sealed class MagicRuntime
|
||||
{
|
||||
private MagicRuntime(MagicCatalog catalog, SpellCastingController casting)
|
||||
{
|
||||
Catalog = catalog;
|
||||
Casting = casting;
|
||||
}
|
||||
|
||||
public MagicCatalog Catalog { get; }
|
||||
public SpellCastingController Casting { get; }
|
||||
|
||||
public static MagicRuntime Create(
|
||||
MagicCatalog catalog,
|
||||
Spellbook spellbook,
|
||||
ClientObjectTable objects,
|
||||
Func<uint?> selectedObject,
|
||||
Func<uint> localPlayerId,
|
||||
Func<string> accountName,
|
||||
Action stopCompletely,
|
||||
Action<uint> sendUntargeted,
|
||||
Action<uint, uint> sendTargeted,
|
||||
Action<string> displayMessage,
|
||||
Action incrementBusy,
|
||||
Func<bool> canSend)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(spellbook);
|
||||
ArgumentNullException.ThrowIfNull(objects);
|
||||
ArgumentNullException.ThrowIfNull(displayMessage);
|
||||
|
||||
SpellComponentRequirementService requirements = catalog.CreateRequirementService(
|
||||
objects, localPlayerId, accountName);
|
||||
bool TargetCompatible(uint target, SpellMetadata spell, bool showMessage)
|
||||
{
|
||||
if (objects.Get(target) is not { } targetObject)
|
||||
return false;
|
||||
|
||||
SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate(
|
||||
localPlayerId(), targetObject, spell);
|
||||
if (showMessage && !result.Allowed && result.Message is { } message)
|
||||
displayMessage(message);
|
||||
return result.Allowed;
|
||||
}
|
||||
|
||||
var casting = new SpellCastingController(
|
||||
spellbook,
|
||||
selectedObject,
|
||||
localPlayerId,
|
||||
stopCompletely,
|
||||
sendUntargeted,
|
||||
sendTargeted,
|
||||
displayMessage,
|
||||
targetCompatible: (target, spell) => TargetCompatible(target, spell, true),
|
||||
hasRequiredComponents: requirements.HasRequiredComponents,
|
||||
incrementBusy: incrementBusy,
|
||||
canSend: canSend,
|
||||
targetCompatibleSilent: (target, spell) => TargetCompatible(target, spell, false));
|
||||
return new MagicRuntime(catalog, casting);
|
||||
}
|
||||
|
||||
public void Reset() => Casting.Reset();
|
||||
}
|
||||
45
src/AcDream.App/Spells/RetailSpellTargetPolicy.cs
Normal file
45
src/AcDream.App/Spells/RetailSpellTargetPolicy.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
public readonly record struct SpellTargetPolicyResult(bool Allowed, string? Message)
|
||||
{
|
||||
public static SpellTargetPolicyResult Accept { get; } = new(true, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure projection of ClientMagicSystem::ObjectCompatibleWithSpellTargetType
|
||||
/// (0x00567230). The caller owns target lookup and message presentation.
|
||||
/// </summary>
|
||||
public static class RetailSpellTargetPolicy
|
||||
{
|
||||
private const uint SpecialTargetMask = 0x00008107u;
|
||||
|
||||
public static SpellTargetPolicyResult Evaluate(
|
||||
uint localPlayerId,
|
||||
ClientObject target,
|
||||
SpellMetadata spell)
|
||||
{
|
||||
uint mask = spell.TargetMask;
|
||||
uint special = mask & SpecialTargetMask;
|
||||
if (target.ObjectId == localPlayerId && special == 0u)
|
||||
return new(false, "You cannot cast this spell upon yourself.");
|
||||
if (target.StackSize > 1)
|
||||
return new(false, "Cannot cast spell on a stack of items.");
|
||||
|
||||
if (((uint)target.Type & mask) == 0u && special == 0u)
|
||||
return new(false, $"This spell cannot be cast on {target.Name}.");
|
||||
|
||||
// Retail joins the ordinary type match and the 0x8107 special-mask
|
||||
// bypass before these two gates. IsPlayer is PWD bit 3; otherwise the
|
||||
// object must carry BF_ATTACKABLE. Pets are never legal spell targets.
|
||||
var flags = (PublicWeenieFlags)target.PublicWeenieBitfield.GetValueOrDefault();
|
||||
bool playerOrAttackable = (flags
|
||||
& (PublicWeenieFlags.Player | PublicWeenieFlags.Attackable)) != 0;
|
||||
if (!playerOrAttackable || target.PetOwnerId != 0u)
|
||||
return new(false, $"This spell cannot be cast on {target.Name}.");
|
||||
|
||||
return SpellTargetPolicyResult.Accept;
|
||||
}
|
||||
}
|
||||
157
src/AcDream.App/Spells/SpellCastingController.cs
Normal file
157
src/AcDream.App/Spells/SpellCastingController.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using System;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
/// <summary>
|
||||
/// App-layer owner for retail cast intent. It validates the client-owned
|
||||
/// selection rules, stops movement, and emits exactly one targeted or
|
||||
/// untargeted request. ACE owns every gameplay result after that boundary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Port of <c>ClientMagicSystem::CastSpell</c> (0x00568040) and
|
||||
/// <c>FreeHandsAndCastSpell</c> (0x00566EF0). This controller deliberately
|
||||
/// does not consume mana/components or start local effects.
|
||||
/// </remarks>
|
||||
public sealed class SpellCastingController
|
||||
{
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly Func<uint?> _selectedObject;
|
||||
private readonly Func<uint> _localPlayerId;
|
||||
private readonly Action _stopCompletely;
|
||||
private readonly Action<uint> _sendUntargeted;
|
||||
private readonly Action<uint, uint> _sendTargeted;
|
||||
private readonly Action<string> _displayMessage;
|
||||
private readonly Func<uint, SpellMetadata, bool> _targetCompatible;
|
||||
private readonly Func<uint, SpellMetadata, bool> _targetCompatibleSilent;
|
||||
private readonly Func<uint, bool> _hasRequiredComponents;
|
||||
private readonly Action _incrementBusy;
|
||||
private readonly Func<bool> _canSend;
|
||||
|
||||
public SpellCastingController(
|
||||
Spellbook spellbook,
|
||||
Func<uint?> selectedObject,
|
||||
Func<uint> localPlayerId,
|
||||
Action stopCompletely,
|
||||
Action<uint> sendUntargeted,
|
||||
Action<uint, uint> sendTargeted,
|
||||
Action<string> displayMessage,
|
||||
Func<uint, SpellMetadata, bool>? targetCompatible = null,
|
||||
Func<uint, bool>? hasRequiredComponents = null,
|
||||
Action? incrementBusy = null,
|
||||
Func<bool>? canSend = null,
|
||||
Func<uint, SpellMetadata, bool>? targetCompatibleSilent = null)
|
||||
{
|
||||
_spellbook = spellbook ?? throw new ArgumentNullException(nameof(spellbook));
|
||||
_selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject));
|
||||
_localPlayerId = localPlayerId ?? throw new ArgumentNullException(nameof(localPlayerId));
|
||||
_stopCompletely = stopCompletely ?? throw new ArgumentNullException(nameof(stopCompletely));
|
||||
_sendUntargeted = sendUntargeted ?? throw new ArgumentNullException(nameof(sendUntargeted));
|
||||
_sendTargeted = sendTargeted ?? throw new ArgumentNullException(nameof(sendTargeted));
|
||||
_displayMessage = displayMessage ?? throw new ArgumentNullException(nameof(displayMessage));
|
||||
_targetCompatible = targetCompatible ?? ((_, _) => true);
|
||||
_targetCompatibleSilent = targetCompatibleSilent ?? _targetCompatible;
|
||||
_hasRequiredComponents = hasRequiredComponents ?? (_ => true);
|
||||
_incrementBusy = incrementBusy ?? (() => { });
|
||||
_canSend = canSend ?? (() => true);
|
||||
}
|
||||
|
||||
public uint? LastRequestedSpellId { get; private set; }
|
||||
public uint? LastRequestedTargetId { get; private set; }
|
||||
|
||||
public bool IsTargetReady(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId)
|
||||
|| !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
return false;
|
||||
if (spell.IsSelfTargeted || spell.IsUntargeted || spell.TargetMask == 0u)
|
||||
return true;
|
||||
return _selectedObject() is uint target and not 0u
|
||||
&& _targetCompatibleSilent(target, spell);
|
||||
}
|
||||
|
||||
public CastRequestResult Cast(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId) || !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
{
|
||||
_displayMessage("You do not know that spell.");
|
||||
return CastRequestResult.UnknownSpell;
|
||||
}
|
||||
|
||||
if (!_hasRequiredComponents(spellId))
|
||||
{
|
||||
_displayMessage("You do not have all of this spell's components.");
|
||||
return CastRequestResult.MissingComponents;
|
||||
}
|
||||
|
||||
uint? target;
|
||||
bool untargeted;
|
||||
if (spell.IsSelfTargeted)
|
||||
{
|
||||
uint playerId = _localPlayerId();
|
||||
target = playerId == 0 ? null : playerId;
|
||||
untargeted = target is null;
|
||||
}
|
||||
else if (spell.IsUntargeted || spell.TargetMask == 0)
|
||||
{
|
||||
target = null;
|
||||
untargeted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = _selectedObject();
|
||||
untargeted = false;
|
||||
if (target is null or 0)
|
||||
{
|
||||
_displayMessage("You must select a suitable target.");
|
||||
return CastRequestResult.NoTarget;
|
||||
}
|
||||
if (!_targetCompatible(target.Value, spell))
|
||||
return CastRequestResult.IncompatibleTarget;
|
||||
}
|
||||
|
||||
if (!_canSend())
|
||||
{
|
||||
_displayMessage("You cannot cast a spell right now.");
|
||||
return CastRequestResult.Unavailable;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_stopCompletely();
|
||||
LastRequestedSpellId = spellId;
|
||||
LastRequestedTargetId = target;
|
||||
if (untargeted)
|
||||
_sendUntargeted(spellId);
|
||||
else
|
||||
_sendTargeted(target!.Value, spellId);
|
||||
// FreeHandsAndCastSpell @ 0x00566EF0 increments the shared UI
|
||||
// busy reference only after Event_Cast has been emitted. The
|
||||
// matching server UseDone event decrements it.
|
||||
_incrementBusy();
|
||||
}
|
||||
catch
|
||||
{
|
||||
LastRequestedSpellId = null;
|
||||
LastRequestedTargetId = null;
|
||||
throw;
|
||||
}
|
||||
return CastRequestResult.Sent;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
LastRequestedSpellId = null;
|
||||
LastRequestedTargetId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CastRequestResult
|
||||
{
|
||||
Sent,
|
||||
UnknownSpell,
|
||||
NoTarget,
|
||||
IncompatibleTarget,
|
||||
MissingComponents,
|
||||
Unavailable,
|
||||
}
|
||||
121
src/AcDream.App/Spells/SpellComponentRequirementService.cs
Normal file
121
src/AcDream.App/Spells/SpellComponentRequirementService.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
public sealed record SpellFormulaDefinition(
|
||||
uint FormulaVersion,
|
||||
IReadOnlyList<uint> ComponentIds,
|
||||
uint School = 0u);
|
||||
|
||||
/// <summary>
|
||||
/// Client-side retail component preflight for ClientMagicSystem::CastSpell
|
||||
/// (0x00568040). Spell formula entries are SCIDs; carried inventory objects and
|
||||
/// SetDesiredComponent use WCIDs, so all comparisons cross the DAT enum map.
|
||||
/// </summary>
|
||||
public sealed class SpellComponentRequirementService
|
||||
{
|
||||
public const uint SpellComponentsRequiredProperty = 68u;
|
||||
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<string> _accountName;
|
||||
private readonly IReadOnlyDictionary<uint, SpellFormulaDefinition> _formulas;
|
||||
private readonly IReadOnlyDictionary<uint, uint> _wcidByScid;
|
||||
private readonly IReadOnlyDictionary<uint, uint> _magicPackWcidBySchool;
|
||||
|
||||
public SpellComponentRequirementService(
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<string> accountName,
|
||||
IReadOnlyDictionary<uint, SpellFormulaDefinition> formulas,
|
||||
IReadOnlyDictionary<uint, uint> wcidByScid,
|
||||
IReadOnlyDictionary<uint, uint>? magicPackWcidBySchool = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_accountName = accountName ?? throw new ArgumentNullException(nameof(accountName));
|
||||
_formulas = formulas ?? throw new ArgumentNullException(nameof(formulas));
|
||||
_wcidByScid = wcidByScid ?? throw new ArgumentNullException(nameof(wcidByScid));
|
||||
_magicPackWcidBySchool = magicPackWcidBySchool
|
||||
?? new Dictionary<uint, uint>();
|
||||
}
|
||||
|
||||
public bool HasRequiredComponents(uint spellId)
|
||||
{
|
||||
uint playerGuid = _playerGuid();
|
||||
ClientObject? player = _objects.Get(playerGuid);
|
||||
if (player is not null
|
||||
&& !player.Properties.GetBool(SpellComponentsRequiredProperty, true))
|
||||
return true;
|
||||
if (!_formulas.TryGetValue(spellId, out SpellFormulaDefinition? formula))
|
||||
return false;
|
||||
|
||||
HashSet<uint> ownedWcids = _objects.Objects
|
||||
.Where(item => IsOwnedByPlayer(item, playerGuid))
|
||||
.Select(item => item.WeenieClassId)
|
||||
.ToHashSet();
|
||||
IReadOnlyList<uint> components = GetAppropriateFormula(
|
||||
formula, player, playerGuid);
|
||||
foreach (uint scid in components)
|
||||
{
|
||||
if (scid == 0u) continue;
|
||||
if (!_wcidByScid.TryGetValue(scid, out uint wcid)
|
||||
|| !ownedWcids.Contains(wcid))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>ClientMagicSystem::GetAppropriateSpellFormula</c>
|
||||
/// (0x00567D50): an infused school or a directly carried school focus uses
|
||||
/// the scarab-only formula; otherwise the account-customized formula wins.
|
||||
/// </summary>
|
||||
private IReadOnlyList<uint> GetAppropriateFormula(
|
||||
SpellFormulaDefinition formula,
|
||||
ClientObject? player,
|
||||
uint playerGuid)
|
||||
{
|
||||
bool infused = SchoolInfusionProperty(formula.School) is uint propertyId
|
||||
&& player?.Properties.GetInt(propertyId) > 0;
|
||||
bool carriesMagicPack = _magicPackWcidBySchool.TryGetValue(
|
||||
formula.School, out uint packWcid)
|
||||
&& _objects.Objects.Any(item =>
|
||||
item.ContainerId == playerGuid
|
||||
&& item.WeenieClassId == packWcid);
|
||||
return infused || carriesMagicPack
|
||||
? RetailSpellFormula.InqScarabOnlyFormula(formula.ComponentIds)
|
||||
: RetailSpellFormula.CustomizeForAccount(
|
||||
formula.ComponentIds, formula.FormulaVersion, _accountName());
|
||||
}
|
||||
|
||||
private static uint? SchoolInfusionProperty(uint school) => school switch
|
||||
{
|
||||
1u => 0x129u, // AugmentationInfusedWarMagic
|
||||
2u => 0x128u, // AugmentationInfusedLifeMagic
|
||||
3u => 0x127u, // AugmentationInfusedItemMagic
|
||||
4u => 0x126u, // AugmentationInfusedCreatureMagic
|
||||
5u => 0x148u, // AugmentationInfusedVoidMagic
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private bool IsOwnedByPlayer(ClientObject item, uint playerGuid)
|
||||
{
|
||||
if (playerGuid == 0u) return false;
|
||||
ClientObject current = item;
|
||||
for (int depth = 0; depth < 16; depth++)
|
||||
{
|
||||
if (current.WielderId == playerGuid || current.ContainerId == playerGuid)
|
||||
return true;
|
||||
if (current.ContainerId == 0u
|
||||
|| _objects.Get(current.ContainerId) is not { } parent)
|
||||
return false;
|
||||
current = parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue