Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View File

@@ -0,0 +1,825 @@
#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.Frozen;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
public enum BotMcvExpansionMode { CheckResource, CheckBase, CheckCurrentLocation }
[TraitLocation(SystemActors.Player)]
[Desc("Manages AI MCVs and expansion.")]
public class McvExpansionManagerBotModuleInfo : ConditionalTraitInfo, Requires<ResourceMapBotModuleInfo>, NotBefore<ResourceMapBotModuleInfo>
{
[Desc("Actor types that are considered MCVs (deploy into base builders).")]
public readonly FrozenSet<string> McvTypes = FrozenSet<string>.Empty;
[Desc("Actor types that are considered construction yards (base builders).")]
public readonly FrozenSet<string> ConstructionYardTypes = FrozenSet<string>.Empty;
[Desc("Actor types that are able to produce MCVs.")]
public readonly FrozenSet<string> McvFactoryTypes = FrozenSet<string>.Empty;
[Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.")]
public readonly int MinimumConstructionYardCount = 1;
[Desc("Try to maintain at additional this many ConstructionYardTypes.")]
public readonly int AdditionalConstructionYardCount = 0;
[Desc("Build additional MCV if cash is above this.")]
public readonly int BuildAdditionalMCVCashAmount = 5000;
[Desc("Delay (in ticks) for giving orders to idle MCVs.")]
public readonly int ScanForNewMcvInterval = 20;
[Desc("Delay (in ticks) for checking and building a MCV.")]
public readonly int BuildMcvInterval = 101;
[Desc("Delay (in ticks) for moving a conyard to better expansion. Only work with more than 1 conyard.")]
public readonly int MoveConyardTick = 5700;
[Desc("Should moving the oldest or newest conyard be preferred? Random ordering if unset.")]
public readonly bool? MoveOldConyardFirst = null;
[Desc("Initial expansion mode chosen by AI.")]
public readonly BotMcvExpansionMode InitialExpansionMode = BotMcvExpansionMode.CheckResource;
[Desc("Allow the bot to switch expansion mode automatically on enough failure or successful attempts.")]
public readonly bool ExpansionModeAutoSwitch = true;
/* those are CheckResource mode options */
[Desc("Minimum distance (in cells) from the found resource creator location when checking for MCV deployment location.")]
public readonly int CRmodeMinDeployRadius = 2;
[Desc("Maximum distance (in cells) the found resource creator location when checking for MCV deployment location.")]
public readonly int CRmodeMaxDeployRadius = 20;
[Desc("When moving to a resource, what distance (in cells) to resource should we attempt to maintain?")]
public readonly int CRmodeTryMaintainRange = 8;
[Desc("Distance (in cells) to avoid a friendly conyard when choosing an expansion location.",
"Recommended to set it equal or larger than ResourceMapStrideRadius.")]
public readonly int CRmodeFriendlyConyardDislikeRange = 14;
[Desc("Distance (in cells) to avoid a friendly refinery when choosing an expansion location.",
"Recommended to set it equal or larger than ResourceMapStrideRadius.")]
public readonly int CRmodeFriendlyRefineryDislikeRange = 14;
/* those are CheckBase mode options */
[Desc("Minimum distance (in cells) from center of the base expansion when checking for MCV deployment location.")]
public readonly int CBmodeMinDeployRadius = 2;
[Desc("Maximum distance (in cells) from center of the base expansion when checking for MCV deployment location.")]
public readonly int CBmodeMaxDeployRadius = 20;
public override object Create(ActorInitializer init) { return new McvExpansionManagerBotModule(init.Self, this); }
}
public class McvExpansionManagerBotModule :
ConditionalTrait<McvExpansionManagerBotModuleInfo>,
IBotTick,
IBotRespondToAttack,
IBotBaseExpansion,
INotifyActorDisposing
{
// When ExpansionModeAutoSwitch is true, if the AI fails to find a deploy spot enough time even in CheckBase mode
// NegativeMaxFailedAttempts is applied to make AI switch bettween modes more frequently until a successful attempt
const int CRmodPositiveMaxFailedAttempts = 3;
const int CBmodPositiveMaxFailedAttempts = 2;
const int NegativeMaxFailedAttempts = 0;
readonly World world;
readonly Player player;
readonly ActorIndex.OwnerAndNamesAndTrait<TransformsInfo> mcvs;
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> constructionYards;
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> mcvFactories;
IBotPositionsUpdated[] notifyPositionsUpdated;
IBotRequestUnitProduction[] requestUnitProduction;
IBotSuggestRefineryProduction[] suggestRefineryProduction;
readonly Dictionary<Actor, CPos?> activeMCVs = [];
PathFinder pathfinder;
ResourceMapBotModule resourceMapModule;
PlayerResources playerResources;
Actor mustUndeployCoyard;
int scanInterval;
int buildMCVInterval;
int moveConyardInterval;
bool firstTick = true;
bool undeployEvenNoBase = false;
bool allowfallback = true;
BotMcvExpansionMode mcvExpansionMode;
int mcvDeploymentMinDeployRadius;
int mcvDeploymentMaxDeployRadius;
int mcvDeploymentTryMaintainRange;
int maxFailedAttempts;
int failedAttempts;
CPos? lastFailedCheckSpot;
// It is unnecessary to respond every tick, we only need to respond once in a while.
int attackrespondcooldown = 20;
int pathDistanceSquareFactor;
public McvExpansionManagerBotModule(Actor self, McvExpansionManagerBotModuleInfo info)
: base(info)
{
world = self.World;
player = self.Owner;
mcvs = new ActorIndex.OwnerAndNamesAndTrait<TransformsInfo>(world, info.McvTypes, player);
constructionYards = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.ConstructionYardTypes, player);
mcvFactories = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.McvFactoryTypes, player);
}
protected override void Created(Actor self)
{
// Special case handling is required for the Player actor.
// Created is called before Player.PlayerActor is assigned,
// so we must query player traits from self, which refers
// for bot modules always to the Player actor.
notifyPositionsUpdated = self.TraitsImplementing<IBotPositionsUpdated>().ToArray();
requestUnitProduction = self.TraitsImplementing<IBotRequestUnitProduction>().ToArray();
suggestRefineryProduction = self.TraitsImplementing<IBotSuggestRefineryProduction>().ToArray();
pathfinder = world.WorldActor.Trait<PathFinder>();
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
}
protected override void TraitEnabled(Actor self)
{
// Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay.
scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval << 1);
buildMCVInterval = world.LocalRandom.Next(Info.BuildMcvInterval, Info.BuildMcvInterval << 1);
moveConyardInterval = world.LocalRandom.Next(Info.MoveConyardTick, Info.MoveConyardTick << 1);
}
void SwitchExpansionMode(BotMcvExpansionMode nextMode)
{
mcvExpansionMode = nextMode;
switch (nextMode)
{
case BotMcvExpansionMode.CheckResource:
mcvDeploymentMinDeployRadius = Info.CRmodeMinDeployRadius;
mcvDeploymentMaxDeployRadius = Info.CRmodeMaxDeployRadius;
mcvDeploymentTryMaintainRange = Info.CRmodeTryMaintainRange;
break;
case BotMcvExpansionMode.CheckBase:
mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius;
mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius;
mcvDeploymentTryMaintainRange = (Info.CBmodeMaxDeployRadius + Info.CBmodeMinDeployRadius) >> 1;
break;
case BotMcvExpansionMode.CheckCurrentLocation:
mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius;
mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius;
mcvDeploymentTryMaintainRange = 0;
break;
default:
break;
}
}
void FindBadDeploySpot(CPos? failedSpot)
{
lastFailedCheckSpot = failedSpot;
if (!Info.ExpansionModeAutoSwitch)
{
if (++failedAttempts >= maxFailedAttempts)
failedAttempts = maxFailedAttempts;
return;
}
if (++failedAttempts >= maxFailedAttempts)
{
failedAttempts = 0;
switch (mcvExpansionMode)
{
case BotMcvExpansionMode.CheckResource:
SwitchExpansionMode(BotMcvExpansionMode.CheckBase);
break;
case BotMcvExpansionMode.CheckBase:
SwitchExpansionMode(BotMcvExpansionMode.CheckResource);
maxFailedAttempts = NegativeMaxFailedAttempts;
break;
case BotMcvExpansionMode.CheckCurrentLocation:
SwitchExpansionMode(BotMcvExpansionMode.CheckResource);
maxFailedAttempts = NegativeMaxFailedAttempts;
break;
}
}
}
void FindGoodDeploySpot()
{
lastFailedCheckSpot = null;
if (!Info.ExpansionModeAutoSwitch)
{
if (--failedAttempts <= -maxFailedAttempts)
failedAttempts = -maxFailedAttempts;
return;
}
if (--failedAttempts <= -maxFailedAttempts)
{
switch (mcvExpansionMode)
{
case BotMcvExpansionMode.CheckResource:
maxFailedAttempts = CRmodPositiveMaxFailedAttempts;
failedAttempts = -maxFailedAttempts;
break;
case BotMcvExpansionMode.CheckBase:
maxFailedAttempts = CRmodPositiveMaxFailedAttempts;
failedAttempts = maxFailedAttempts - 1;
SwitchExpansionMode(BotMcvExpansionMode.CheckResource);
break;
case BotMcvExpansionMode.CheckCurrentLocation:
maxFailedAttempts = CBmodPositiveMaxFailedAttempts;
failedAttempts = maxFailedAttempts - 1;
SwitchExpansionMode(BotMcvExpansionMode.CheckBase);
break;
}
}
}
public (CPos? ExpandLocation, int Attraction, CPos? CheckSpot) GetExpansionCenter(Actor mcv, Mobile mobile, bool allowfallback)
{
/*
* indiceSideLengthSquare (which is equal to indiceSideLength * indiceSideLength) is used as the basic unit to calculate the attraction of a candidate,
* we compare the attraction on the same scale on different factors, such as candidate's distance to current MCV and ally construction yard & refinery within range, etc:
*
* 1). the weight of candidate's distance-square to current MCV
*
* a) if not Mobile: range from 0 to -indiceSideLengthSquare.
*
* The reason why:
*
* It is calculated as "(candidate - mcv.Location).LengthSquared / pathDistanceSquareFactor".
* note that: pathDistanceSquareFactor = resourceMapIndicesColumnCount * resourceMapIndicesColumnCount + resourceMapIndicesRowCount * resourceMapIndicesRowCount,
*
* Consider a map, we divide it at the length of indiceSideLength = r, and then its resourceMapIndicesColumnCount = a, resourceMapIndicesRowCount = b,
* so the map.width a*r, map.height b*r,
* the maximum euclid distance-square between two points on the map is (a*r)(a*r) + (b*r)(b*r),
* so the maximum "weight of candidate's distance to current MCV" is from 0 to -((a*r)(a*r) + (b*r)(b*r)) / (a*a + b*b) = -r*r = -indiceSideLengthSquare.
*
* b) if Mobile: range depends on pathfinding distance in cell.
*
* It is calculated as "pathfindDistance * pathfindDistance / pathDistanceSquareFactor".
*
* 2). the weight of friendly construction yard within range: -indiceSideLengthSquare. If it belongs to an ally, -indiceSideLengthSquare/2.
*
* 3). the weight of enemy within range: -indiceSideLengthSquare*8 for base building, otherwise -indiceSideLengthSquare/64
*
* 4). the weight of friendly refinery within range (not for CheckBase mode): -indiceSideLengthSquare. If it belongs to an ally, -indiceSideLengthSquare/2.
*
* 5). the weight of resource amount (only for CheckResource mode): from 0 to +indiceSideLengthSquare/8.
*
* The reason why:
*
* The maximum resource amount in a indice of resource map is approximately indiceSideLengthSquare (full of it), but a stride full of resources is less likely to
* have room for buildings. So we prefer the indice have half of resource cells the most, which may give us enough room to place buildings.
*
* so the weight can be: (indiceSideLengthSquare/2) - |(indiceResourceCellCount - (indiceSideLengthSquare/2))|, range from (0 to +indiceSideLengthSquare/2).
*
* Note: In practive resource weight is not very important, we cannot let MCV go a long way just for a rich resource spot.
* We have to take only 1/4 of it, wich is (0 to +indiceSideLengthSquare/8),
* and apply some additional method to filter the indice for acceptable resource (not too low).
*/
var indiceSideLengthSquare = resourceMapModule.GetIndiceSideLength() * resourceMapModule.GetIndiceSideLength();
switch (mcvExpansionMode)
{
/*
* CheckBase mode only considers the distance to current MCV, ally construction yard within range and enemy buildings within range.
* Attaction has a base value of indiceSideLengthSquare >> 1 (1/2 of the maximum distance weight, 1/ sqrt(2) 1/1.4 of maximum euclid distance in map)
*/
case BotMcvExpansionMode.CheckBase:
var cb_conyardlocs = world.ActorsHavingTrait<Building>()
.Where(a => a.Owner.IsAlliedWith(player) && Info.ConstructionYardTypes.Contains(a.Info.Name))
.Select(a => (a.Location, a.Owner == player))
.ToArray();
CPos? cb_suitablespot = null;
CPos? cb_checkspot = null;
var cb_best = int.MinValue;
for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++)
{
var indiceCenter = resourceMapModule.GetIndice(i).IndiceCenter;
if (lastFailedCheckSpot == indiceCenter)
continue;
var attraction = indiceSideLengthSquare >> 1;
attraction -= (indiceCenter - mcv.Location).LengthSquared / pathDistanceSquareFactor;
attraction -= CalculateThreats(indiceSideLengthSquare, i);
foreach (var (location, isAlly) in cb_conyardlocs)
{
var sdistance = (indiceCenter - location).LengthSquared;
if (sdistance <= indiceSideLengthSquare)
{
if (isAlly)
attraction -= indiceSideLengthSquare;
else
attraction -= indiceSideLengthSquare << 1;
}
}
foreach (var (othermcv, dest) in activeMCVs)
{
if (dest == indiceCenter && othermcv != mcv)
attraction -= indiceSideLengthSquare << 1;
}
if (!allowfallback)
{
var sdistance = (indiceCenter - mcv.Location).LengthSquared;
if (sdistance <= indiceSideLengthSquare)
attraction -= indiceSideLengthSquare << 1;
}
if (attraction > cb_best)
{
cb_best = attraction;
cb_checkspot = indiceCenter;
cb_suitablespot = indiceCenter;
}
}
return (cb_suitablespot ?? mcv.Location, cb_best, cb_checkspot);
/*
* CheckResource mode considers the distance to current MCV, ally construction yard & refinery within range,
* Attaction has a base value of:
* 1. if not Mobile: indiceSideLengthSquare >> 2 (1/4 of the maximum distance weight, = 0.5 of the maximum euclid distance in map)
* 2. if Mobile: indiceSideLengthSquare >> 1 (1/2 of the maximum distance weight, 0.71 of the maximum euclid distance in map)
*/
case BotMcvExpansionMode.CheckResource:
var cr_refinarylocs = world.ActorsHavingTrait<Refinery>()
.Where(a => a.Owner == player && resourceMapModule.Info.RefineryTypes.Contains(a.Info.Name))
.Select(a => (a.Location, a.Owner != player))
.ToArray();
var cr_conyardlocs = world.ActorsHavingTrait<Building>()
.Where(a => a.Owner.IsAlliedWith(player) && Info.ConstructionYardTypes.Contains(a.Info.Name))
.Select(a => (a.Location, a.Owner != player))
.ToArray();
// We only take indice has more than the half of average indice value (in weight calculation), to skip the indice with very poor resource
// when failedAttempts is acceptable.
var thresholdRes = 0;
for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++)
{
var resourceCellCounts = resourceMapModule.GetIndice(i).ResourceCellsCount;
thresholdRes += (indiceSideLengthSquare >> 1) - Math.Abs(resourceCellCounts - (indiceSideLengthSquare >> 1));
}
thresholdRes = (thresholdRes / resourceMapModule.GetIndicesLength()) >> 1;
CPos? cr_suitablespot = null;
CPos? cr_checkspot = null;
var cr_best = int.MinValue;
for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++)
{
var indice = resourceMapModule.GetIndice(i);
var indiceCenter = indice.IndiceCenter;
var resourceCellsCount = indice.ResourceCellsCount;
var resourceCellsCenter = indice.ResourceCellsCenter;
var resourceCreatorLocs = indice.ResourceCreatorLocs;
if ((failedAttempts > maxFailedAttempts >> 1 && resourceCellsCount <= thresholdRes) || lastFailedCheckSpot == indiceCenter)
continue;
var attraction = 0;
if (mobile == null)
{
attraction = indiceSideLengthSquare >> 2;
attraction -= (resourceCellsCenter - mcv.Location).LengthSquared / pathDistanceSquareFactor;
}
else
{
attraction = indiceSideLengthSquare >> 1;
var path = pathfinder.FindPathToTargetCells(mcv, mcv.Location, [resourceCellsCenter], BlockedByActor.None);
if (path == PathFinder.NoPath)
continue;
attraction -= path.Count * path.Count / pathDistanceSquareFactor;
}
// it is better that resource cells takes only half of the indice cells, which give us the place to place building.
attraction += ((indiceSideLengthSquare >> 1) - Math.Abs(resourceCellsCount - (indiceSideLengthSquare >> 1))) >> 2;
attraction += 8 * resourceCreatorLocs.Length;
var resCenter = resourceCreatorLocs.Length == 0 || world.LocalRandom.Next(2) > 0 ? resourceCellsCenter : resourceCreatorLocs.Random(world.LocalRandom);
attraction -= CalculateThreats(indiceSideLengthSquare, i);
foreach (var (location, isAlly) in cr_refinarylocs)
{
var sdistance = (resCenter - location).LengthSquared;
if (sdistance <= Info.CRmodeFriendlyRefineryDislikeRange * Info.CRmodeFriendlyRefineryDislikeRange)
{
if (isAlly)
attraction -= indiceSideLengthSquare;
else
attraction -= indiceSideLengthSquare << 1;
}
}
foreach (var (location, isAlly) in cr_conyardlocs)
{
var sdistance = (resCenter - location).LengthSquared;
if (sdistance <= Info.CRmodeFriendlyConyardDislikeRange * Info.CRmodeFriendlyConyardDislikeRange)
{
if (isAlly)
attraction -= indiceSideLengthSquare;
else
attraction -= indiceSideLengthSquare << 1;
}
}
foreach (var (othermcv, dest) in activeMCVs)
{
if (dest == indiceCenter && othermcv != mcv)
attraction -= indiceSideLengthSquare << 1;
}
if (!allowfallback)
{
var sdistance = (resCenter - mcv.Location).LengthSquared;
if (sdistance <= Info.CRmodeFriendlyConyardDislikeRange * Info.CRmodeFriendlyConyardDislikeRange)
attraction -= indiceSideLengthSquare << 1;
}
if (attraction > cr_best)
{
cr_best = attraction;
cr_checkspot = indiceCenter;
cr_suitablespot = resCenter;
}
}
if (cr_suitablespot == null)
return (null, int.MinValue, null);
return (cr_suitablespot, cr_best, cr_checkspot);
case BotMcvExpansionMode.CheckCurrentLocation:
return (mcv.Location, int.MaxValue, null);
default:
return (null, int.MinValue, null);
}
}
int CalculateThreats(int indiceSideLengthSquare, int index)
{
var baseIndice = resourceMapModule.GetIndice(index);
var (indiceCount, nearbyEnemyThreat, nearbyEnemyBaseThreat) = resourceMapModule.GetNearbyIndicesThreat(index);
var indiceEnemyBaseThreat = Math.Max(baseIndice.EnemyBaseCount - baseIndice.FriendlyBaseCount, 0);
var indiceEnemyUnitThreat = Math.Max(baseIndice.EnemyUnitCount - baseIndice.FriendlyUnitCount, 0);
if (indiceCount == 0)
return (indiceEnemyUnitThreat * indiceSideLengthSquare >> 6) + (indiceEnemyBaseThreat * indiceSideLengthSquare << 3);
return ((indiceEnemyUnitThreat * indiceSideLengthSquare + nearbyEnemyThreat * indiceSideLengthSquare / indiceCount) >> 6) +
((indiceEnemyBaseThreat * indiceSideLengthSquare + nearbyEnemyBaseThreat * indiceSideLengthSquare / indiceCount) << 3);
}
void IBotTick.BotTick(IBot bot)
{
attackrespondcooldown--;
if (firstTick)
{
resourceMapModule = bot.Player.PlayerActor.TraitsImplementing<ResourceMapBotModule>().FirstOrDefault(t => t.IsTraitEnabled());
SwitchExpansionMode(Info.InitialExpansionMode);
pathDistanceSquareFactor = resourceMapModule.GetIndiceRowCount() * resourceMapModule.GetIndiceRowCount()
+ resourceMapModule.GetIndiceColumnCount() * resourceMapModule.GetIndiceColumnCount();
DeployMcvs(bot, false);
firstTick = false;
}
if (--scanInterval <= 0)
{
foreach (var amcv in activeMCVs.Keys.ToList())
{
if (amcv.IsDead || !amcv.IsInWorld)
activeMCVs.Remove(amcv);
}
scanInterval = Info.ScanForNewMcvInterval;
DeployMcvs(bot, true);
}
if (--buildMCVInterval <= 0)
{
buildMCVInterval = Info.BuildMcvInterval;
BuildMCV(bot);
}
if (--moveConyardInterval <= 0)
{
foreach (var amcv in activeMCVs.Keys.ToList())
{
if (amcv.IsDead || !amcv.IsInWorld)
activeMCVs.Remove(amcv);
}
moveConyardInterval = Info.MoveConyardTick;
UnDeployConyard(bot);
}
}
void BuildMCV(IBot bot)
{
if (Info.McvTypes.Count <= 0)
return;
if (AIUtils.CountActorByCommonName(mcvFactories) <= 0)
return;
var mcvNum = AIUtils.CountActorByCommonName(mcvs);
var conyardNum = AIUtils.CountActorByCommonName(constructionYards);
var mcvShouldHave = playerResources.GetCashAndResources() >= Info.BuildAdditionalMCVCashAmount
? Info.MinimumConstructionYardCount + Info.AdditionalConstructionYardCount : Info.MinimumConstructionYardCount;
// If we only have 1 MCV and no conyard, we should be allowed to build another MCV.
// Otherwise, when an mcv is on the move and we should wait.
if ((conyardNum <= 0 && mcvNum > 1) || (conyardNum > 0 && mcvNum > 0))
return;
if (conyardNum + mcvNum >= mcvShouldHave)
return;
// We have MCV in production queue, let's wait.
if (mcvFactories.Actors
.Any(a => !a.IsDead && a.TraitsImplementing<ProductionQueue>().Any(t => t.Enabled && t.AllQueued().Any(q => Info.McvTypes.Contains(q.Item)))))
return;
// We have MCV in production queue, let's wait.
if (player.PlayerActor.TraitsImplementing<ProductionQueue>()
.Any(t => t.Enabled && t.AllQueued().Any(q => Info.McvTypes.Contains(q.Item))))
return;
var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault();
if (unitBuilder == null)
return;
var mcvType = Info.McvTypes.Random(world.LocalRandom);
// Make sure we only request one MCV at a time.
if (unitBuilder.RequestedProductionCount(bot, mcvType) <= 0)
unitBuilder.RequestUnitProduction(bot, mcvType);
}
void DeployMcvs(IBot bot, bool chooseLocation)
{
var newMCVs = world.ActorsHavingTrait<Transforms>()
.Where(a => a.Owner == player && a.IsIdle && Info.McvTypes.Contains(a.Info.Name));
foreach (var mcv in newMCVs)
DeployMcv(bot, mcv, chooseLocation);
}
void UnDeployConyard(IBot bot)
{
if (mustUndeployCoyard != null && mustUndeployCoyard.IsInWorld && !mustUndeployCoyard.IsDead && mustUndeployCoyard.Owner == player)
{
bot.QueueOrder(new Order("DeployTransform", mustUndeployCoyard, true));
mustUndeployCoyard = null;
return;
}
if (activeMCVs.Count > 0)
return;
var conyards = constructionYards.Actors
.Where(a => !a.IsDead);
var moveOldConyardFirst = Info.MoveOldConyardFirst ?? world.LocalRandom.Next(2) > 0;
if (moveOldConyardFirst)
conyards = conyards.OrderBy(a => a.ActorID);
else
conyards = conyards.OrderByDescending(a => a.ActorID);
var conyardslist = conyards.ToList();
if (conyardslist.Count > 1 || undeployEvenNoBase)
{
// We don't want to interrupt refinery production, otherwise it may cause a dead loop of deploy/undeploy.
var movableMCV = conyardslist.FirstOrDefault(a => !a.TraitsImplementing<ProductionQueue>()
.Any(t => t.Enabled && t.AllQueued().Any(q => resourceMapModule.Info.RefineryTypes.Contains(q.Item))));
if (movableMCV != null)
bot.QueueOrder(new Order("DeployTransform", movableMCV, true));
undeployEvenNoBase = false;
}
}
// Find any MCV and deploy them at a sensible location.
void DeployMcv(IBot bot, Actor mcv, bool move)
{
CPos? desiredLocation = null;
var transformsInfo = mcv.Info.TraitInfo<TransformsInfo>();
var actorInfo = world.Map.Rules.Actors[transformsInfo.IntoActor];
var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>();
if (bi == null)
return;
if (move)
{
var (deployLocation, resLoc, checkloc) = ChooseMcvDeployLocation(mcv, actorInfo, bi, transformsInfo.Offset, allowfallback);
allowfallback = true;
desiredLocation = deployLocation;
if (desiredLocation == null)
return;
activeMCVs[mcv] = checkloc;
if (resLoc != null)
{
foreach (var srp in suggestRefineryProduction)
srp.RequestLocation(resLoc.Value, desiredLocation.Value, mcv);
}
bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true));
}
else
{
if (!world.CanPlaceBuilding(mcv.Location + transformsInfo.Offset, actorInfo, bi, mcv))
return;
desiredLocation = mcv.Location;
}
bot.QueueOrder(new Order("DeployTransform", mcv, true));
// When we don't have a construction yard, we notify the new location to other traits for defence,
// If not, we only notify sometimes, because we are not sure if mcv can successfully deploy at the desired location.
// TODO: This could be addressed via INotifyTransform.
if (constructionYards.Actors.All(a => a.IsDead) || world.LocalRandom.Next(2) > 0)
{
foreach (var n in notifyPositionsUpdated)
{
n.UpdatedBaseCenter(desiredLocation.Value);
n.UpdatedDefenseCenter(desiredLocation.Value);
}
}
}
// First, find a suitable expansion location according to current mode,
// Then, find a deployable cell around it.
(CPos? DeployLoc, CPos? ResourceLoc, CPos? CheckLoc) ChooseMcvDeployLocation(
Actor mcv,
ActorInfo transformIntoInfo,
BuildingInfo transformIntoBuildingInfo,
CVec offset,
bool allowfallback)
{
if (!mcv.Info.HasTraitInfo<IMoveInfo>())
return (null, null, null);
var mobile = mcv.TraitOrDefault<Mobile>();
var (expandCenter, attraction, checkspot) = GetExpansionCenter(mcv, mobile, allowfallback);
// Find the deployable cell
CPos? FindDeployCell(CPos? sourceCell, CPos? targetCell, int minRange, int maxRange, int tryMaintainRange)
{
if (!sourceCell.HasValue || !targetCell.HasValue)
return null;
var target = targetCell.Value;
var source = sourceCell.Value;
var cells = world.Map.FindTilesInAnnulus(target, minRange, maxRange);
/* First, sort the cells that keep tryMaintainRange to target (meanwhile direction is from center to target) the first to be considered
* by using following code. The idea is to use a linear combination of two distances-square for sorting weight.
*
* See comments in https://github.com/OpenRA/OpenRA/pull/22028#issuecomment-3242518793 for explaination.
*/
if (source != target)
{
var theta = tryMaintainRange;
var deta = (target - source).Length - tryMaintainRange;
cells = cells.OrderBy(c => deta * (c - target).LengthSquared + theta * (c - source).LengthSquared);
}
else
cells = cells.Shuffle(world.LocalRandom);
CPos? bestcell = null;
foreach (var cell in cells)
{
if (world.CanPlaceBuilding(cell + offset, transformIntoInfo, transformIntoBuildingInfo, mcv))
{
bestcell = cell;
break;
}
}
// If no deployble cell found, return null
if (bestcell == null)
return null;
if (source != target && mobile != null && !pathfinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, source, bestcell.Value))
bestcell = null;
// If the best deploy cell is not ideal ( >= tryMaintainRange + 2), which means there might be some huge blockers
// so we fall back to default behavior, which is the directly closest cell to target
if (!bestcell.HasValue || (source != target && (bestcell.Value - target).LengthSquared >= (tryMaintainRange + 2) * (tryMaintainRange + 2)))
{
cells = cells.OrderBy(c => (c - target).LengthSquared);
foreach (var cell in cells)
{
if (world.CanPlaceBuilding(cell + offset, transformIntoInfo, transformIntoBuildingInfo, mcv))
{
if (mobile != null && !pathfinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, source, cell))
return null;
return (!bestcell.HasValue) || (cell - target).LengthSquared < (bestcell.Value - target).LengthSquared ? cell : bestcell;
}
}
}
return bestcell;
}
var bc = FindDeployCell(mcv.Location, expandCenter, mcvDeploymentMinDeployRadius, mcvDeploymentMaxDeployRadius, mcvDeploymentTryMaintainRange);
// At last, if the attraction of the found expansion location is good enough (>0) and deploy cell found,
// we consider it as a good expansion, otherwise, we consider it as a bad expansion.
if (bc.HasValue && attraction > 0)
FindGoodDeploySpot();
else
FindBadDeploySpot(bc.HasValue ? null : checkspot);
if (mcvExpansionMode == BotMcvExpansionMode.CheckResource && expandCenter.HasValue && bc.HasValue)
return (bc, expandCenter, checkspot);
return (bc, null, checkspot);
}
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
{
if (attackrespondcooldown <= 0 && Info.McvTypes.Contains(self.Info.Name))
{
attackrespondcooldown = 20;
DeployMcv(bot, self, false);
if (AIUtils.CountActorByCommonName(constructionYards) == 0)
{
foreach (var n in notifyPositionsUpdated)
n.UpdatedBaseCenter(self.Location);
}
}
}
void INotifyActorDisposing.Disposing(Actor self)
{
mcvs.Dispose();
constructionYards.Dispose();
mcvFactories.Dispose();
}
void IBotBaseExpansion.UpdateExpansionParams(IBot bot, bool fallback, bool undeployEvenNoBase, Actor mustUndeploy)
{
moveConyardInterval = 20; // allow some order latency
allowfallback = fallback;
this.undeployEvenNoBase = undeployEvenNoBase;
}
}
}