using System; using System.Collections.Generic; using System.Numerics; using log4net; using ACE.Common; using ACE.DatLoader; using ACE.DatLoader.FileTypes; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Managers; using ACE.Server.Physics.Animation; namespace ACE.Server.WorldObjects { /// /// A passive summonable creature /// public class Pet : Creature { public Player P_PetOwner; public PetDevice P_PetDevice; private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public uint? PetDevice { get => GetProperty(PropertyInstanceId.PetDevice); set { if (value.HasValue) SetProperty(PropertyInstanceId.PetDevice, value.Value); else RemoveProperty(PropertyInstanceId.PetDevice); } } /// /// A new biota be created taking all of its values from weenie. /// public Pet(Weenie weenie, ObjectGuid guid) : base(weenie, guid) { SetEphemeralValues(); } /// /// Restore a WorldObject from the database. /// public Pet(Biota biota) : base(biota) { SetEphemeralValues(); } private void SetEphemeralValues() { Ethereal = true; RadarBehavior = ACE.Entity.Enum.RadarBehavior.ShowNever; ItemUseable = Usable.No; SuppressGenerateEffect = true; } public virtual bool? Init(Player player, PetDevice petDevice) { var result = HandleCurrentActivePet(player); if (result == null || !result.Value) return result; // get physics radius of player and pet var playerRadius = player.PhysicsObj.GetPhysicsRadius(); var petRadius = GetPetRadius(); var spawnDist = playerRadius + petRadius + MinDistance; if (IsPassivePet) { Location = player.Location.InFrontOf(spawnDist, true); TimeToRot = -1; } else { Location = player.Location.InFrontOf(spawnDist, false); } Location.LandblockId = new LandblockId(Location.GetCell()); Name = player.Name + "'s " + Name; PetOwner = player.Guid.Full; P_PetOwner = player; // All pets don't leave corpses, this maybe should have been in data, but isn't so lets make sure its true. NoCorpse = true; var success = EnterWorld(); if (!success) { player.SendTransientError($"Couldn't spawn {Name}"); return false; } player.CurrentActivePet = this; petDevice.Pet = Guid.Full; PetDevice = petDevice.Guid.Full; P_PetDevice = petDevice; if (IsPassivePet) nextSlowTickTime = Time.GetUnixTime(); return true; } public bool? HandleCurrentActivePet(Player player) { if (PropertyManager.GetBool("pet_stow_replace").Item) return HandleCurrentActivePet_Replace(player); else return HandleCurrentActivePet_Retail(player); } public bool HandleCurrentActivePet_Replace(Player player) { // original ace logic if (player.CurrentActivePet == null) return true; if (player.CurrentActivePet is CombatPet) { // possibly add the ability to stow combat pets with passive pet devices here? player.SendTransientError($"{player.CurrentActivePet.Name} is already active"); return false; } var stowPet = WeenieClassId == player.CurrentActivePet.WeenieClassId; // despawn passive pet player.CurrentActivePet.Destroy(); return !stowPet; } public bool? HandleCurrentActivePet_Retail(Player player) { if (player.CurrentActivePet == null) return true; if (IsPassivePet) { // using a passive pet device // stow currently active passive/combat pet, as per retail // spawning the new passive pet requires another double click player.CurrentActivePet.Destroy(); } else { // using a combat pet device if (player.CurrentActivePet is CombatPet) { player.SendTransientError($"{player.CurrentActivePet.Name} is already active"); } else { // stow currently active passive pet // stowing the currently active passive pet w/ a combat pet device will unfortunately start the cooldown timer (and decrease the structure?) on the combat pet device, as per retail // spawning the combat pet will require another double click in ~45s, as per retail player.CurrentActivePet.Destroy(); return null; } } return false; } /// /// Called 5x per second for passive pets /// public void Tick(double currentUnixTime) { NextMonsterTickTime = currentUnixTime + monsterTickInterval; if (IsMoving) { PhysicsObj.update_object(); UpdatePosition_SyncLocation(); SendUpdatePosition(); } if (currentUnixTime >= nextSlowTickTime) SlowTick(currentUnixTime); } private const double slowTickSeconds = 1.0; private double nextSlowTickTime; /// /// Called 1x per second /// public void SlowTick(double currentUnixTime) { //Console.WriteLine($"{Name}.HeartbeatStatic({currentUnixTime})"); nextSlowTickTime += slowTickSeconds; if (P_PetOwner?.PhysicsObj == null) { log.Error($"{Name} ({Guid}).SlowTick() - P_PetOwner: {P_PetOwner}, P_PetOwner.PhysicsObj: {P_PetOwner?.PhysicsObj}"); Destroy(); return; } var dist = GetCylinderDistance(P_PetOwner); if (dist > MaxDistance) Destroy(); if (!IsMoving && dist > MinDistance) StartFollow(); } // if the passive pet is between min-max distance to owner, // it will turn and start running torwards its owner private const float MinDistance = 2.0f; private const float MaxDistance = 192.0f; private void StartFollow() { // similar to Monster_Navigation.StartTurn() //Console.WriteLine($"{Name}.StartFollow()"); IsMoving = true; // broadcast to clients MoveTo(P_PetOwner, RunRate); // perform movement on server var mvp = new MovementParameters(); mvp.DistanceToObject = MinDistance; mvp.WalkRunThreshold = 0.0f; //mvp.UseFinalHeading = true; PhysicsObj.MoveToObject(P_PetOwner.PhysicsObj, mvp); // prevent snap forward PhysicsObj.UpdateTime = Physics.Common.PhysicsTimer.CurrentTime; } /// /// Broadcasts passive pet movement to clients /// public override void MoveTo(WorldObject target, float runRate = 1.0f) { if (!IsPassivePet) { base.MoveTo(target, runRate); return; } if (MoveSpeed == 0.0f) GetMovementSpeed(); var motion = new Motion(this, target, MovementType.MoveToObject); motion.MoveToParameters.MovementParameters |= MovementParams.CanCharge; motion.MoveToParameters.DistanceToObject = MinDistance; motion.MoveToParameters.WalkRunThreshold = 0.0f; motion.RunRate = RunRate; CurrentMotionState = motion; EnqueueBroadcastMotion(motion); } /// /// Called when the MoveTo process has completed /// public override void OnMoveComplete(WeenieError status) { //Console.WriteLine($"{Name}.OnMoveComplete({status})"); if (!IsPassivePet) { base.OnMoveComplete(status); return; } if (status != WeenieError.None) return; PhysicsObj.CachedVelocity = Vector3.Zero; IsMoving = false; } public static Dictionary PetRadiusCache = new Dictionary(); private float GetPetRadius() { if (PetRadiusCache.TryGetValue(WeenieClassId, out var radius)) return radius; var setup = DatManager.PortalDat.ReadFromDat(SetupTableId); var scale = ObjScale ?? 1.0f; return ProjectileRadiusCache[WeenieClassId] = setup.Spheres[0].Radius * scale; } } }