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

105
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)
* 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)
* Repository: [https://github.com/OpenRA/OpenRA](https://github.com/OpenRA/OpenRA) ![Continuous Integration](https://github.com/OpenRA/OpenRA/workflows/Continuous%20Integration/badge.svg)
- 官网:https://www.openra.net
- 交流Libera 上的 `#openra`[web](https://web.libera.chat/#openra))或 [Discord](https://discord.openra.net)
- 上游仓库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
* Command & Conquer: Tiberian Dawn
* Dune 2000
- 开局自动托管给传统人机:`OpenRA.Mods.Common/Traits/Player/BotTakeoverManager.cs`
- 作为 Player Trait`WorldLoaded` 后为“本地人类玩家LocalPlayer”自动激活经典 `ModularBot(normal)`
- 仅在主机侧生效,并避免在回放中启用(防止非权威端/回放污染世界状态)。
- 当前在 `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.
* 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.
EA 未背书且不支持本产品。
## 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.
* 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).
## 编译与运行Windows
## 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.
* There exists an auto-generated [Trait documentation](https://docs.openra.net/en/latest/release/traits/) to get started with yaml files.
* 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).
更多开发环境配置请阅读:
- [INSTALL.md](https://github.com/OpenRA/OpenRA/blob/bleed/INSTALL.md)
- Wiki: [Compiling](https://github.com/OpenRA/OpenRA/wiki/Compiling)
## Support
## 贡献
* Sponsor a [mirror server](https://github.com/OpenRA/OpenRAWebsiteV3/tree/master/packages) if you have some bandwidth to spare.
* You can immediately set up a [Dedicated](https://github.com/OpenRA/OpenRA/wiki/Dedicated-Server) Game Server.
- 请阅读并遵守:[Code of Conduct](https://github.com/OpenRA/OpenRA/blob/bleed/CODE_OF_CONDUCT.md)
- 提交补丁请遵循:[CONTRIBUTING.md](https://github.com/OpenRA/OpenRA/blob/bleed/CONTRIBUTING.md)
- 引擎架构概览(可能已过时):[Hacking](https://github.com/OpenRA/OpenRA/wiki/Hacking)
## License
Copyright (c) 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](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING).
## 地图制作Mapping
# Sponsors
Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
- 教程:[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)
## 许可证
Copyright (c) OpenRA Developers and Contributors
OpenRA 使用 GNU GPLv3或更高版本授权发布详见 [COPYING](https://github.com/OpenRA/OpenRA/blob/bleed/COPYING)。
## 赞助方
Windows 平台的免费代码签名由 [SignPath.io](https://about.signpath.io/) 提供,证书来自 [SignPath Foundation](https://signpath.org/)。

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
TooltipText: button-llm-takeover.tooltip
TooltipDesc: button-llm-takeover.tooltipdesc
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,11 @@ Player:
PlayerExperience:
GameSaveViewportManager:
PlayerRadarTerrain:
BotTakeoverManager:
AutoActivate: true
AutoActivateDelayTicks: 1
BotType: normal
GrantConditions: enable-normal-ai
BotTakeoverCameraFollower:
Enabled: true
RequireTakeoverActive: true