Files
OpenRA/OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs
let5sne.win10 ec3107e6e7
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
ra: 开局自动托管给传统人机(Bot Takeover)
背景

- OpenRA 的 Player 默认按‘人类’设计:不自动执行建造/展开/生产等,需要玩家输入。

- 为了实现全人机阵营交锋(后续再叠加 LLM 增强),需要在开局就把本地玩家控制权交还给传统人机。

改动

- 新增 BotTakeoverManager(Player trait):WorldLoaded 后自动为本地人类玩家激活传统 ModularBot(normal),并授予 nable-normal-ai,确保传统 AI 全链路模块从开局开始运行。

- 将右下角托管按钮逻辑改为切换 BotTakeoverManager,用于随时取消/恢复托管(避免直接激活 LLM bot 导致双 bot 并行与性能风险)。

- RA UI 增加托管按钮与中文提示文案。

影响

- 开局无需等待/点击即可展开 MCV 并开始传统 AI 运营;同时仍可通过按钮取消托管恢复手动操作。
2026-01-11 20:52:14 +08:00

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));
}
}
}