## Summary - Fixes pets falling behind mounted masters in AOS+ by setting `CurrentSpeed = 0.1` when following master - Fixes AI timer permanently stopping when `Obey()`/`Think()` returns `false` for transient conditions - Fixes controlled pets losing AI in inactive sectors (pet follows owner across sector boundary, sector deactivates, AI dies) - Adds defense-in-depth: AI timer restarts on pet resurrection and order changes ## AI Timer Permanent Stop (Bug Fix) `AITimer.OnTick()` called `Stop()` when `Obey()` or `Think()` returned `false`. By that point, `ShouldStop()` had already validated the creature is alive, on a valid map, and in an active sector — so any `false` return was a **transient** condition, not terminal. The timer stopped permanently with no mechanism to restart it. **Scenarios that triggered permanent AI death:** - Dead bonded pet with attack order (`DoOrderAttack` returned `false` for `IsDeadPet`) - Failed pet transfer — loyalty refusal, combat, disconnected player, or pending trade (`DoOrderTransfer` returned `false` for 5 different transient conditions) - Unknown `OrderType` or `ActionType` (defensive defaults) **Fixes:** - Removed `Stop()` from the `Obey()`/`Think()` failure path — timer skips the tick and fires again next interval - Changed `DoOrderAttack()` and all five `DoOrderTransfer()` failure paths to return `true` (correct semantics: these are recoverable states, not "stop AI forever" signals) - Added `Activate()` call in `ResurrectPet()` — ensures dead bonded pets have AI running after resurrection - Added `Activate()` call in `OnCurrentOrderChanged()` — self-heals timer if any voice command is issued to a pet with a stopped timer ## Controlled Pet Sector Deactivation (Bug Fix) `ShouldStop()` stopped the AI timer for **all** `PlayerRangeSensitive` creatures in inactive sectors, including controlled pets. But `Deactivate()` intentionally exempted controlled pets. The exemption was dead code — `ShouldStop()` bypassed it. This matters when a pet follows its owner across a sector boundary: the owner enters the next sector (active), the pet's old sector deactivates (no more players), and the pet's AI dies. The pet stops following and stands there until the player backtracks far enough to reactivate the sector. **Fix:** Added `Controlled` check to `ShouldStop()` to match `Deactivate()`. Controlled pets now keep their AI running in inactive sectors. The overhead is negligible — controlled pets are bounded by follower slots. ## Movement Speed Simplification - Simplifies `AITimer` to use `CurrentSpeed` directly as the tick interval (in seconds), removing the complex multiplier/floor logic in `GetBaseInterval` - Refactors `DoMoveImpl` speed assignment into explicit if/else for clarity - AOS+ pets following master use `CurrentSpeed = 0.1` (100ms), matching `RunMountDelay` ## Files Changed - `AITimer.cs` — removed `Stop()` on Obey/Think failure, added `Controlled` exemption to `ShouldStop()`, simplified interval logic - `BaseAI.cs` — renamed `_timer` to `AITimer` (public), simplified `Deactivate()`, fixed `ReturnToHome` to use `Activate()` - `PetOrders.cs` — `DoOrderAttack` and `DoOrderTransfer` return `true` for transient failures - `PetOrderHandlers.cs` — `OnCurrentOrderChanged()` calls `Activate()` to self-heal stopped timers - `BaseCreature.cs` — `ResurrectPet()` calls `Activate()`, fixed `GoHome_Callback` PlayerRangeSensitive check - `AIMovement.cs` — refactored speed assignment, AOS+ follow-master speed fix
118 lines
3.4 KiB
C#
118 lines
3.4 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: AITimer.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
************************************************************************/
|
|
|
|
using System;
|
|
|
|
namespace Server.Mobiles;
|
|
|
|
public sealed class AITimer : Timer
|
|
{
|
|
private readonly BaseAI _owner;
|
|
private int _detectHiddenMinDelay;
|
|
private int _detectHiddenMaxDelay;
|
|
|
|
public AITimer(BaseAI owner) : base(TimeSpan.FromMilliseconds(Utility.Random(3000)),
|
|
TimeSpan.FromSeconds(owner.Mobile.CurrentSpeed))
|
|
{
|
|
_owner = owner;
|
|
_owner._nextDetectHidden = Core.TickCount;
|
|
}
|
|
|
|
public void Activate()
|
|
{
|
|
Interval = TimeSpan.FromSeconds(_owner.Mobile.CurrentSpeed);
|
|
Start();
|
|
}
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (ShouldStop())
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
_owner.Mobile.OnThink();
|
|
|
|
if (ShouldStop())
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
Interval = TimeSpan.FromSeconds(_owner.Mobile.CurrentSpeed);
|
|
HandleBardEffects();
|
|
|
|
if (_owner.Mobile.Controlled ? !_owner.Obey() : !_owner.Think())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HandleDetectHidden();
|
|
}
|
|
|
|
private bool ShouldStop()
|
|
{
|
|
if (_owner.Mobile.Deleted)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (_owner.Mobile.Map == null || _owner.Mobile.Map == Map.Internal || _owner.Mobile.PlayerRangeSensitive &&
|
|
!_owner.Mobile.Controlled && !_owner.Mobile.Map.GetSector(_owner.Mobile.Location).Active)
|
|
{
|
|
_owner.Deactivate();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void HandleBardEffects()
|
|
{
|
|
if (_owner.Mobile.BardPacified)
|
|
{
|
|
_owner.DoBardPacified();
|
|
}
|
|
else if (_owner.Mobile.BardProvoked)
|
|
{
|
|
_owner.DoBardProvoked();
|
|
}
|
|
}
|
|
|
|
private void CacheDetectHiddenDelays()
|
|
{
|
|
var delay = Math.Min(30000 / _owner.Mobile.Int, 120);
|
|
_detectHiddenMinDelay = delay * 900; // 26s to 108s
|
|
_detectHiddenMaxDelay = delay * 1100; // 32s to 132s
|
|
}
|
|
|
|
private void HandleDetectHidden()
|
|
{
|
|
if (!_owner.CanDetectHidden || Core.TickCount - _owner._nextDetectHidden < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_owner.DetectHidden();
|
|
|
|
if (_detectHiddenMinDelay == 0 || _detectHiddenMaxDelay == 0)
|
|
{
|
|
CacheDetectHiddenDelays();
|
|
}
|
|
|
|
_owner._nextDetectHidden = Core.TickCount + Utility.RandomMinMax(_detectHiddenMinDelay, _detectHiddenMaxDelay);
|
|
}
|
|
}
|