Compare commits

3 Commits

Author SHA1 Message Date
let5sne.win10
d00ffa28f3 docs: 中文 README 并记录托管实现
将 README.md 翻译为中文,并补充本分支关键实现:开局交还控制权给传统人机(BotTakeoverManager)与托管时镜头防守热点跟随(BotTakeoverCameraFollower),同时给出相关入口/配置文件路径。
2026-01-11 23:39:03 +08:00
let5sne.win10
9d80847a9a ra: 托管时镜头自动跟随防守热点
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
新增 BotTakeoverCameraFollower(Player trait):托管开启时通过 ViewportCenterProvider 自动跟随热点事件,默认偏防守(基地/矿区受击优先,建造落点次之),并使用热点衰减 + 最短驻留时间 + 切换阈值避免多处战斗时镜头抖动。

同时:

- mods/ra/rules/player.yaml 接入该 trait(仅 takeover 激活时生效)

- 修复 ingame-player.yaml 的 TooltipDesc/FTL 校验警告

- 小幅代码风格优化(不改行为)
2026-01-11 23:27:24 +08:00
let5sne.win10
ec3107e6e7 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 运营;同时仍可通过按钮取消托管恢复手动操作。
2026-01-11 20:52:14 +08:00
7 changed files with 849 additions and 41 deletions

View File

@@ -0,0 +1,501 @@
#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.Linq;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.Player)]
[Desc("When the local human player is handed over to a classic bot, automatically follow important defensive events with the camera.")]
public sealed class BotTakeoverCameraFollowerInfo : TraitInfo
{
[Desc("Enable camera following behavior.")]
public readonly bool Enabled = true;
[Desc("Only follow while the BotTakeoverManager is active.")]
public readonly bool RequireTakeoverActive = true;
[Desc("How often (in ticks) to re-evaluate the best focus point.")]
public readonly int ReevaluateIntervalTicks = 5;
[Desc("Minimum time (in milliseconds) to stay on a focus point before switching, unless a much higher priority event appears.")]
public readonly int MinFocusDurationMs = 2500;
[Desc("Before MinFocusDurationMs expires, the new focus must have at least this multiple of the current focus score to switch.")]
public readonly float LockedSwitchScoreMultiplier = 1.35f;
[Desc("Forget hotspots after this time (in milliseconds).")]
public readonly int HotspotLifetimeMs = 15000;
[Desc("Merge nearby hotspots within this distance (in WPos units).")]
public readonly int HotspotMergeDistance = 2048;
[Desc("Maximum number of hotspots to track.")]
public readonly int MaxHotspots = 12;
[Desc("Camera move speed (in WPos units per second) when smoothly tracking within a focus point.")]
public readonly float CameraSpeed = 60000f;
[Desc("Snap instantly when the focus point changes.")]
public readonly bool SnapOnFocusChange = true;
[Desc("Weight when a base building (construction yard / MCV) is attacked.")]
public readonly float BaseBuildingAttackedWeight = 120f;
[Desc("Weight when a harvester is attacked.")]
public readonly float HarvesterAttackedWeight = 110f;
[Desc("Weight when a refinery is attacked.")]
public readonly float RefineryAttackedWeight = 105f;
[Desc("Weight when another structure is attacked.")]
public readonly float StructureAttackedWeight = 80f;
[Desc("Weight when a unit is attacked.")]
public readonly float UnitAttackedWeight = 45f;
[Desc("Weight when a building is placed (used as a low-priority 'expansion' focus when not under attack).")]
public readonly float BuildingPlacedWeight = 22f;
public override object Create(ActorInitializer init) { return new BotTakeoverCameraFollower(init.Self, this); }
}
public sealed class BotTakeoverCameraFollower : ITick, IWorldLoaded, INotifyDamage, INotifyBuildingPlaced
{
enum HotspotKind { Damage, Build }
sealed class Hotspot
{
public float2 Position;
public float Strength;
public long LastUpdateTime;
public HotspotKind Kind;
}
readonly Actor playerActor;
readonly World world;
readonly BotTakeoverCameraFollowerInfo info;
WorldRenderer worldRenderer;
Func<float2> previousCenterProvider;
bool centerProviderInstalled;
readonly List<Hotspot> hotspots = [];
bool lastFollowActive;
int reevaluateCountdown;
Hotspot currentFocus;
long currentFocusStartTime;
float2 targetPos;
bool hasTarget;
bool snapToTarget;
float2 cameraPos;
bool hasCameraPos;
long lastCameraUpdateTime;
public BotTakeoverCameraFollower(Actor self, BotTakeoverCameraFollowerInfo info)
{
playerActor = self;
world = self.World;
this.info = info;
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
{
worldRenderer = wr;
}
void ITick.Tick(Actor self)
{
if (!info.Enabled || worldRenderer == null)
return;
var followActive = IsFollowActive();
if (followActive)
EnsureCenterProviderInstalled();
else
EnsureCenterProviderUninstalled();
if (followActive != lastFollowActive)
{
lastFollowActive = followActive;
OnFollowActiveChanged(followActive);
}
if (!followActive)
return;
if (--reevaluateCountdown > 0)
return;
reevaluateCountdown = Math.Max(1, info.ReevaluateIntervalTicks);
PruneHotspots();
UpdateTarget();
}
void INotifyDamage.Damaged(Actor damaged, AttackInfo e)
{
if (!info.Enabled || worldRenderer == null)
return;
if (!IsLocalPlayablePlayer())
return;
if (damaged == null || damaged.IsDead || !damaged.IsInWorld)
return;
if (e.Attacker == null)
return;
if (e.Attacker.Owner == damaged.Owner)
return;
if (e.Attacker == damaged.World.WorldActor)
return;
// Don't track healing / resurrect.
if (e.Damage.Value <= 0)
return;
AddHotspot(damaged.CenterPosition, GetDamageWeight(damaged), HotspotKind.Damage);
}
void INotifyBuildingPlaced.BuildingPlaced(Actor self, Actor building)
{
if (!info.Enabled || worldRenderer == null)
return;
if (!IsLocalPlayablePlayer())
return;
if (building == null || building.IsDead || !building.IsInWorld)
return;
AddHotspot(building.CenterPosition, info.BuildingPlacedWeight, HotspotKind.Build);
}
bool IsLocalPlayablePlayer()
{
var localPlayer = world.LocalPlayer;
if (localPlayer == null || localPlayer.Spectating)
return false;
return playerActor.Owner == localPlayer;
}
bool IsFollowActive()
{
if (!IsLocalPlayablePlayer())
return false;
if (!info.RequireTakeoverActive)
return true;
var takeover = playerActor.TraitOrDefault<BotTakeoverManager>();
return takeover != null && takeover.IsActive;
}
void EnsureCenterProviderInstalled()
{
if (centerProviderInstalled)
return;
if (!IsLocalPlayablePlayer())
return;
previousCenterProvider = worldRenderer.Viewport.ViewportCenterProvider;
worldRenderer.Viewport.ViewportCenterProvider = ViewportCenterProvider;
centerProviderInstalled = true;
}
void EnsureCenterProviderUninstalled()
{
if (!centerProviderInstalled)
return;
worldRenderer.Viewport.ViewportCenterProvider = previousCenterProvider;
previousCenterProvider = null;
centerProviderInstalled = false;
}
float2 ViewportCenterProvider()
{
if (worldRenderer == null)
return float2.Zero;
var now = Game.RunTime;
var viewportCenter = new float2(worldRenderer.Viewport.CenterPosition.X, worldRenderer.Viewport.CenterPosition.Y);
if (!IsFollowActive())
{
lastCameraUpdateTime = now;
cameraPos = viewportCenter;
hasCameraPos = true;
return viewportCenter;
}
if (!hasCameraPos)
{
lastCameraUpdateTime = now;
cameraPos = viewportCenter;
hasCameraPos = true;
}
var dtMs = Math.Max(0, now - lastCameraUpdateTime);
lastCameraUpdateTime = now;
if (!hasTarget)
return viewportCenter;
if (snapToTarget)
{
snapToTarget = false;
cameraPos = targetPos;
return cameraPos;
}
// Prevent huge jumps when the framerate is very low or the game is paused.
var dt = Math.Min(dtMs / 1000f, 0.25f);
var maxStep = info.CameraSpeed * dt;
var delta = targetPos - cameraPos;
var dist = delta.Length;
if (dist <= 0.001f || dist <= maxStep)
cameraPos = targetPos;
else
cameraPos += delta * (maxStep / dist);
return cameraPos;
}
void OnFollowActiveChanged(bool active)
{
reevaluateCountdown = 0;
currentFocus = null;
currentFocusStartTime = 0;
hasTarget = false;
snapToTarget = false;
hasCameraPos = false;
lastCameraUpdateTime = 0;
if (!active)
{
hotspots.Clear();
return;
}
hotspots.Clear();
targetPos = GetBaseFocusOrViewport();
hasTarget = true;
snapToTarget = info.SnapOnFocusChange;
}
float2 GetBaseFocusOrViewport()
{
var baseActor = world.ActorsHavingTrait<BaseBuilding>()
.FirstOrDefault(a => !a.IsDead && a.IsInWorld && a.Owner == playerActor.Owner);
if (baseActor != null)
return new float2(baseActor.CenterPosition.X, baseActor.CenterPosition.Y);
return new float2(worldRenderer.Viewport.CenterPosition.X, worldRenderer.Viewport.CenterPosition.Y);
}
float GetDamageWeight(Actor damaged)
{
if (damaged.Info.HasTraitInfo<BaseBuildingInfo>())
return info.BaseBuildingAttackedWeight;
if (damaged.Info.HasTraitInfo<HarvesterInfo>())
return info.HarvesterAttackedWeight;
if (damaged.Info.HasTraitInfo<RefineryInfo>())
return info.RefineryAttackedWeight;
if (damaged.Info.HasTraitInfo<BuildingInfo>())
return info.StructureAttackedWeight;
return info.UnitAttackedWeight;
}
void AddHotspot(WPos pos, float weight, HotspotKind kind)
{
if (weight <= 0)
return;
var now = Game.RunTime;
var p = new float2(pos.X, pos.Y);
var mergeDist2 = info.HotspotMergeDistance * (long)info.HotspotMergeDistance;
Hotspot best = null;
var bestDist2 = long.MaxValue;
for (var i = 0; i < hotspots.Count; i++)
{
var h = hotspots[i];
var d = p - h.Position;
var dist2 = (long)(d.X * d.X + d.Y * d.Y);
if (dist2 <= mergeDist2 && dist2 < bestDist2)
{
best = h;
bestDist2 = dist2;
}
}
if (best != null)
{
var total = best.Strength + weight;
if (total <= 0.001f)
best.Position = p;
else
best.Position = (best.Position * best.Strength + p * weight) / total;
best.Strength = total;
best.LastUpdateTime = now;
if (kind == HotspotKind.Damage)
best.Kind = HotspotKind.Damage;
return;
}
if (hotspots.Count >= info.MaxHotspots)
RemoveLowestScoreHotspot(now);
hotspots.Add(new Hotspot
{
Position = p,
Strength = weight,
LastUpdateTime = now,
Kind = kind
});
}
void RemoveLowestScoreHotspot(long now)
{
var worstIndex = -1;
var worstScore = float.MaxValue;
for (var i = 0; i < hotspots.Count; i++)
{
var score = GetHotspotScore(hotspots[i], now);
if (score < worstScore)
{
worstScore = score;
worstIndex = i;
}
}
if (worstIndex >= 0)
hotspots.RemoveAt(worstIndex);
}
void PruneHotspots()
{
var now = Game.RunTime;
for (var i = hotspots.Count - 1; i >= 0; i--)
{
var age = now - hotspots[i].LastUpdateTime;
if (age > info.HotspotLifetimeMs)
{
if (ReferenceEquals(hotspots[i], currentFocus))
currentFocus = null;
hotspots.RemoveAt(i);
}
}
}
float GetHotspotScore(Hotspot h, long now)
{
var age = now - h.LastUpdateTime;
if (age <= 0)
return h.Strength;
if (age >= info.HotspotLifetimeMs)
return 0;
var decay = 1f - (float)age / info.HotspotLifetimeMs;
return h.Strength * decay;
}
void UpdateTarget()
{
var now = Game.RunTime;
Hotspot best = null;
var bestScore = 0f;
for (var i = 0; i < hotspots.Count; i++)
{
var h = hotspots[i];
var score = GetHotspotScore(h, now);
if (score <= 0)
continue;
if (score > bestScore)
{
best = h;
bestScore = score;
}
}
var focusScore = currentFocus != null ? GetHotspotScore(currentFocus, now) : 0f;
if (focusScore <= 0)
currentFocus = null;
var want = best;
if (want == null || bestScore <= 0)
{
currentFocus = null;
targetPos = GetBaseFocusOrViewport();
hasTarget = true;
return;
}
if (currentFocus == null)
{
SwitchFocus(want, now);
return;
}
if (!ReferenceEquals(currentFocus, want))
{
var locked = now < currentFocusStartTime + info.MinFocusDurationMs;
if (!locked)
SwitchFocus(want, now);
else if (bestScore >= focusScore * info.LockedSwitchScoreMultiplier)
SwitchFocus(want, now);
}
targetPos = currentFocus.Position;
hasTarget = true;
}
void SwitchFocus(Hotspot newFocus, long now)
{
currentFocus = newFocus;
currentFocusStartTime = now;
targetPos = newFocus.Position;
hasTarget = true;
if (info.SnapOnFocusChange)
snapToTarget = true;
}
}
}

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 = [];
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,56 @@
#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>();
takeover?.Toggle();
}
}
}

103
README.md
View File

@@ -1,60 +1,83 @@
# OpenRA # OpenRA(中文)
A Libre/Free Real Time Strategy game engine supporting early Westwood classics. OpenRA 是一个自由/开源的即时战略RTS游戏引擎支持早期 Westwood 经典作品的重制与扩展。
* Website: [https://www.openra.net](https://www.openra.net) - 官网:https://www.openra.net
* Chat: [#openra on Libera](ircs://irc.libera.chat:6697/openra) ([web](https://web.libera.chat/#openra)) or [Discord](https://discord.openra.net) ![Discord Badge](https://discordapp.com/api/guilds/153649279762694144/widget.png) - 交流Libera 上的 `#openra`[web](https://web.libera.chat/#openra))或 [Discord](https://discord.openra.net)
* Repository: [https://github.com/OpenRA/OpenRA](https://github.com/OpenRA/OpenRA) ![Continuous Integration](https://github.com/OpenRA/OpenRA/workflows/Continuous%20Integration/badge.svg) - 上游仓库https://github.com/OpenRA/OpenRA
Please read the [FAQ](https://github.com/OpenRA/OpenRA/wiki/FAQ) in our [Wiki](https://github.com/OpenRA/OpenRA/wiki) and report problems at [https://github.com/OpenRA/OpenRA/issues](https://github.com/OpenRA/OpenRA/issues). 请先阅读 Wiki 中的 [FAQ](https://github.com/OpenRA/OpenRA/wiki/FAQ) 与开发文档,并在上游 Issues 反馈引擎问题:https://github.com/OpenRA/OpenRA/issues
Join the [Forum](https://forum.openra.net/) for discussion. 也可加入论坛讨论:https://forum.openra.net/
## Play ## 本仓库/本分支的关键实现记录
Distributed mods include a reimagining of 本分支围绕“开局将本地人类玩家控制权交还给传统人机Classic Bot并让比赛以人机 vs 人机方式运转”,以及“托管时镜头自动跟随防守热点”做了增强。
* Command & Conquer: Red Alert - 开局自动托管给传统人机:`OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs`
* Command & Conquer: Tiberian Dawn - 作为 Player Trait`WorldLoaded` 后为“本地人类玩家LocalPlayer”自动激活经典 `ModularBot(normal)`
* Dune 2000 - 仅在主机侧生效,并避免在回放中启用(防止非权威端/回放污染世界状态)。
- 当前在 `mods/ra/rules/player.yaml` 默认开启:`BotTakeoverManager: AutoActivate: true`
- 托管时镜头自动跟随(默认偏“防守保家”):`OpenRA.Mods.Common/Traits/Player/BotTakeoverCameraFollower.cs`
- 通过 `INotifyDamage`/`INotifyBuildingPlaced` 收集“热点”(受击位置、建造落点等),按权重+时间衰减选取最佳焦点。
- 默认优先级:基地/MCV/矿车/矿厂受击 > 其它建筑受击 > 单位受击 > 新建筑落点。
- 多处战斗会使用“最短驻留时间 + 切换阈值”避免镜头频繁抖动;切换焦点可选择瞬移或限速平滑移动。
- 当前在 `mods/ra/rules/player.yaml` 默认开启,并且只在托管激活时生效(`RequireTakeoverActive: true`)。
- 游戏内托管开关按钮(用于取消/恢复托管):
- UI`mods/ra/chrome/ingame-player.yaml``Button@LLM_TAKEOVER`
- 逻辑:`OpenRA.Mods.Common/Widgets/Logic/Ingame/LLMTakeoverLogic.cs`
EA has not endorsed and does not support this product. ## 游玩
Check our [Playing the Game](https://github.com/OpenRA/OpenRA/wiki/Playing-the-game) Guide to win multiplayer matches. 发行的内置模组包含对以下经典 RTS 的重制:
## Contribute - Command & Conquer: Red Alert红色警戒
- Command & Conquer: Tiberian Dawn泰伯利亚黎明
- Dune 2000沙丘 2000
* Please read [INSTALL.md](https://github.com/OpenRA/OpenRA/blob/bleed/INSTALL.md) and [Compiling](https://github.com/OpenRA/OpenRA/wiki/Compiling) on how to set up an OpenRA development environment. EA 未背书且不支持本产品。
* See [Hacking](https://github.com/OpenRA/OpenRA/wiki/Hacking) for a (now very outdated) overview of the engine.
* Read and follow our [Code of Conduct](https://github.com/OpenRA/OpenRA/blob/bleed/CODE_OF_CONDUCT.md).
* To get your patches merged, please adhere to the [Contributing](https://github.com/OpenRA/OpenRA/blob/bleed/CONTRIBUTING.md) guidelines.
## Mapping 多人对战入门可参考:[Playing the Game](https://github.com/OpenRA/OpenRA/wiki/Playing-the-game)
* We offer a [Mapping](https://github.com/OpenRA/OpenRA/wiki/Mapping) Tutorial as you can change gameplay drastically with custom rules. ## 编译与运行Windows
* For scripted mission have a look at the [Lua API](https://docs.openra.net/en/release/lua/).
* If you want to share your maps with the community, upload them at the [OpenRA Resource Center](https://resource.openra.net).
## Modding - 编译(推荐):`.\make.cmd all`
- 快速启动红色警戒模组(本仓库提供脚本):`.\start-ra.cmd`
* Download a copy of the [OpenRA Mod SDK](https://github.com/OpenRA/OpenRAModSDK) to start your own mod. 更多开发环境配置请阅读:
* Check the [Modding Guide](https://github.com/OpenRA/OpenRA/wiki/Modding-Guide) to create your own classic RTS. - [INSTALL.md](https://github.com/OpenRA/OpenRA/blob/bleed/INSTALL.md)
* There exists an auto-generated [Trait documentation](https://docs.openra.net/en/latest/release/traits/) to get started with yaml files. - Wiki: [Compiling](https://github.com/OpenRA/OpenRA/wiki/Compiling)
* Some hints on how to create new OpenRA compatible [Pixelart](https://github.com/OpenRA/OpenRA/wiki/Pixelart).
* Upload total conversions at [our Mod DB profile](https://www.moddb.com/games/openra/mods).
## Support ## 贡献
* Sponsor a [mirror server](https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages) if you have some bandwidth to spare. - 请阅读并遵守:[Code of Conduct](https://github.com/OpenRA/OpenRA/blob/bleed/CODE_OF_CONDUCT.md)
* You can immediately set up a [Dedicated](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server) Game Server. - 提交补丁请遵循:[CONTRIBUTING.md](https://github.com/OpenRA/OpenRA/blob/bleed/CONTRIBUTING.md)
- 引擎架构概览(可能已过时):[Hacking](https://github.com/OpenRA/OpenRA/wiki/Hacking)
## 地图制作Mapping
- 教程:[Mapping](https://github.com/OpenRA/OpenRA/wiki/Mapping)
- Lua 脚本任务相关:[Lua API](https://docs.openra.net/en/release/lua/)
- 分享地图OpenRA Resource Centerhttps://resource.openra.net
## 模组开发Modding
- Mod SDKhttps://github.com/OpenRA/OpenRAModSDK
- 指南:[Modding Guide](https://github.com/OpenRA/OpenRA/wiki/Modding-Guide)
- Traits 文档自动生成https://docs.openra.net/en/latest/release/traits/
- 像素美术提示:[Pixelart](https://github.com/OpenRA/OpenRA/wiki/Pixelart)
- 发布大型模组Total Conversionhttps://www.moddb.com/games/openra/mods
## 支持
- 赞助镜像下载服务器: https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages
- 搭建专用服务器:[Dedicated Server](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server)
## 许可证
## License
Copyright (c) OpenRA Developers and Contributors Copyright (c) OpenRA Developers and Contributors
This file is part of OpenRA, which is free software. It is made OpenRA 使用 GNU GPLv3或更高版本授权发布详见 [COPYING](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING)。
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](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING).
# Sponsors ## 赞助方
Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
Windows 平台的免费代码签名由 [SignPath.io](https://about.signpath.io/) 提供,证书来自 [SignPath Foundation](https://signpath.org/)。

View File

@@ -285,6 +285,20 @@ Container@PLAYER_WIDGETS:
Y: 5 Y: 5
ImageCollection: stance-icons ImageCollection: stance-icons
ImageName: hold-fire 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
TooltipText: button-llm-takeover.tooltip
TooltipDesc: button-llm-takeover.tooltipdesc
TooltipContainer: TOOLTIP_CONTAINER
Container@MUTE_INDICATOR: Container@MUTE_INDICATOR:
Logic: MuteIndicatorLogic Logic: MuteIndicatorLogic
X: WINDOW_WIDTH - WIDTH - 260 X: WINDOW_WIDTH - WIDTH - 260

View File

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

View File

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