ra: 开局自动托管给传统人机(Bot Takeover)
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

背景

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

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

改动

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

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

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

影响

- 开局无需等待/点击即可展开 MCV 并开始传统 AI 运营;同时仍可通过按钮取消托管恢复手动操作。
This commit is contained in:
let5sne.win10
2026-01-11 20:52:14 +08:00
parent 2d5e58d5ce
commit ec3107e6e7
5 changed files with 282 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
#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));
}
}
}

View File

@@ -0,0 +1,57 @@
#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.Collections.Generic;
using OpenRA.Mods.Common.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public class LLMTakeoverLogic : ChromeLogic
{
readonly World world;
[ObjectCreator.UseCtor]
public LLMTakeoverLogic(Widget widget, World world, Dictionary<string, MiniYaml> logicArgs)
{
this.world = world;
var button = widget.Get<ButtonWidget>("LLM_TAKEOVER");
button.GetText = () => IsTakeoverActive() ? "Cancel AI" : "AI";
button.IsHighlighted = () => IsTakeoverActive();
button.OnClick = OnTakeoverClick;
button.IsDisabled = () => world.LocalPlayer == null
|| world.LocalPlayer.WinState != WinState.Undefined;
}
bool IsTakeoverActive()
{
var player = world.LocalPlayer;
if (player == null)
return false;
var takeover = player.PlayerActor.TraitOrDefault<BotTakeoverManager>();
return takeover != null && takeover.IsActive;
}
void OnTakeoverClick()
{
var player = world.LocalPlayer;
if (player == null)
return;
// Prefer the classic AI takeover manager (uses ModularBot + original bot modules).
var takeover = player.PlayerActor.TraitOrDefault<BotTakeoverManager>();
if (takeover != null)
takeover.Toggle();
}
}
}

View File

@@ -285,6 +285,20 @@ Container@PLAYER_WIDGETS:
Y: 5
ImageCollection: stance-icons
ImageName: hold-fire
Container@LLM_TAKEOVER_BAR:
Logic: LLMTakeoverLogic
X: 440
Y: WINDOW_HEIGHT - HEIGHT - 14
Width: 80
Height: 26
Children:
Button@LLM_TAKEOVER:
Width: 80
Height: 26
Font: Bold
Text: LLM AI
TooltipText: button-llm-takeover.tooltip
TooltipContainer: TOOLTIP_CONTAINER
Container@MUTE_INDICATOR:
Logic: MuteIndicatorLogic
X: WINDOW_WIDTH - WIDTH - 260

View File

@@ -190,3 +190,13 @@ button-production-types-aircraft-tooltip = 飞机
button-production-types-naval-tooltip = 海军
button-production-types-scroll-up-tooltip = 向上滚动
button-production-types-scroll-down-tooltip = 向下滚动
## LLM Takeover
button-llm-takeover =
.tooltip = AI托管
.tooltipdesc =
启用AI托管模式默认使用传统人机AI
AI将自动控制你的所有单位和建筑
包括建造、生产、战斗和资源管理。
再次点击可取消托管,恢复手动控制。

View File

@@ -185,3 +185,8 @@ Player:
PlayerExperience:
GameSaveViewportManager:
PlayerRadarTerrain:
BotTakeoverManager:
AutoActivate: true
AutoActivateDelayTicks: 1
BotType: normal
GrantConditions: enable-normal-ai