Files
OpenRA/OpenRA.Mods.Common/Traits/Player/BulkProductionQueue.cs
let5sne.win10 9cf6ebb986
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:46:54 +08:00

333 lines
10 KiB
C#

#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.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.Player)]
[Desc("Attach this to the player actor (not a building!) to define a new shared build queue.",
"Allows you to build multiple actors before delivery",
"Will work together with the `ProductionBulkAirDrop` trait on the actor that actually does the production.",
"You will also want to add `PrimaryBuilding` to let the user choose where new units should exit.")]
public class BulkProductionQueueInfo : ProductionQueueInfo, Requires<TechTreeInfo>, Requires<PlayerResourcesInfo>
{
[Desc("Maximum order capacity.")]
public readonly int MaxCapacity = 6;
[Desc("Delivery delay in ticks.")]
public readonly int DeliveryDelay = 1500;
[NotificationReference("Speech")]
[Desc("Notification played when the delivery started.")]
public readonly string StartDeliveryNotification = null;
[Desc("Return funds if the delivery fails.")]
public readonly bool RefundUndeliveredActors = true;
[FluentReference(optional: true)]
[Desc("Notification displayed when the delivery started.")]
public readonly string StartDeliveryTextNotification = null;
[NotificationReference("Speech")]
[Desc("Notifications to play when the delivery actor is on its way. Last notification is played when the actors to deliver are spawned on the map.")]
public readonly ImmutableArray<string> DeliveryProgressNotifications = [];
public override object Create(ActorInitializer init) { return new BulkProductionQueue(init, this); }
}
public class BulkProductionQueue : ProductionQueue
{
static readonly ActorInfo[] NoItems = [];
readonly Actor self;
readonly BulkProductionQueueInfo info;
protected readonly List<(ActorInfo Actor, int Resources, int Cash)> ActorsReadyForDelivery = [];
public int DeliveryDelay { get; private set; } = 0;
protected int notificationInterval = 0;
protected int notificationIndex = 0;
protected bool deliveryProcessStarted = false;
public BulkProductionQueue(ActorInitializer init, BulkProductionQueueInfo info)
: base(init, info)
{
self = init.Self;
this.info = info;
if (info.DeliveryProgressNotifications.Length != 0)
notificationInterval = info.DeliveryDelay / info.DeliveryProgressNotifications.Length;
}
protected override void Tick(Actor self)
{
// PERF: Avoid LINQ.
Enabled = false;
var isActive = false;
foreach (var x in self.World.ActorsWithTrait<Production>())
{
if (x.Trait.IsTraitDisabled)
continue;
if (x.Actor.Owner != self.Owner || !x.Trait.Info.Produces.Contains(Info.Type))
continue;
Enabled |= IsValidFaction;
isActive |= !x.Trait.IsTraitPaused;
}
if (!Enabled)
{
DeliverFinished();
ClearQueue();
}
else
{
if (HasDeliveryStarted() && DeliveryDelay > 0)
{
DeliveryDelay--;
PlayDeliveryProgressNotifications();
}
else if (HasDeliveryStarted() && DeliveryDelay == 0)
{
PlayDeliveryProgressNotifications();
DeliveryHasArrived();
DeliveryDelay--;
}
}
TickInner(self, !isActive);
}
public override IEnumerable<ActorInfo> AllItems()
{
return Enabled ? base.AllItems() : NoItems;
}
public override IEnumerable<ActorInfo> BuildableItems()
{
return Enabled && ActorsReadyForDelivery.Count != info.MaxCapacity && !deliveryProcessStarted ? base.BuildableItems() : NoItems;
}
public override TraitPair<Production> MostLikelyProducer()
{
var productionActor = self.World.ActorsWithTrait<Production>()
.Where(x => x.Actor.Owner == self.Owner
&& !x.Trait.IsTraitDisabled && x.Trait.Info.Produces.Contains(Info.Type))
.OrderBy(x => x.Trait.IsTraitPaused)
.ThenByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.ThenByDescending(x => x.Actor.ActorID)
.FirstOrDefault();
return productionActor;
}
protected override bool BuildUnit(ActorInfo unit)
{
// Find a production structure to build this actor
var bi = unit.TraitInfo<BuildableInfo>();
// Some units may request a specific production type, which is ignored if the AllTech cheat is enabled
var type = developerMode.AllTech ? Info.Type : (bi.BuildAtProductionType ?? Info.Type);
var producers = self.World.ActorsWithTrait<Production>()
.Where(x => x.Actor.Owner == self.Owner
&& !x.Trait.IsTraitDisabled
&& x.Trait.Info.Produces.Contains(type))
.OrderByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.OrderByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.ThenByDescending(x => x.Actor.ActorID);
var anyProducers = false;
foreach (var p in producers)
{
anyProducers = true;
if (p.Trait.IsTraitPaused)
continue;
if (ActorsReadyForDelivery.Count <= info.MaxCapacity)
{
var item = Queue.First(i => i.Done && i.Item == unit.Name);
ActorsReadyForDelivery.Add((unit, item.ResourcesPaid, item.TotalCost - item.ResourcesPaid));
EndProduction(item);
return true;
}
}
if (!anyProducers)
CancelProduction(unit.Name, 1);
return false;
}
public override void ResolveOrder(Actor self, Order order)
{
if (!Enabled)
return;
var rules = self.World.Map.Rules;
switch (order.OrderString)
{
case "StartProduction":
var unit = rules.Actors[order.TargetString];
var bi = unit.TraitInfo<BuildableInfo>();
// Not built by this queue
if (!bi.Queue.Contains(Info.Type))
return;
// You can't build that
if (BuildableItems().All(b => b.Name != order.TargetString))
return;
// Check if the player is trying to build more units that they are allowed
var fromLimit = int.MaxValue;
if (!developerMode.AllTech)
{
if (Info.QueueLimit > 0)
fromLimit = Info.QueueLimit - Queue.Count;
if (Info.ItemLimit > 0)
fromLimit = Math.Min(fromLimit, Info.ItemLimit - Queue.Count(i => i.Item == order.TargetString));
if (bi.BuildLimit > 0)
{
var inQueue = Queue.Count(pi => pi.Item == order.TargetString);
var owned = self.Owner.World.ActorsHavingTrait<Buildable>().Count(a => a.Info.Name == order.TargetString && a.Owner == self.Owner);
fromLimit = Math.Min(fromLimit, bi.BuildLimit - (inQueue + owned));
}
if (fromLimit <= 0)
return;
}
var cost = GetProductionCost(unit);
var time = GetBuildTime(unit, bi);
var amountToBuild = Math.Min(fromLimit, order.ExtraData);
for (var n = 0; n < amountToBuild; n++)
{
if (Info.PayUpFront && cost > playerResources.GetCashAndResources())
return;
BeginProduction(new ProductionItem(this, order.TargetString, cost, playerPower, () => self.World.AddFrameEndTask(_ =>
{
// Make sure the item hasn't been invalidated between the ProductionItem ticking and this FrameEndTask running
if (!Queue.Any(i => i.Done && i.Item == unit.Name))
return;
BuildUnit(unit);
})), !order.Queued);
}
break;
case "PauseProduction":
PauseProduction(order.TargetString, order.ExtraData != 0);
break;
case "CancelProduction":
CancelProduction(order.TargetString, order.ExtraData);
break;
case "ReturnOrder":
ReturnOrder(order.TargetString, order.ExtraData);
break;
case "PurchaseOrder":
if (!deliveryProcessStarted && order.TargetString == info.Type)
StartDeliveryProcess();
break;
}
}
public void DeliverFinished()
{
if (info.RefundUndeliveredActors || !deliveryProcessStarted)
ReturnOrder();
ActorsReadyForDelivery.Clear();
deliveryProcessStarted = false;
}
public bool HasDeliveryStarted()
{
return deliveryProcessStarted;
}
public List<(ActorInfo Actor, int Resources, int Cash)> GetActorsReadyForDelivery()
{
return ActorsReadyForDelivery;
}
protected void StartDeliveryProcess()
{
ClearQueue();
deliveryProcessStarted = true;
DeliveryDelay = info.DeliveryDelay;
var rules = self.World.Map.Rules;
Game.Sound.PlayNotification(rules, self.Owner, "Speech", info.StartDeliveryNotification, self.Owner.Faction.InternalName);
if (info.StartDeliveryTextNotification != null)
TextNotificationsManager.AddTransientLine(self.Owner, info.StartDeliveryTextNotification);
}
protected void DeliveryHasArrived()
{
var producers = self.World.ActorsWithTrait<ProductionBulkAirdrop>()
.Where(x => x.Actor.Owner == self.Owner
&& !x.Trait.IsTraitDisabled
&& !x.Trait.IsTraitPaused
&& x.Trait.Info.Produces.Contains(Info.Type))
.OrderByDescending(x => x.Actor.Trait<PrimaryBuilding>().IsPrimary)
.ThenByDescending(x => x.Actor.ActorID);
var p = producers.FirstOrDefault();
p.Trait?.DeliverOrder(p.Actor, ActorsReadyForDelivery, Info.Type, this);
}
public void ReturnOrder(string itemName = null, uint numberToCancel = 1)
{
if (itemName == null)
{
foreach (var actorData in ActorsReadyForDelivery)
{
playerResources.RefundResources(actorData.Resources);
playerResources.RefundCash(actorData.Cash);
}
ActorsReadyForDelivery.Clear();
return;
}
for (var i = 0; i < numberToCancel; i++)
{
var actor = ActorsReadyForDelivery.LastOrDefault(actor => actor.Actor.Name == itemName);
if (actor.Actor == null)
break;
playerResources.RefundResources(actor.Resources);
playerResources.RefundCash(actor.Cash);
ActorsReadyForDelivery.Remove(actor);
}
}
protected void PlayDeliveryProgressNotifications()
{
if (notificationInterval == 0)
{
Game.Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Speech",
info.DeliveryProgressNotifications[notificationIndex], self.Owner.Faction.InternalName);
notificationInterval = info.DeliveryDelay / info.DeliveryProgressNotifications.Length;
notificationIndex++;
if (info.DeliveryProgressNotifications.Length == notificationIndex)
{
notificationIndex = 0;
return;
}
}
notificationInterval--;
}
}
}