From ec3107e6e75cfa14b0c0d0f7a9e0876ccf2a60db Mon Sep 17 00:00:00 2001 From: "let5sne.win10" Date: Sun, 11 Jan 2026 20:52:14 +0800 Subject: [PATCH] =?UTF-8?q?ra:=20=E5=BC=80=E5=B1=80=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=89=98=E7=AE=A1=E7=BB=99=E4=BC=A0=E7=BB=9F=E4=BA=BA=E6=9C=BA?= =?UTF-8?q?=EF=BC=88Bot=20Takeover=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景 - OpenRA 的 Player 默认按‘人类’设计:不自动执行建造/展开/生产等,需要玩家输入。 - 为了实现全人机阵营交锋(后续再叠加 LLM 增强),需要在开局就把本地玩家控制权交还给传统人机。 改动 - 新增 BotTakeoverManager(Player trait):WorldLoaded 后自动为本地人类玩家激活传统 ModularBot(normal),并授予 nable-normal-ai,确保传统 AI 全链路模块从开局开始运行。 - 将右下角托管按钮逻辑改为切换 BotTakeoverManager,用于随时取消/恢复托管(避免直接激活 LLM bot 导致双 bot 并行与性能风险)。 - RA UI 增加托管按钮与中文提示文案。 影响 - 开局无需等待/点击即可展开 MCV 并开始传统 AI 运营;同时仍可通过按钮取消托管恢复手动操作。 --- .../Traits/Player/BotTakeoverManager.cs | 196 ++++++++++++++++++ .../Widgets/Logic/Ingame/LLMTakeoverLogic.cs | 57 +++++ mods/ra/chrome/ingame-player.yaml | 14 ++ mods/ra/fluent/zh/chrome.ftl | 10 + mods/ra/rules/player.yaml | 5 + 5 files changed, 282 insertions(+) create mode 100644 OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs create mode 100644 OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs diff --git a/OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs b/OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs new file mode 100644 index 0000000..7ac2566 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs @@ -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--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)); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs new file mode 100644 index 0000000..b40d35b --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs @@ -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 logicArgs) + { + this.world = world; + + var button = widget.Get("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(); + 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(); + if (takeover != null) + takeover.Toggle(); + } + } +} diff --git a/mods/ra/chrome/ingame-player.yaml b/mods/ra/chrome/ingame-player.yaml index a218eb3..5667d52 100644 --- a/mods/ra/chrome/ingame-player.yaml +++ b/mods/ra/chrome/ingame-player.yaml @@ -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 diff --git a/mods/ra/fluent/zh/chrome.ftl b/mods/ra/fluent/zh/chrome.ftl index 62e1d0d..8f82b55 100644 --- a/mods/ra/fluent/zh/chrome.ftl +++ b/mods/ra/fluent/zh/chrome.ftl @@ -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将自动控制你的所有单位和建筑, + 包括建造、生产、战斗和资源管理。 + + 再次点击可取消托管,恢复手动控制。 diff --git a/mods/ra/rules/player.yaml b/mods/ra/rules/player.yaml index 451762b..cf30068 100644 --- a/mods/ra/rules/player.yaml +++ b/mods/ra/rules/player.yaml @@ -185,3 +185,8 @@ Player: PlayerExperience: GameSaveViewportManager: PlayerRadarTerrain: + BotTakeoverManager: + AutoActivate: true + AutoActivateDelayTicks: 1 + BotType: normal + GrantConditions: enable-normal-ai