ra: 开局自动托管给传统人机(Bot Takeover)
背景 - 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:
196
OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs
Normal file
196
OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
57
OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs
Normal file
57
OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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将自动控制你的所有单位和建筑,
|
||||
包括建造、生产、战斗和资源管理。
|
||||
|
||||
再次点击可取消托管,恢复手动控制。
|
||||
|
||||
@@ -185,3 +185,8 @@ Player:
|
||||
PlayerExperience:
|
||||
GameSaveViewportManager:
|
||||
PlayerRadarTerrain:
|
||||
BotTakeoverManager:
|
||||
AutoActivate: true
|
||||
AutoActivateDelayTicks: 1
|
||||
BotType: normal
|
||||
GrantConditions: enable-normal-ai
|
||||
|
||||
Reference in New Issue
Block a user