#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you 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. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using OpenRA.Graphics; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.Player)] [Desc("Automatically activates a classic AI bot for the local human player so the match starts as bot-vs-bot.")] public sealed class BotTakeoverManagerInfo : TraitInfo { [Desc("Automatically enable takeover when the world is loaded.")] public readonly bool AutoActivate = false; [Desc("Delay in ticks before activating after world load (lets other world init settle).")] public readonly int AutoActivateDelayTicks = 1; [Desc("Bot type to activate (e.g. normal/rush/turtle/naval). Must match an IBotInfo.Type on the Player actor.")] public readonly string BotType = "normal"; [Desc("Conditions to grant when takeover is enabled. If empty, will infer enable--ai for known bot types.")] public readonly ImmutableArray GrantConditions = []; public override object Create(ActorInitializer init) { return new BotTakeoverManager(init.Self, this); } } public sealed class BotTakeoverManager : ITick, IWorldLoaded { readonly Actor self; readonly World world; readonly BotTakeoverManagerInfo info; IBot activeBot; readonly List conditionTokens = new(); bool pendingAutoActivate; int autoActivateRemainingTicks; public BotTakeoverManager(Actor self, BotTakeoverManagerInfo info) { this.self = self; world = self.World; this.info = info; } public bool IsActive { get { if (activeBot == null) return false; return activeBot switch { ModularBot mb => mb.IsEnabled, _ => false }; } } public void Toggle() { SetActive(!IsActive); } public void SetActive(bool active) { if (active) ActivateTakeover(); else DeactivateTakeover(); } void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) { if (info.AutoActivate) { pendingAutoActivate = true; autoActivateRemainingTicks = Math.Max(0, info.AutoActivateDelayTicks); } } void ITick.Tick(Actor self) { if (!pendingAutoActivate) return; if (autoActivateRemainingTicks-- > 0) return; pendingAutoActivate = false; ActivateTakeover(); } bool CanTakeover() { // Bot logic is host-authoritative. Avoid enabling during replays or on non-host clients. if (!Game.IsHost || world.IsReplay) return false; var player = self.Owner; // If this player is already a lobby bot, then the engine will have activated it already. if (player == null || player.IsBot) return false; // Only takeover the local player (avoid accidentally hijacking other human slots). return world.LocalPlayer != null && world.LocalPlayer == player; } void ActivateTakeover() { if (!CanTakeover()) return; if (IsActive) return; var botType = info.BotType ?? ""; if (string.IsNullOrWhiteSpace(botType)) botType = "normal"; var bot = self.TraitsImplementing().FirstOrDefault(b => b.Info.Type == botType); if (bot == null) { Console.WriteLine($"[Bot Takeover] No IBot found for type '{botType}'."); return; } GrantTakeoverConditions(botType); bot.Activate(self.Owner); activeBot = bot; Console.WriteLine($"[Bot Takeover] Activated bot type '{botType}' for player {self.Owner.PlayerName}."); } void DeactivateTakeover() { if (activeBot is ModularBot mb) mb.IsEnabled = false; activeBot = null; for (var i = conditionTokens.Count - 1; i >= 0; i--) { var token = conditionTokens[i]; if (self.TokenValid(token)) self.RevokeCondition(token); } conditionTokens.Clear(); Console.WriteLine("[Bot Takeover] Deactivated."); } void GrantTakeoverConditions(string botType) { if (conditionTokens.Count > 0) return; var conditions = info.GrantConditions; if (conditions.Length == 0) { var inferred = botType.ToLowerInvariant() switch { "rush" => "enable-rush-ai", "normal" => "enable-normal-ai", "turtle" => "enable-turtle-ai", "naval" => "enable-naval-ai", _ => null }; if (!string.IsNullOrEmpty(inferred)) conditions = conditions.Add(inferred); } foreach (var c in conditions.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct(StringComparer.OrdinalIgnoreCase)) conditionTokens.Add(self.GrantCondition(c)); } } }