#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.Mods.Common.Traits; using OpenRA.Orders; using OpenRA.Primitives; using OpenRA.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Orders { [Flags] public enum PlaceBuildingCellType { None = 0, Valid = 1, Invalid = 2, LineBuild = 4 } [RequireExplicitImplementation] public interface IPlaceBuildingPreviewGeneratorInfo : ITraitInfoInterface { IPlaceBuildingPreview CreatePreview(WorldRenderer wr, ActorInfo ai, TypeDictionary init); } public interface IPlaceBuildingPreview { int2 TopLeftScreenOffset { get; } void Tick(); IEnumerable Render(WorldRenderer wr, CPos topLeft, Dictionary footprint); IEnumerable RenderAnnotations(WorldRenderer wr, CPos topLeft); } public class PlaceBuildingOrderGenerator : IOrderGenerator { readonly string worldDefaultCursor = ChromeMetrics.Get("WorldDefaultCursor"); class VariantWrapper { public readonly ActorInfo ActorInfo; public readonly BuildingInfo BuildingInfo; public readonly PlugInfo PlugInfo; public readonly LineBuildInfo LineBuildInfo; public readonly IPlaceBuildingPreview Preview; public VariantWrapper(WorldRenderer wr, ProductionQueue queue, ActorInfo ai) { ActorInfo = ai; BuildingInfo = ActorInfo.TraitInfo(); PlugInfo = ActorInfo.TraitInfoOrDefault(); LineBuildInfo = ActorInfo.TraitInfoOrDefault(); var previewGeneratorInfo = ActorInfo.TraitInfoOrDefault(); if (previewGeneratorInfo != null) { string faction; var buildableInfo = ActorInfo.TraitInfoOrDefault(); if (buildableInfo != null && buildableInfo.ForceFaction != null) faction = buildableInfo.ForceFaction; else { var mostLikelyProducer = queue.MostLikelyProducer(); faction = mostLikelyProducer.Trait != null ? mostLikelyProducer.Trait.Faction : queue.Actor.Owner.Faction.InternalName; } var td = new TypeDictionary() { new FactionInit(faction), new OwnerInit(queue.Actor.Owner), }; foreach (var api in ActorInfo.TraitInfos()) foreach (var o in api.ActorPreviewInits(ActorInfo, ActorPreviewType.PlaceBuilding)) td.Add(o); Preview = previewGeneratorInfo.CreatePreview(wr, ActorInfo, td); } } } readonly World world; protected readonly ProductionQueue Queue; readonly PlaceBuildingInfo placeBuildingInfo; readonly IResourceLayer resourceLayer; readonly Viewport viewport; readonly VariantWrapper[] variants; int variant; public PlaceBuildingOrderGenerator(ProductionQueue queue, string name, WorldRenderer worldRenderer) { Queue = queue; world = queue.Actor.World; placeBuildingInfo = queue.Actor.Owner.PlayerActor.Info.TraitInfo(); resourceLayer = world.WorldActor.TraitOrDefault(); viewport = worldRenderer.Viewport; // Clear selection if using Left-Click Orders if (Game.Settings.Game.UseClassicMouseStyle) world.Selection.Clear(); var variants = new List() { new VariantWrapper(worldRenderer, queue, world.Map.Rules.Actors[name]) }; foreach (var v in variants[0].ActorInfo.TraitInfos()) foreach (var a in v.Actors) variants.Add(new VariantWrapper(worldRenderer, queue, world.Map.Rules.Actors[a])); this.variants = variants.ToArray(); } PlaceBuildingCellType MakeCellType(bool valid, bool lineBuild = false) { var cell = valid ? PlaceBuildingCellType.Valid : PlaceBuildingCellType.Invalid; if (lineBuild) cell |= PlaceBuildingCellType.LineBuild; return cell; } public IEnumerable Order(World world, CPos cell, int2 worldPixel, MouseInput mi) { if ((mi.Button == MouseButton.Left && mi.Event == MouseInputEvent.Down) || (mi.Button == MouseButton.Right && mi.Event == MouseInputEvent.Up)) { if (mi.Button == MouseButton.Right) world.CancelInputMode(); var ret = InnerOrder(world, cell, mi).ToArray(); // If there was a successful placement order if (ret.Any(o => o.OrderString == "PlaceBuilding" || o.OrderString == "LineBuild" || o.OrderString == "PlacePlug")) world.CancelInputMode(); return ret; } return Enumerable.Empty(); } CPos TopLeft { get { var offsetPos = Viewport.LastMousePos; if (variants[variant].Preview != null) offsetPos += variants[variant].Preview.TopLeftScreenOffset; return viewport.ViewToWorld(offsetPos); } } protected virtual IEnumerable InnerOrder(World world, CPos cell, MouseInput mi) { if (world.Paused) yield break; var owner = Queue.Actor.Owner; var ai = variants[variant].ActorInfo; var bi = variants[variant].BuildingInfo; var notification = Queue.Info.CannotPlaceAudio ?? placeBuildingInfo.CannotPlaceNotification; if (mi.Button == MouseButton.Left) { var orderType = "PlaceBuilding"; var topLeft = TopLeft; var plugInfo = ai.TraitInfoOrDefault(); if (plugInfo != null) { orderType = "PlacePlug"; if (!AcceptsPlug(topLeft, plugInfo)) { Game.Sound.PlayNotification(world.Map.Rules, owner, "Speech", notification, owner.Faction.InternalName); TextNotificationsManager.AddTransientLine(placeBuildingInfo.CannotPlaceTextNotification, owner); yield break; } } else { if (!world.CanPlaceBuilding(topLeft, ai, bi, null) || !bi.IsCloseEnoughToBase(world, owner, ai, topLeft)) { foreach (var order in ClearBlockersOrders(world, topLeft)) yield return order; Game.Sound.PlayNotification(world.Map.Rules, owner, "Speech", notification, owner.Faction.InternalName); TextNotificationsManager.AddTransientLine(placeBuildingInfo.CannotPlaceTextNotification, owner); yield break; } if (ai.HasTraitInfo() && !mi.Modifiers.HasModifier(Modifiers.Shift)) orderType = "LineBuild"; } yield return new Order(orderType, owner.PlayerActor, Target.FromCell(world, topLeft), false) { // Building to place TargetString = variants[0].ActorInfo.Name, // Actor ID to associate with placement may be quite large, so it gets its own uint ExtraData = Queue.Actor.ActorID, // Actor variant will always be small enough to safely pack in a CPos ExtraLocation = new CPos(variant, 0), SuppressVisualFeedback = true }; } } void IOrderGenerator.Tick(World world) { if (Queue.AllQueued().All(i => !i.Done || i.Item != variants[0].ActorInfo.Name)) world.CancelInputMode(); foreach (var v in variants) v.Preview?.Tick(); } void IOrderGenerator.SelectionChanged(World world, IEnumerable selected) { } bool AcceptsPlug(CPos cell, PlugInfo plug) { foreach (var a in world.ActorMap.GetActorsAt(cell)) foreach (var p in a.TraitsImplementing()) if (p.AcceptsPlug(plug.Type)) return true; return false; } IEnumerable IOrderGenerator.Render(WorldRenderer wr, World world) { yield break; } IEnumerable IOrderGenerator.RenderAboveShroud(WorldRenderer wr, World world) { var topLeft = TopLeft; var footprint = new Dictionary(); var activeVariant = variants[variant]; var actorInfo = activeVariant.ActorInfo; var buildingInfo = activeVariant.BuildingInfo; var plugInfo = activeVariant.PlugInfo; var lineBuildInfo = activeVariant.LineBuildInfo; var preview = activeVariant.Preview; var owner = Queue.Actor.Owner; if (plugInfo != null) { if (buildingInfo.Dimensions.X != 1 || buildingInfo.Dimensions.Y != 1) throw new InvalidOperationException("Plug requires a 1x1 sized Building"); footprint.Add(topLeft, MakeCellType(AcceptsPlug(topLeft, plugInfo))); } else if (lineBuildInfo != null && owner.Shroud.IsExplored(topLeft)) { // Linebuild for walls. if (buildingInfo.Dimensions.X != 1 || buildingInfo.Dimensions.Y != 1) throw new InvalidOperationException("LineBuild requires a 1x1 sized Building"); if (!Game.GetModifierKeys().HasModifier(Modifiers.Shift)) { var segmentInfo = actorInfo; var segmentBuildingInfo = buildingInfo; if (!string.IsNullOrEmpty(lineBuildInfo.SegmentType)) { segmentInfo = world.Map.Rules.Actors[lineBuildInfo.SegmentType]; segmentBuildingInfo = segmentInfo.TraitInfo(); } foreach (var t in BuildingUtils.GetLineBuildCells(world, topLeft, actorInfo, buildingInfo, owner)) { var lineBuildable = world.IsCellBuildable(t.Cell, segmentInfo, segmentBuildingInfo); var lineCloseEnough = segmentBuildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, segmentInfo, t.Cell); footprint.Add(t.Cell, MakeCellType(lineBuildable && lineCloseEnough, true)); } } var buildable = world.IsCellBuildable(topLeft, actorInfo, buildingInfo); var closeEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, actorInfo, topLeft); footprint[topLeft] = MakeCellType(buildable && closeEnough); } else { var isCloseEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, actorInfo, topLeft); foreach (var t in buildingInfo.Tiles(topLeft)) footprint.Add(t, MakeCellType(isCloseEnough && world.IsCellBuildable(t, actorInfo, buildingInfo) && (resourceLayer == null || resourceLayer.GetResource(t).Type == null))); } return preview?.Render(wr, topLeft, footprint) ?? Enumerable.Empty(); } IEnumerable IOrderGenerator.RenderAnnotations(WorldRenderer wr, World world) { var preview = variants[variant].Preview; return preview?.RenderAnnotations(wr, TopLeft) ?? Enumerable.Empty(); } public virtual string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) { return worldDefaultCursor; } bool IOrderGenerator.HandleKeyPress(KeyInput e) { if (variants.Length > 0 && placeBuildingInfo.ToggleVariantKey.IsActivatedBy(e)) { if (++variant >= variants.Length) variant = 0; return true; } return false; } void IOrderGenerator.Deactivate() { } IEnumerable ClearBlockersOrders(World world, CPos topLeft) { var allTiles = variants[variant].BuildingInfo.Tiles(topLeft).ToArray(); var adjacentTiles = Util.ExpandFootprint(allTiles, true).Except(allTiles) .Where(world.Map.Contains).ToList(); var blockers = allTiles.SelectMany(world.ActorMap.GetActorsAt) .Where(a => a.Owner == Queue.Actor.Owner && a.IsIdle) .Select(a => new TraitPair(a, a.TraitOrDefault())) .Where(x => x.Trait != null); foreach (var blocker in blockers) { CPos moveCell; if (blocker.Trait is Mobile mobile) { var availableCells = adjacentTiles.Where(t => mobile.CanEnterCell(t)).ToList(); if (availableCells.Count == 0) continue; moveCell = blocker.Actor.ClosestCell(availableCells); } else if (blocker.Trait is Aircraft) moveCell = blocker.Actor.Location; else continue; yield return new Order("Move", blocker.Actor, Target.FromCell(world, moveCell), false) { SuppressVisualFeedback = true }; } } } }