#region Copyright & License Information /* * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) * 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. For more information, * see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Linq; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Orders; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("This actor can transport Passenger actors.")] public class CargoInfo : ITraitInfo, Requires { [Desc("The maximum sum of Passenger.Weight that this actor can support.")] public readonly int MaxWeight = 0; [Desc("Number of pips to display when this actor is selected.")] public readonly int PipCount = 0; [Desc("`Passenger.CargoType`s that can be loaded into this actor.")] public readonly HashSet Types = new HashSet(); [Desc("A list of actor types that are initially spawned into this actor.")] public readonly string[] InitialUnits = { }; [Desc("When this actor is sold should all of its passengers be unloaded?")] public readonly bool EjectOnSell = true; [Desc("Terrain types that this actor is allowed to eject actors onto. Leave empty for all terrain types.")] public readonly HashSet UnloadTerrainTypes = new HashSet(); [Desc("Voice to play when ordered to unload the passengers.")] [VoiceReference] public readonly string UnloadVoice = "Action"; [Desc("Which direction the passenger will face (relative to the transport) when unloading.")] public readonly int PassengerFacing = 128; [Desc("Cursor to display when able to unload the passengers.")] public readonly string UnloadCursor = "deploy"; [Desc("Cursor to display when unable to unload the passengers.")] public readonly string UnloadBlockedCursor = "deploy-blocked"; public object Create(ActorInitializer init) { return new Cargo(init, this); } } public class Cargo : IPips, IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled, INotifyOwnerChanged, INotifyAddedToWorld, ITick, INotifySold, IDisableMove, INotifyActorDisposing { public readonly CargoInfo Info; readonly Actor self; readonly Stack cargo = new Stack(); readonly HashSet reserves = new HashSet(); readonly Lazy facing; readonly bool checkTerrainType; int totalWeight = 0; int reservedWeight = 0; Aircraft aircraft; CPos currentCell; public IEnumerable CurrentAdjacentCells { get; private set; } public bool Unloading { get; internal set; } public IEnumerable Passengers { get { return cargo; } } public int PassengerCount { get { return cargo.Count; } } public Cargo(ActorInitializer init, CargoInfo info) { self = init.Self; Info = info; Unloading = false; checkTerrainType = info.UnloadTerrainTypes.Count > 0; if (init.Contains()) { cargo = new Stack(init.Get()); totalWeight = cargo.Sum(c => GetWeight(c)); } else if (init.Contains()) { foreach (var u in init.Get()) { var unit = self.World.CreateActor(false, u.ToLowerInvariant(), new TypeDictionary { new OwnerInit(self.Owner) }); cargo.Push(unit); } totalWeight = cargo.Sum(c => GetWeight(c)); } else { foreach (var u in info.InitialUnits) { var unit = self.World.CreateActor(false, u.ToLowerInvariant(), new TypeDictionary { new OwnerInit(self.Owner) }); cargo.Push(unit); } totalWeight = cargo.Sum(c => GetWeight(c)); } facing = Exts.Lazy(self.TraitOrDefault); } public void Created(Actor self) { aircraft = self.TraitOrDefault(); } static int GetWeight(Actor a) { return a.Info.TraitInfo().Weight; } public IEnumerable Orders { get { yield return new DeployOrderTargeter("Unload", 10, () => CanUnload() ? Info.UnloadCursor : Info.UnloadBlockedCursor); } } public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued) { if (order.OrderID == "Unload") return new Order(order.OrderID, self, queued); return null; } public void ResolveOrder(Actor self, Order order) { if (order.OrderString == "Unload") { if (!CanUnload()) return; Unloading = true; self.CancelActivity(); if (aircraft != null) self.QueueActivity(new HeliLand(self, true)); self.QueueActivity(new UnloadCargo(self, true)); } } IEnumerable GetAdjacentCells() { return Util.AdjacentCells(self.World, Target.FromActor(self)).Where(c => self.Location != c); } bool CanUnload() { if (checkTerrainType) { var terrainType = self.World.Map.GetTerrainInfo(self.Location).Type; if (!Info.UnloadTerrainTypes.Contains(terrainType)) return false; } return !IsEmpty(self) && (aircraft == null || aircraft.CanLand(self.Location)) && CurrentAdjacentCells != null && CurrentAdjacentCells.Any(c => Passengers.Any(p => p.Trait().CanEnterCell(c))); } public bool CanLoad(Actor self, Actor a) { return (reserves.Contains(a) || HasSpace(GetWeight(a))) && self.IsAtGroundLevel(); } internal bool ReserveSpace(Actor a) { if (reserves.Contains(a)) return true; var w = GetWeight(a); if (!HasSpace(w)) return false; reserves.Add(a); reservedWeight += w; return true; } internal void UnreserveSpace(Actor a) { if (!reserves.Contains(a)) return; reservedWeight -= GetWeight(a); reserves.Remove(a); } public string CursorForOrder(Actor self, Order order) { if (order.OrderString != "Unload") return null; return CanUnload() ? Info.UnloadCursor : Info.UnloadBlockedCursor; } public string VoicePhraseForOrder(Actor self, Order order) { if (order.OrderString != "Unload" || IsEmpty(self) || !self.HasVoice(Info.UnloadVoice)) return null; return Info.UnloadVoice; } public bool MoveDisabled(Actor self) { return reserves.Any(); } public bool HasSpace(int weight) { return totalWeight + reservedWeight + weight <= Info.MaxWeight; } public bool IsEmpty(Actor self) { return cargo.Count == 0; } public Actor Peek(Actor self) { return cargo.Peek(); } public Actor Unload(Actor self) { var a = cargo.Pop(); totalWeight -= GetWeight(a); SetPassengerFacing(a); foreach (var npe in self.TraitsImplementing()) npe.PassengerExited(self, a); var p = a.Trait(); p.Transport = null; foreach (var u in p.Info.GrantUpgrades) self.Trait().RevokeUpgrade(self, u, p); return a; } void SetPassengerFacing(Actor passenger) { if (facing.Value == null) return; var passengerFacing = passenger.TraitOrDefault(); if (passengerFacing != null) passengerFacing.Facing = facing.Value.Facing + Info.PassengerFacing; var passengerTurreted = passenger.TraitOrDefault(); if (passengerTurreted != null) passengerTurreted.TurretFacing = facing.Value.Facing + Info.PassengerFacing; } public IEnumerable GetPips(Actor self) { var numPips = Info.PipCount; for (var i = 0; i < numPips; i++) yield return GetPipAt(i); } PipType GetPipAt(int i) { var n = i * Info.MaxWeight / Info.PipCount; foreach (var c in cargo) { var pi = c.Info.TraitInfo(); if (n < pi.Weight) return pi.PipType; else n -= pi.Weight; } return PipType.Transparent; } public void Load(Actor self, Actor a) { cargo.Push(a); var w = GetWeight(a); totalWeight += w; if (reserves.Contains(a)) { reservedWeight -= w; reserves.Remove(a); } foreach (var npe in self.TraitsImplementing()) npe.PassengerEntered(self, a); var p = a.Trait(); p.Transport = self; foreach (var u in p.Info.GrantUpgrades) self.Trait().GrantUpgrade(self, u, p); } public void Killed(Actor self, AttackInfo e) { foreach (var c in cargo) c.Kill(e.Attacker); cargo.Clear(); } public void Disposing(Actor self) { foreach (var c in cargo) c.Dispose(); cargo.Clear(); } public void Selling(Actor self) { } public void Sold(Actor self) { if (!Info.EjectOnSell || cargo == null) return; while (!IsEmpty(self)) SpawnPassenger(Unload(self)); } void SpawnPassenger(Actor passenger) { self.World.AddFrameEndTask(w => { w.Add(passenger); passenger.Trait().SetPosition(passenger, self.Location); // TODO: this won't work well for >1 actor as they should move towards the next enterable (sub) cell instead }); } public void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { if (cargo == null) return; self.World.AddFrameEndTask(w => { foreach (var p in Passengers) p.Owner = newOwner; }); } public void AddedToWorld(Actor self) { // Force location update to avoid issues when initial spawn is outside map currentCell = self.Location; CurrentAdjacentCells = GetAdjacentCells(); } bool initialized; public void Tick(Actor self) { // Notify initial cargo load if (!initialized) { foreach (var c in cargo) { c.Trait().Transport = self; foreach (var npe in self.TraitsImplementing()) npe.PassengerEntered(self, c); } initialized = true; } var cell = self.World.Map.CellContaining(self.CenterPosition); if (currentCell != cell) { currentCell = cell; CurrentAdjacentCells = GetAdjacentCells(); } } } public interface INotifyPassengerEntered { void PassengerEntered(Actor self, Actor passenger); } public interface INotifyPassengerExited { void PassengerExited(Actor self, Actor passenger); } public class RuntimeCargoInit : IActorInit, ISuppressInitExport { [FieldFromYamlKey] readonly Actor[] value = { }; public RuntimeCargoInit() { } public RuntimeCargoInit(Actor[] init) { value = init; } public Actor[] Value(World world) { return value; } } public class CargoInit : IActorInit { [FieldFromYamlKey] readonly string[] value = { }; public CargoInit() { } public CargoInit(string[] init) { value = init; } public string[] Value(World world) { return value; } } }