ace/Source/ACE.Server/WorldObjects/WorldObject.cs
Mag-nus e8284981d6
SpellbookCache (#1656)
* WIP

* SpellbookCache

This uses a dictionary to check if spells are known instead of having to iterate over every record of BiotaPropertiesSpellBook every time a player casts a spell.
2019-04-07 08:05:35 -05:00

1021 lines
41 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using log4net;
using ACE.Common;
using ACE.Common.Extensions;
using ACE.Database.Models.Shard;
using ACE.Database.Models.World;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Managers;
using ACE.Server.Network;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Network.Sequence;
using ACE.Server.Physics;
using ACE.Server.Physics.Animation;
using ACE.Server.Physics.Common;
using ACE.Server.Physics.Util;
using Landblock = ACE.Server.Entity.Landblock;
using Position = ACE.Entity.Position;
namespace ACE.Server.WorldObjects
{
public abstract partial class WorldObject : IActor
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// This is object property overrides that should have come from the shard db (or init to defaults of object is new to this instance).
/// You should not manipulate these values directly. To manipulate this use the exposed SetProperty and RemoveProperty functions instead.
/// </summary>
public Biota Biota { get; }
/// <summary>
/// This is just a wrapper around Biota.Id
/// </summary>
public ObjectGuid Guid { get; }
public PhysicsObj PhysicsObj { get; protected set; }
public bool InitPhysics { get; protected set; }
public ObjectDescriptionFlag BaseDescriptionFlags { get; protected set; }
public SequenceManager Sequences { get; } = new SequenceManager();
public virtual float ListeningRadius { get; protected set; } = 5f;
/// <summary>
/// Should only be adjusted by Landblock -- default is null
/// </summary>
public Landblock CurrentLandblock { get; internal set; }
public DateTime? ItemManaDepletionMessageTimestamp { get; set; } = null;
public DateTime? ItemManaConsumptionTimestamp { get; set; } = null;
public bool IsBusy { get; set; }
public bool IsMovingTo { get; set; }
public bool IsShield { get => CombatUse != null && CombatUse == ACE.Entity.Enum.CombatUse.Shield; }
public bool IsTwoHanded { get => CurrentWieldedLocation != null && CurrentWieldedLocation == EquipMask.TwoHanded; }
public bool IsBow { get => DefaultCombatStyle != null && (DefaultCombatStyle == CombatStyle.Bow || DefaultCombatStyle == CombatStyle.Crossbow); }
public bool IsAtlatl { get => DefaultCombatStyle != null && DefaultCombatStyle == CombatStyle.Atlatl; }
public bool IsAmmoLauncher { get => IsBow || IsAtlatl; }
public bool IsThrownWeapon { get => DefaultCombatStyle != null && DefaultCombatStyle == CombatStyle.ThrownWeapon; }
public bool IsRanged { get => IsAmmoLauncher || IsThrownWeapon; }
public EmoteManager EmoteManager;
public EnchantmentManagerWithCaching EnchantmentManager;
public WorldObject ProjectileSource;
public WorldObject ProjectileTarget;
public WorldObject Wielder;
public WorldObject() { }
/// <summary>
/// A new biota will be created taking all of its values from weenie.
/// </summary>
protected WorldObject(Weenie weenie, ObjectGuid guid)
{
Biota = weenie.CreateCopyAsBiota(guid.Full);
Guid = guid;
InitializePropertyDictionaries();
SetEphemeralValues();
InitializeHeartbeats();
CreationTimestamp = (int)Time.GetUnixTime();
}
/// <summary>
/// Restore a WorldObject from the database.
/// Any properties tagged as Ephemeral will be removed from the biota.
/// </summary>
protected WorldObject(Biota biota)
{
Biota = biota;
Guid = new ObjectGuid(Biota.Id);
biotaOriginatedFromDatabase = true;
InitializePropertyDictionaries();
SetEphemeralValues();
InitializeHeartbeats();
}
/// <summary>
/// Initializes a new default physics object
/// </summary>
public virtual void InitPhysicsObj()
{
//Console.WriteLine($"InitPhysicsObj({Name} - {Guid})");
var defaultState = CalculatedPhysicsState();
PhysicsObj = new PhysicsObj();
PhysicsObj.set_object_guid(Guid);
// will eventually map directly to WorldObject
PhysicsObj.set_weenie_obj(new WeenieObject(this));
var creature = this as Creature;
if (creature == null)
{
var isDynamic = Static == null || !Static.Value;
PhysicsObj = PhysicsObj.makeObject(SetupTableId, Guid.Full, isDynamic);
PhysicsObj.set_weenie_obj(new WeenieObject(this));
}
else
PhysicsObj.makeAnimObject(SetupTableId, true);
PhysicsObj.SetMotionTableID(MotionTableId);
PhysicsObj.SetScaleStatic(ObjScale ?? 1.0f);
PhysicsObj.State = defaultState;
// gaerlan rolling balls of death
if (Name.Equals("Rolling Death"))
{
PhysicsObj.SetScaleStatic(1.0f);
PhysicsObj.State |= PhysicsState.Ethereal;
}
//if (creature != null) AllowEdgeSlide = true;
}
public bool AddPhysicsObj()
{
if (PhysicsObj.CurCell != null)
return false;
AdjustDungeon(Location);
// exclude linkspots from spawning
if (WeenieClassId == 10762) return true;
var cell = LScape.get_landcell(Location.Cell);
if (cell == null) return false;
PhysicsObj.Position.ObjCellID = cell.ID;
var location = new Physics.Common.Position();
location.ObjCellID = cell.ID;
location.Frame.Origin = Location.Pos;
location.Frame.Orientation = Location.Rotation;
var success = PhysicsObj.enter_world(location);
if (!success)
{
//Console.WriteLine($"AddPhysicsObj: failure: {Name} @ {cell.ID.ToString("X8")} - {Location.Pos} - {Location.Rotation} - SetupID: {SetupTableId.ToString("X8")}, MTableID: {MotionTableId.ToString("X8")}");
return false;
}
//Console.WriteLine($"AddPhysicsObj: success: {Name} ({Guid})");
Location.LandblockId = new LandblockId(PhysicsObj.Position.ObjCellID);
Location.Pos = PhysicsObj.Position.Frame.Origin;
Location.Rotation = PhysicsObj.Position.Frame.Orientation;
SetPosition(PositionType.Home, new Position(Location));
return true;
}
private void InitializePropertyDictionaries()
{
foreach (var x in Biota.BiotaPropertiesBool)
biotaPropertyBools[(PropertyBool)x.Type] = x;
foreach (var x in Biota.BiotaPropertiesDID)
biotaPropertyDataIds[(PropertyDataId)x.Type] = x;
foreach (var x in Biota.BiotaPropertiesFloat)
biotaPropertyFloats[(PropertyFloat)x.Type] = x;
foreach (var x in Biota.BiotaPropertiesIID)
biotaPropertyInstanceIds[(PropertyInstanceId)x.Type] = x;
foreach (var x in Biota.BiotaPropertiesInt)
biotaPropertyInts[(PropertyInt)x.Type] = x;
foreach (var x in Biota.BiotaPropertiesInt64)
biotaPropertyInt64s[(PropertyInt64)x.Type] = x;
foreach (var x in Biota.BiotaPropertiesString)
biotaPropertyStrings[(PropertyString)x.Type] = x;
foreach (var x in EphemeralProperties.PropertiesBool.ToList())
ephemeralPropertyBools.TryAdd((PropertyBool)x, null);
foreach (var x in EphemeralProperties.PropertiesDataId.ToList())
ephemeralPropertyDataIds.TryAdd((PropertyDataId)x, null);
foreach (var x in EphemeralProperties.PropertiesDouble.ToList())
ephemeralPropertyFloats.TryAdd((PropertyFloat)x, null);
foreach (var x in EphemeralProperties.PropertiesInstanceId.ToList())
ephemeralPropertyInstanceIds.TryAdd((PropertyInstanceId)x, null);
foreach (var x in EphemeralProperties.PropertiesInt.ToList())
ephemeralPropertyInts.TryAdd((PropertyInt)x, null);
foreach (var x in EphemeralProperties.PropertiesInt64.ToList())
ephemeralPropertyInt64s.TryAdd((PropertyInt64)x, null);
foreach (var x in EphemeralProperties.PropertiesString.ToList())
ephemeralPropertyStrings.TryAdd((PropertyString)x, null);
foreach (var x in Biota.BiotaPropertiesSpellBook)
BiotaPropertySpells[x.Spell] = x;
}
private void SetEphemeralValues()
{
foreach (var x in Biota.BiotaPropertiesBool.Where(i => EphemeralProperties.PropertiesBool.Contains(i.Type)).ToList())
ephemeralPropertyBools[(PropertyBool)x.Type] = x.Value;
foreach (var x in Biota.BiotaPropertiesDID.Where(i => EphemeralProperties.PropertiesDataId.Contains(i.Type)).ToList())
ephemeralPropertyDataIds[(PropertyDataId)x.Type] = x.Value;
foreach (var x in Biota.BiotaPropertiesFloat.Where(i => EphemeralProperties.PropertiesDouble.Contains(i.Type)).ToList())
ephemeralPropertyFloats[(PropertyFloat)x.Type] = x.Value;
foreach (var x in Biota.BiotaPropertiesIID.Where(i => EphemeralProperties.PropertiesInstanceId.Contains(i.Type)).ToList())
ephemeralPropertyInstanceIds[(PropertyInstanceId)x.Type] = x.Value;
foreach (var x in Biota.BiotaPropertiesInt.Where(i => EphemeralProperties.PropertiesInt.Contains(i.Type)).ToList())
ephemeralPropertyInts[(PropertyInt)x.Type] = x.Value;
foreach (var x in Biota.BiotaPropertiesInt64.Where(i => EphemeralProperties.PropertiesInt64.Contains(i.Type)).ToList())
ephemeralPropertyInt64s[(PropertyInt64)x.Type] = x.Value;
foreach (var x in Biota.BiotaPropertiesString.Where(i => EphemeralProperties.PropertiesString.Contains(i.Type)).ToList())
ephemeralPropertyStrings[(PropertyString)x.Type] = x.Value;
foreach (var x in EphemeralProperties.PositionTypes.ToList())
ephemeralPositions.TryAdd((PositionType)x, null);
foreach (var x in Biota.BiotaPropertiesPosition.Where(i => EphemeralProperties.PositionTypes.Contains(i.PositionType)).ToList())
ephemeralPositions[(PositionType)x.PositionType] = new Position(x.ObjCellId, x.OriginX, x.OriginY, x.OriginZ, x.AnglesX, x.AnglesY, x.AnglesZ, x.AnglesW);
AddGeneratorProfiles();
BaseDescriptionFlags = ObjectDescriptionFlag.Attackable;
EmoteManager = new EmoteManager(this);
EnchantmentManager = new EnchantmentManagerWithCaching(this);
if (Placement == null)
Placement = ACE.Entity.Enum.Placement.Resting;
//CurrentMotionState = new Motion(MotionStance.Invalid, new MotionItem(MotionCommand.Invalid));
}
/// <summary>
/// This will be true when teleporting
/// </summary>
public bool Teleporting { get; set; } = false;
public bool HandleNPCReceiveItem(WorldObject item, WorldObject giver, out BiotaPropertiesEmote emote)
{
// NPC refuses this item, with a custom response
var refuseItem = EmoteManager.GetEmoteSet(EmoteCategory.Refuse, null, null, item.WeenieClassId);
if (refuseItem != null)
{
emote = refuseItem;
EmoteManager.ExecuteEmoteSet(refuseItem, giver);
return true;
}
// NPC accepts this item
var giveItem = EmoteManager.GetEmoteSet(EmoteCategory.Give, null, null, item.WeenieClassId);
if (giveItem != null)
{
emote = giveItem;
EmoteManager.ExecuteEmoteSet(giveItem, giver);
return true;
}
emote = null;
return false;
}
/// <summary>
/// Returns TRUE if this object has the input object in its PVS
/// Note that this is NOT a direct line of sight test!
/// </summary>
public bool IsVisible(WorldObject wo)
{
if (PhysicsObj == null || wo.PhysicsObj == null)
return false;
// note: visibility lists are actively maintained only for players
return PhysicsObj.ObjMaint.VisibleObjectTable.ContainsKey(wo.PhysicsObj.ID);
}
public static PhysicsObj SightObj = PhysicsObj.makeObject(0x02000124, 0, false, true); // arrow
/// <summary>
/// Returns TRUE if this object has direct line-of-sight visibility to input object
/// </summary>
public bool IsDirectVisible(WorldObject wo)
{
if (PhysicsObj == null || wo.PhysicsObj == null)
return false;
var startPos = new Physics.Common.Position(PhysicsObj.Position);
var targetPos = new Physics.Common.Position(wo.PhysicsObj.Position);
// set to eye level
startPos.Frame.Origin.Z += PhysicsObj.GetHeight() - SightObj.GetHeight();
targetPos.Frame.Origin.Z += wo.PhysicsObj.GetHeight() - SightObj.GetHeight();
var dir = Vector3.Normalize(targetPos.Frame.Origin - startPos.Frame.Origin);
var radsum = PhysicsObj.GetPhysicsRadius() + SightObj.GetPhysicsRadius();
startPos.Frame.Origin += dir * radsum;
SightObj.CurCell = PhysicsObj.CurCell;
SightObj.ProjectileTarget = wo.PhysicsObj;
// perform line of sight test
var transition = SightObj.transition(startPos, targetPos, false);
if (transition == null) return false;
// check if target object was reached
var isVisible = transition.CollisionInfo.CollideObject.FirstOrDefault(c => c.ID == wo.PhysicsObj.ID) != null;
return isVisible;
}
public bool IsDirectVisible(WorldObject wo, Position pos)
{
if (PhysicsObj == null)
return false;
var startPos = new Physics.Common.Position(PhysicsObj.Position);
var targetPos = new Physics.Common.Position(pos);
// set to eye level
startPos.Frame.Origin.Z += PhysicsObj.GetHeight() - SightObj.GetHeight();
targetPos.Frame.Origin.Z += SightObj.GetHeight();
var dir = Vector3.Normalize(targetPos.Frame.Origin - startPos.Frame.Origin);
var radsum = PhysicsObj.GetPhysicsRadius() + SightObj.GetPhysicsRadius();
startPos.Frame.Origin += dir * radsum;
SightObj.CurCell = PhysicsObj.CurCell;
SightObj.ProjectileTarget = PhysicsObj;
// perform line of sight test
var transition = SightObj.transition(targetPos, startPos, false);
if (transition == null) return false;
// check if target object was reached
var isVisible = transition.CollisionInfo.CollideObject.FirstOrDefault(c => c.ID == PhysicsObj.ID) != null;
return isVisible;
}
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
// ******************************************************************* OLD CODE BELOW ********************************
public static float MaxObjectTrackingRange { get; } = 20000f;
public Position ForcedLocation { get; private set; }
public Position RequestedLocation { get; set; }
public Position PreviousLocation { get; set; }
/// <summary>
/// Time when this object will despawn, -1 is never.
/// </summary>
public double DespawnTime { get; set; } = -1;
/// <summary>
/// tick-stamp for the server time of the last time the player moved.
/// TODO: implement
/// </summary>
public double LastAnimatedTicks { get; set; }
public virtual void PlayScript(Session session) { }
////// Logical Game Data
public ContainerType ContainerType
{
get
{
if (WeenieType == WeenieType.Container)
return ContainerType.Container;
else if (RequiresBackpackSlot ?? false)
return ContainerType.Foci;
else
return ContainerType.NonContainer;
}
}
public void ReadBookPage(Session reader, uint pageNum)
{
//PageData pageData = new PageData();
//AceObjectPropertiesBook bookPage = PropertiesBook[pageNum];
//pageData.AuthorID = bookPage.AuthorId;
//pageData.AuthorName = bookPage.AuthorName;
//pageData.AuthorAccount = bookPage.AuthorAccount;
//pageData.PageIdx = pageNum;
//pageData.PageText = bookPage.PageText;
//pageData.IgnoreAuthor = false;
//// TODO - check for PropertyBool.IgnoreAuthor flag
//var bookDataResponse = new GameEventBookPageDataResponse(reader, Guid.Full, pageData);
//reader.Network.EnqueueSend(bookDataResponse);
}
public string DebugOutputString(WorldObject obj)
{
var sb = new StringBuilder();
sb.AppendLine("ACE Debug Output:");
sb.AppendLine("ACE Class File: " + GetType().Name + ".cs");
sb.AppendLine("Guid: " + obj.Guid.Full + " (0x" + obj.Guid.Full.ToString("X") + ")");
sb.AppendLine("----- Private Fields -----");
foreach (var prop in obj.GetType().GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).OrderBy(field => field.Name))
{
if (prop.GetValue(obj) == null)
continue;
sb.AppendLine($"{prop.Name.Replace("<", "").Replace(">k__BackingField", "")} = {prop.GetValue(obj)}");
}
sb.AppendLine("----- Public Properties -----");
foreach (var prop in obj.GetType().GetProperties().OrderBy(property => property.Name))
{
if (prop.GetValue(obj, null) == null)
continue;
switch (prop.Name.ToLower())
{
case "guid":
sb.AppendLine($"{prop.Name} = {obj.Guid.Full} (GuidType.{obj.Guid.Type.ToString()})");
break;
case "descriptionflags":
var descriptionFlags = CalculatedDescriptionFlag();
sb.AppendLine($"{prop.Name} = {descriptionFlags.ToString()}" + " (" + (uint)descriptionFlags + ")");
break;
case "weenieflags":
var weenieFlags = CalculatedWeenieHeaderFlag();
sb.AppendLine($"{prop.Name} = {weenieFlags.ToString()}" + " (" + (uint)weenieFlags + ")");
break;
case "weenieflags2":
var weenieFlags2 = CalculatedWeenieHeaderFlag2();
sb.AppendLine($"{prop.Name} = {weenieFlags2.ToString()}" + " (" + (uint)weenieFlags2 + ")");
break;
case "itemtype":
sb.AppendLine($"{prop.Name} = {obj.ItemType.ToString()}" + " (" + (uint)obj.ItemType + ")");
break;
case "creaturetype":
sb.AppendLine($"{prop.Name} = {obj.CreatureType.ToString()}" + " (" + (uint)obj.CreatureType + ")");
break;
case "containertype":
sb.AppendLine($"{prop.Name} = {obj.ContainerType.ToString()}" + " (" + (uint)obj.ContainerType + ")");
break;
case "usable":
sb.AppendLine($"{prop.Name} = {obj.Usable.ToString()}" + " (" + (uint)obj.Usable + ")");
break;
case "radarbehavior":
sb.AppendLine($"{prop.Name} = {obj.RadarBehavior.ToString()}" + " (" + (uint)obj.RadarBehavior + ")");
break;
case "physicsdescriptionflag":
var physicsDescriptionFlag = CalculatedPhysicsDescriptionFlag();
sb.AppendLine($"{prop.Name} = {physicsDescriptionFlag.ToString()}" + " (" + (uint)physicsDescriptionFlag + ")");
break;
case "physicsstate":
var physicsState = PhysicsObj.State;
sb.AppendLine($"{prop.Name} = {physicsState.ToString()}" + " (" + (uint)physicsState + ")");
break;
//case "propertiesspellid":
// foreach (var item in obj.PropertiesSpellId)
// {
// sb.AppendLine($"PropertySpellId.{Enum.GetName(typeof(Spell), item.SpellId)} ({item.SpellId})");
// }
// break;
case "validlocations":
sb.AppendLine($"{prop.Name} = {obj.ValidLocations}" + " (" + (uint)obj.ValidLocations + ")");
break;
case "currentwieldedlocation":
sb.AppendLine($"{prop.Name} = {obj.CurrentWieldedLocation}" + " (" + (uint)obj.CurrentWieldedLocation + ")");
break;
case "priority":
sb.AppendLine($"{prop.Name} = {obj.ClothingPriority}" + " (" + (uint)obj.ClothingPriority + ")");
break;
case "radarcolor":
sb.AppendLine($"{prop.Name} = {obj.RadarColor}" + " (" + (uint)obj.RadarColor + ")");
break;
case "location":
sb.AppendLine($"{prop.Name} = {obj.Location.ToLOCString()}");
break;
case "destination":
sb.AppendLine($"{prop.Name} = {obj.Destination.ToLOCString()}");
break;
case "instantiation":
sb.AppendLine($"{prop.Name} = {obj.Instantiation.ToLOCString()}");
break;
case "sanctuary":
sb.AppendLine($"{prop.Name} = {obj.Sanctuary.ToLOCString()}");
break;
case "home":
sb.AppendLine($"{prop.Name} = {obj.Home.ToLOCString()}");
break;
case "portalsummonloc":
sb.AppendLine($"{prop.Name} = {obj.PortalSummonLoc.ToLOCString()}");
break;
case "houseboot":
sb.AppendLine($"{prop.Name} = {obj.HouseBoot.ToLOCString()}");
break;
case "lastoutsidedeath":
sb.AppendLine($"{prop.Name} = {obj.LastOutsideDeath.ToLOCString()}");
break;
case "linkedlifestone":
sb.AppendLine($"{prop.Name} = {obj.LinkedLifestone.ToLOCString()}");
break;
case "channelsactive":
sb.AppendLine($"{prop.Name} = {(Channel)obj.GetProperty(PropertyInt.ChannelsActive)}" + " (" + (uint)obj.GetProperty(PropertyInt.ChannelsActive) + ")");
break;
case "channelsallowed":
sb.AppendLine($"{prop.Name} = {(Channel)obj.GetProperty(PropertyInt.ChannelsAllowed)}" + " (" + (uint)obj.GetProperty(PropertyInt.ChannelsAllowed) + ")");
break;
case "playerkillerstatus":
sb.AppendLine($"{prop.Name} = {obj.PlayerKillerStatus}" + " (" + (uint)obj.PlayerKillerStatus + ")");
break;
default:
sb.AppendLine($"{prop.Name} = {prop.GetValue(obj, null)}");
break;
}
}
sb.AppendLine("----- Property Dictionaries -----");
foreach (var item in obj.GetAllPropertyBools())
sb.AppendLine($"PropertyBool.{Enum.GetName(typeof(PropertyBool), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyDataId())
sb.AppendLine($"PropertyDataId.{Enum.GetName(typeof(PropertyDataId), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyFloat())
sb.AppendLine($"PropertyFloat.{Enum.GetName(typeof(PropertyFloat), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyInstanceId())
sb.AppendLine($"PropertyInstanceId.{Enum.GetName(typeof(PropertyInstanceId), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyInt())
sb.AppendLine($"PropertyInt.{Enum.GetName(typeof(PropertyInt), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyInt64())
sb.AppendLine($"PropertyInt64.{Enum.GetName(typeof(PropertyInt64), item.Key)} ({(int)item.Key}) = {item.Value}");
foreach (var item in obj.GetAllPropertyString())
sb.AppendLine($"PropertyString.{Enum.GetName(typeof(PropertyString), item.Key)} ({(int)item.Key}) = {item.Value}");
sb.AppendLine("\n");
return sb.ToString().Replace("\r", "");
}
public void QueryHealth(Session examiner)
{
float healthPercentage = 1f;
if (this is Creature creature)
healthPercentage = (float)creature.Health.Current / (float)creature.Health.MaxValue;
var updateHealth = new GameEventUpdateHealth(examiner, Guid.Full, healthPercentage);
examiner.Network.EnqueueSend(updateHealth);
}
public void QueryItemMana(Session examiner)
{
float manaPercentage = 1f;
uint success = 0;
if (ItemCurMana != null && ItemMaxMana != null)
{
manaPercentage = (float)ItemCurMana / (float)ItemMaxMana;
success = 1;
}
if (success == 0) // according to retail PCAPs, if success = 0, mana = 0.
manaPercentage = 0;
var updateMana = new GameEventQueryItemManaResponse(examiner, Guid.Full, manaPercentage, success);
examiner.Network.EnqueueSend(updateMana);
}
public void EnqueueBroadcastPhysicsState()
{
if (PhysicsObj != null)
EnqueueBroadcast(new GameMessageSetState(this, PhysicsObj.State));
}
public void EnqueueBroadcastUpdateObject()
{
EnqueueBroadcast(new GameMessageUpdateObject(this));
}
public virtual void OnCollideObject(WorldObject target)
{
// thrown weapons
if (ProjectileTarget == null) return;
var proj = new Projectile(this);
proj.OnCollideObject(target);
}
public virtual void OnCollideObjectEnd(WorldObject target)
{
// empty base
}
public virtual void OnCollideEnvironment()
{
// thrown weapons
if (ProjectileTarget == null) return;
var proj = new Projectile(this);
proj.OnCollideEnvironment();
}
public void EnqueueBroadcastMotion(Motion motion, float? maxRange = null)
{
var msg = new GameMessageUpdateMotion(this, motion);
if (maxRange == null)
EnqueueBroadcast(msg);
else
EnqueueBroadcast(msg, maxRange.Value);
}
public void ApplyVisualEffects(PlayScript effect, float speed = 1)
{
if (CurrentLandblock != null)
PlayParticleEffect(effect, Guid, speed);
}
// plays particle effect like spell casting or bleed etc..
public void PlayParticleEffect(PlayScript effectId, ObjectGuid targetId, float speed = 1)
{
EnqueueBroadcast(new GameMessageScript(targetId, effectId, speed));
}
public void ApplySoundEffects(Sound sound, float volume = 1)
{
if (CurrentLandblock != null)
PlaySoundEffect(sound, Guid, volume);
}
public void PlaySoundEffect(Sound soundId, ObjectGuid targetId, float volume = 1)
{
EnqueueBroadcast(new GameMessageSound(targetId, soundId, volume));
}
public virtual void EnterWorld()
{
if (Location != null)
{
LandblockManager.AddObject(this);
if (SuppressGenerateEffect != true)
ApplyVisualEffects(ACE.Entity.Enum.PlayScript.Create);
}
}
public void AdjustDungeon(Position pos)
{
AdjustDungeonPos(pos);
AdjustDungeonCells(pos);
}
public bool AdjustDungeonCells(Position pos)
{
if (pos == null) return false;
var landblock = LScape.get_landblock(pos.Cell);
if (landblock == null || !landblock.IsDungeon) return false;
var dungeonID = pos.Cell >> 16;
var adjustCell = AdjustCell.Get(dungeonID);
var cellID = adjustCell.GetCell(pos.Pos);
if (cellID != null && pos.Cell != cellID.Value)
{
pos.LandblockId = new LandblockId(cellID.Value);
return true;
}
return false;
}
public bool AdjustDungeonPos(Position pos)
{
if (pos == null) return false;
var landblock = LScape.get_landblock(pos.Cell);
if (landblock == null || !landblock.IsDungeon) return false;
var dungeonID = pos.Cell >> 16;
var adjusted = AdjustPos.Adjust(dungeonID, pos);
return adjusted;
}
/// <summary>
/// Returns a strike message based on damage type and severity
/// </summary>
public virtual string GetAttackMessage(Creature creature, DamageType damageType, uint amount)
{
var percent = (float)amount / creature.Health.Base;
string verb = null, plural = null;
Strings.GetAttackVerb(damageType, percent, ref verb, ref plural);
var type = damageType.GetName().ToLower();
return $"You {verb} {creature.Name} for {amount} points of {type} damage!";
}
public Dictionary<PropertyInt, int?> GetProperties(WorldObject wo)
{
var props = new Dictionary<PropertyInt, int?>();
var fields = Enum.GetValues(typeof(PropertyInt)).Cast<PropertyInt>();
foreach (var field in fields)
{
var prop = wo.GetProperty(field);
props.Add(field, prop);
}
return props;
}
/// <summary>
/// Returns the base damage for a weapon
/// </summary>
public virtual Range GetBaseDamage()
{
var maxDamage = GetProperty(PropertyInt.Damage) ?? 0;
var variance = GetProperty(PropertyFloat.DamageVariance) ?? 0;
var minDamage = maxDamage * (1.0f - variance);
return new Range((float)minDamage, (float)maxDamage);
}
/// <summary>
/// Returns the modified damage for a weapon,
/// with the wielder enchantments taken into account
/// </summary>
public Range GetDamageMod(Creature wielder)
{
var baseDamage = GetBaseDamage();
var weapon = wielder.GetEquippedWeapon();
var damageMod = 0.0f;
var varianceMod = 1.0f;
if (weapon != null)
{
damageMod += weapon.EnchantmentManager.GetDamageMod();
varianceMod *= weapon.EnchantmentManager.GetVarianceMod();
if (weapon.IsEnchantable)
{
// factor in wielder auras for enchantable weapons
damageMod += wielder.EnchantmentManager.GetDamageMod();
varianceMod *= wielder.EnchantmentManager.GetVarianceMod();
}
}
var baseVariance = 1.0f - (baseDamage.Min / baseDamage.Max);
var damageBonus = 1.0f;
if (weapon != null)
{
damageBonus = (float)(weapon.GetProperty(PropertyFloat.DamageMod) ?? 1.0f) * weapon.EnchantmentManager.GetDamageModifier();
if (weapon.IsEnchantable)
damageBonus *= wielder.EnchantmentManager.GetDamageModifier();
}
// additives first, then multipliers?
var maxDamageMod = (baseDamage.Max + damageMod) * damageBonus;
var minDamageMod = maxDamageMod * (1.0f - baseVariance * varianceMod);
return new Range(minDamageMod, maxDamageMod);
}
/// <summary>
/// Returns the damage type for the currently equipped weapon / ammo
/// </summary>
/// <param name="multiple">If true, returns all of the damage types for the weapon</param>
public virtual DamageType GetDamageType(bool multiple = false)
{
var creature = this as Creature;
if (creature == null)
{
Console.WriteLine("WorldObject.GetDamageType(): null creature");
return DamageType.Undef;
}
var weapon = creature.GetEquippedWeapon();
var ammo = creature.GetEquippedAmmo();
if (weapon == null)
return DamageType.Bludgeon;
DamageType damageTypes;
var attackType = creature.GetCombatType();
if (attackType == CombatType.Melee || ammo == null || !weapon.IsAmmoLauncher)
damageTypes = (DamageType)(weapon.GetProperty(PropertyInt.DamageType) ?? 0);
else
damageTypes = (DamageType)(ammo.GetProperty(PropertyInt.DamageType) ?? 0);
// returning multiple damage types
if (multiple) return damageTypes;
// get single damage type
var motion = creature.CurrentMotionState.MotionState.ForwardCommand.ToString();
foreach (DamageType damageType in Enum.GetValues(typeof(DamageType)))
{
if ((damageTypes & damageType) != 0)
{
// handle multiple damage types
if (damageType == DamageType.Slash && motion.Contains("Thrust"))
continue;
return damageType;
}
}
return damageTypes;
}
private bool isDestroyed;
/// <summary>
/// If this is a container or a creature, all of the inventory and/or equipped objects will also be destroyed.<para />
/// An object should only be destroyed once.
/// </summary>
public virtual void Destroy(bool raiseNotifyOfDestructionEvent = true)
{
if (isDestroyed)
{
log.WarnFormat("Item 0x{0:X8}:{1} called destroy more than once.", Guid.Full, Name);
return;
}
isDestroyed = true;
if (this is Container container)
{
foreach (var item in container.Inventory.Values)
item.Destroy();
}
if (this is Creature creature)
{
foreach (var item in creature.EquippedObjects.Values)
item.Destroy();
}
if (this is CombatPet combatPet)
{
if (combatPet.P_PetOwner != null && combatPet.P_PetOwner.CurrentActiveCombatPet == this)
combatPet.P_PetOwner.CurrentActiveCombatPet = null;
}
if (raiseNotifyOfDestructionEvent)
NotifyOfEvent(RegenerationType.Destruction);
CurrentLandblock?.RemoveWorldObject(Guid);
RemoveBiotaFromDatabase();
if (Guid.IsDynamic())
GuidManager.RecycleDynamicGuid(Guid);
}
public void FadeOutAndDestroy(bool raiseNotifyOfDestructionEvent = true)
{
EnqueueBroadcast(new GameMessageScript(Guid, ACE.Entity.Enum.PlayScript.Destroy));
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(1.0f);
actionChain.AddAction(this, () => Destroy(raiseNotifyOfDestructionEvent));
actionChain.EnqueueChain();
}
public string GetPluralName()
{
var pluralName = PluralName;
if (pluralName == null)
pluralName = Name.Pluralize();
return pluralName;
}
/// <summary>
/// Returns TRUE if this object has a non-zero velocity
/// </summary>
public bool IsMoving { get => PhysicsObj != null && (PhysicsObj.Velocity.X != 0 || PhysicsObj.Velocity.Y != 0 || PhysicsObj.Velocity.Z != 0); }
/// <summary>
/// Returns TRUE if this object has non-cyclic animations in progress
/// </summary>
public bool IsAnimating { get => PhysicsObj != null && PhysicsObj.IsAnimating; }
/// <summary>
/// Executes a motion/animation for this object
/// adds to the physics animation system, and broadcasts to nearby players
/// </summary>
/// <returns>The amount it takes to execute the motion</returns>
public float ExecuteMotion(Motion motion, bool sendClient = true, float? maxRange = null)
{
var motionCommand = motion.MotionState.ForwardCommand;
if (motionCommand == MotionCommand.Invalid)
motionCommand = (MotionCommand)motion.Stance;
// run motion command on server through physics animation system
if (PhysicsObj != null && motionCommand != MotionCommand.Invalid)
{
var motionInterp = PhysicsObj.get_minterp();
var rawState = new RawMotionState();
rawState.ForwardCommand = 0; // always 0? must be this for monster sleep animations (skeletons, golems)
// else the monster will immediately wake back up..
rawState.CurrentHoldKey = HoldKey.Run;
rawState.CurrentStyle = (uint)motionCommand;
motionInterp.RawState = rawState;
motionInterp.apply_raw_movement(true, true);
}
// hardcoded ready?
var animLength = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, CurrentMotionState.MotionState.ForwardCommand, motionCommand);
CurrentMotionState = motion;
// broadcast to nearby players
if (sendClient)
EnqueueBroadcastMotion(motion, maxRange);
return animLength;
}
public void SetStance(MotionStance stance, bool broadcast = true)
{
CurrentMotionState = new Motion(stance);
if (broadcast)
EnqueueBroadcastMotion(CurrentMotionState);
}
/// <summary>
/// Returns TRUE if this WorldObject is a generic linkspot
/// Linkspots are used for things like Houses,
/// where the portal destination should be populated at runtime.
/// </summary>
public bool IsLinkSpot => WeenieType == WeenieType.Generic && WeenieClassName.Equals("portaldestination");
public static readonly float LocalBroadcastRange = 96.0f;
public SetPosition ScatterPos;
public DestinationType DestinationType;
public Skill ConvertToMoASkill(Skill skill)
{
if (this is Player player)
{
if (SkillExtensions.RetiredMelee.Contains(skill))
return player.GetHighestMeleeSkill();
if (SkillExtensions.RetiredMissile.Contains(skill))
return Skill.MissileWeapons;
}
return skill;
}
}
}