// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.Bots; using System.Threading; using MUnique.OpenMU.GameLogic.Offline; /// /// A connection-less bot player. It reuses the whole offline-player intelligence (combat, buffs, /// healing, pickup) and adds a which makes it roam to level-appropriate /// hunting grounds instead of standing on a fixed spawn position. /// public sealed class BotPlayer : OfflinePlayer { /// /// After this many AI ticks failing in a row, the bot is considered broken and gets restarted. /// The engine's attribute system is not thread-safe, and a lost race can corrupt a character's /// attribute graph for good: every following tick throws, the bot stops playing and floods the log /// with the same exception until the server restarts. A fresh login rebuilds the graph and heals it, /// which is exactly what a player would do. The threshold is high enough that a single failing tick /// (a transient race, a monster which just died) is simply skipped, like before. /// private const int ConsecutiveFailuresUntilRestart = 20; private BotNavigator? _navigator; private int _consecutiveTickFailures; /// /// Initializes a new instance of the class. /// /// The game context. public BotPlayer(IGameContext gameContext) : base(gameContext) { } /// public override bool RespawnAndContinue => true; /// /// Gets or sets a value indicating whether this bot evolved into its master class and still needs /// the "relog" which mounts the master attributes (see ). /// Set from the bot's own tick, where the evolution runs; acted upon by the maintenance pass, which /// is the only place allowed to restart a bot (a restart from within the bot's own timer callback /// would tear down the very loop it runs in). /// public bool AwaitsMasterRestart { get; set; } /// /// Gets or sets a value indicating whether this bot's AI keeps failing and it has to be restarted /// (see ). Acted upon by the maintenance pass, which is /// the only place allowed to restart a bot. /// public bool AwaitsFaultRestart { get; set; } /// public override async ValueTask StopAsync() { await this.StopNavigatorAsync().ConfigureAwait(false); await base.StopAsync().ConfigureAwait(false); } /// internal override void OnAiTickSucceeded() { if (this._consecutiveTickFailures > 0) { Interlocked.Exchange(ref this._consecutiveTickFailures, 0); } } /// internal override void OnAiTickFailed() { if (Interlocked.Increment(ref this._consecutiveTickFailures) == ConsecutiveFailuresUntilRestart) { // Deliberately '==', not '>=': this arms the restart exactly once, at the tick that crosses // the threshold, so the log gets one line and the flag is raised once. Further failures keep // incrementing the counter (which stays above the threshold) but don't re-fire; the // maintenance pass consumes AwaitsFaultRestart and the restart resets the counter to 0. this.Logger.LogWarning( "Bot '{Name}' failed {Count} AI ticks in a row and gets restarted to heal it.", this.Name, ConsecutiveFailuresUntilRestart); this.AwaitsFaultRestart = true; } } /// protected override void StartIntelligence() { base.StartIntelligence(); this._navigator = new BotNavigator(this); this._navigator.Start(); } /// protected override async ValueTask DisposeAsyncCore() { await this.StopNavigatorAsync().ConfigureAwait(false); await base.DisposeAsyncCore().ConfigureAwait(false); } private async ValueTask StopNavigatorAsync() { if (this._navigator is { } navigator) { this._navigator = null; await navigator.DisposeAsync().ConfigureAwait(false); } } }