背景 - OpenRA 的 Player 默认按‘人类’设计:不自动执行建造/展开/生产等,需要玩家输入。 - 为了实现全人机阵营交锋(后续再叠加 LLM 增强),需要在开局就把本地玩家控制权交还给传统人机。 改动 - 新增 BotTakeoverManager(Player trait):WorldLoaded 后自动为本地人类玩家激活传统 ModularBot(normal),并授予 nable-normal-ai,确保传统 AI 全链路模块从开局开始运行。 - 将右下角托管按钮逻辑改为切换 BotTakeoverManager,用于随时取消/恢复托管(避免直接激活 LLM bot 导致双 bot 并行与性能风险)。 - RA UI 增加托管按钮与中文提示文案。 影响 - 开局无需等待/点击即可展开 MCV 并开始传统 AI 运营;同时仍可通过按钮取消托管恢复手动操作。
197 lines
4.8 KiB
C#
197 lines
4.8 KiB
C#
#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-<bot>-ai for known bot types.")]
|
|
public readonly ImmutableArray<string> 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<int> 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<IBot>().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));
|
|
}
|
|
}
|
|
}
|