diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs index 6bca091ef6..5ee57ec54b 100644 --- a/OpenRA.Game/GameInformation.cs +++ b/OpenRA.Game/GameInformation.cs @@ -122,7 +122,7 @@ namespace OpenRA Team = client.Team, SpawnPoint = runtimePlayer.SpawnPoint, IsRandomFaction = runtimePlayer.Faction.InternalName != client.Faction, - IsRandomSpawnPoint = runtimePlayer.SpawnPoint != client.SpawnPoint, + IsRandomSpawnPoint = runtimePlayer.DisplaySpawnPoint == 0, Fingerprint = client.Fingerprint }; diff --git a/OpenRA.Game/Map/PlayerReference.cs b/OpenRA.Game/Map/PlayerReference.cs index 16a4aaa693..27561656e5 100644 --- a/OpenRA.Game/Map/PlayerReference.cs +++ b/OpenRA.Game/Map/PlayerReference.cs @@ -32,7 +32,19 @@ namespace OpenRA public bool LockColor = false; public Color Color = Game.ModData.Manifest.Get().Color; + /// + /// Sets the "Home" location, which can be used by traits and scripts to e.g. set the initial camera + /// location or choose the map edge for reinforcements. + /// This will usually be overridden for client (lobby slot) players with a location based on the Spawn index + /// + public CPos HomeLocation = CPos.Zero; + public bool LockSpawn = false; + + /// + /// Sets the initial spawn point index that is used to override the "Home" location for client (lobby slot) players. + /// Map players always ignore this and use HomeLocation directly. + /// public int Spawn = 0; public bool LockTeam = false; diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index 3e5f6b1a51..c2537e016a 100644 --- a/OpenRA.Game/Player.cs +++ b/OpenRA.Game/Player.cs @@ -56,6 +56,7 @@ namespace OpenRA public readonly bool NonCombatant = false; public readonly bool Playable = true; public readonly int ClientIndex; + public readonly CPos HomeLocation; public readonly PlayerReference PlayerReference; public readonly bool IsBot; public readonly string BotType; @@ -65,8 +66,13 @@ namespace OpenRA /// The faction (including Random, etc) that was selected in the lobby. public readonly FactionInfo DisplayFaction; + /// The spawn point index that was assigned for client-based players. + public readonly int SpawnPoint; + + /// The spawn point index (including 0 for Random) that was selected in the lobby for client-based players. + public readonly int DisplaySpawnPoint; + public WinState WinState = WinState.Undefined; - public int SpawnPoint; public bool HasObjectives = false; public bool Spectating; @@ -153,6 +159,11 @@ namespace OpenRA BotType = client.Bot; Faction = ChooseFaction(world, client.Faction, !pr.LockFaction); DisplayFaction = ChooseDisplayFaction(world, client.Faction); + + var assignSpawnPoints = world.WorldActor.TraitOrDefault(); + HomeLocation = assignSpawnPoints?.AssignHomeLocation(world, client) ?? pr.HomeLocation; + SpawnPoint = assignSpawnPoints?.SpawnPointForPlayer(this) ?? client.SpawnPoint; + DisplaySpawnPoint = client.SpawnPoint; } else { @@ -166,6 +177,8 @@ namespace OpenRA BotType = pr.Bot; Faction = ChooseFaction(world, pr.Faction, false); DisplayFaction = ChooseDisplayFaction(world, pr.Faction); + HomeLocation = pr.HomeLocation; + SpawnPoint = DisplaySpawnPoint = 0; } if (!Spectating) diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 8e6bf6d79a..d56e21f9f7 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -373,6 +373,13 @@ namespace OpenRA.Traits void CreateServerPlayers(MapPreview map, Session lobbyInfo, List players); } + [RequireExplicitImplementation] + public interface IAssignSpawnPoints + { + CPos AssignHomeLocation(World world, Session.Client client); + int SpawnPointForPlayer(Player player); + } + public interface IBotInfo : ITraitInfoInterface { string Type { get; } diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index 55d94fefc9..d29aec8aef 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -314,6 +314,7 @@ namespace OpenRA using (new PerfTimer(iwl.GetType().Name + ".WorldLoaded")) iwl.WorldLoaded(this, wr); + var assignSpawnLocations = WorldActor.TraitOrDefault(); gameInfo.StartTimeUtc = DateTime.UtcNow; foreach (var player in Players) gameInfo.AddPlayer(player, OrderManager.LobbyInfo); diff --git a/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs index 6bbc9706d0..613d1d1e4e 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs @@ -45,7 +45,7 @@ namespace OpenRA.Mods.Common.Scripting get { var c = Player.World.LobbyInfo.Clients.FirstOrDefault(i => i.Index == Player.ClientIndex); - return c != null ? c.Team : 0; + return c?.Team ?? 0; } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs b/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs index 6e312c41d7..d0faa087cd 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/ProductionAirdrop.cs @@ -51,19 +51,17 @@ namespace OpenRA.Mods.Common.Traits var owner = self.Owner; var map = owner.World.Map; var aircraftInfo = self.World.Map.Rules.Actors[info.ActorType].TraitInfo(); - var mpStart = owner.World.WorldActor.TraitOrDefault(); CPos startPos; CPos endPos; WAngle spawnFacing; - if (info.BaselineSpawn && mpStart != null) + if (info.BaselineSpawn) { - var spawn = mpStart.Start[owner]; var bounds = map.Bounds; var center = new MPos(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2).ToCPos(map); - var spawnVec = spawn - center; - startPos = spawn + spawnVec * (Exts.ISqrt((bounds.Height * bounds.Height + bounds.Width * bounds.Width) / (4 * spawnVec.LengthSquared))); + var spawnVec = owner.HomeLocation - center; + startPos = owner.HomeLocation + spawnVec * (Exts.ISqrt((bounds.Height * bounds.Height + bounds.Width * bounds.Width) / (4 * spawnVec.LengthSquared))); endPos = startPos; var spawnDirection = new WVec((self.Location - startPos).X, (self.Location - startPos).Y, 0); spawnFacing = spawnDirection.Yaw; diff --git a/OpenRA.Mods.Common/Traits/World/MPStartLocations.cs b/OpenRA.Mods.Common/Traits/World/MPStartLocations.cs index 7d98bc22a4..ce7e009eb2 100644 --- a/OpenRA.Mods.Common/Traits/World/MPStartLocations.cs +++ b/OpenRA.Mods.Common/Traits/World/MPStartLocations.cs @@ -9,11 +9,10 @@ */ #endregion -using System; using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; -using OpenRA.Primitives; +using OpenRA.Network; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -58,13 +57,13 @@ namespace OpenRA.Mods.Common.Traits } } - public class MPStartLocations : IWorldLoaded, INotifyCreated + public class MPStartLocations : IWorldLoaded, INotifyCreated, IAssignSpawnPoints { readonly MPStartLocationsInfo info; - - public readonly Dictionary Start = new Dictionary(); - + readonly Dictionary occupiedSpawnPoints = new Dictionary(); bool separateTeamSpawns; + CPos[] spawnLocations; + List availableSpawnPoints; public MPStartLocations(MPStartLocationsInfo info) { @@ -75,71 +74,69 @@ namespace OpenRA.Mods.Common.Traits { separateTeamSpawns = self.World.LobbyInfo.GlobalSettings .OptionOrDefault("separateteamspawns", info.SeparateTeamSpawnsCheckboxEnabled); + + var spawns = new List(); + foreach (var n in self.World.Map.ActorDefinitions) + if (n.Value.Value == "mpspawn") + spawns.Add(new ActorReference(n.Key, n.Value.ToDictionary()).GetValue()); + + spawnLocations = spawns.ToArray(); + + // Initialize the list of unoccupied spawn points for AssignSpawnLocations to pick from + availableSpawnPoints = Enumerable.Range(1, spawnLocations.Length).ToList(); + foreach (var kv in self.World.LobbyInfo.Slots) + { + var client = self.World.LobbyInfo.ClientInSlot(kv.Key); + if (client == null || client.SpawnPoint == 0) + continue; + + availableSpawnPoints.Remove(client.SpawnPoint); + occupiedSpawnPoints.Add(client.SpawnPoint, client); + } } - public void WorldLoaded(World world, WorldRenderer wr) + CPos IAssignSpawnPoints.AssignHomeLocation(World world, Session.Client client) { - var spawns = world.Actors.Where(a => a.Info.Name == "mpspawn") - .Select(a => a.Location) - .ToArray(); + if (client.SpawnPoint > 0 && client.SpawnPoint <= spawnLocations.Length) + return spawnLocations[client.SpawnPoint - 1]; - var taken = world.LobbyInfo.Clients.Where(c => c.SpawnPoint != 0 && c.Slot != null) - .Select(c => spawns[c.SpawnPoint - 1]).ToList(); - var available = spawns.Except(taken).ToList(); + var spawnPoint = occupiedSpawnPoints.Count == 0 || !separateTeamSpawns + ? availableSpawnPoints.Random(world.SharedRandom) + : availableSpawnPoints // pick the most distant spawnpoint from everyone else + .Select(s => (Cell: spawnLocations[s - 1], Index: s)) + .MaxBy(s => occupiedSpawnPoints.Sum(kv => (spawnLocations[kv.Key - 1] - s.Cell).LengthSquared)).Index; - // Set spawn - foreach (var kv in world.LobbyInfo.Slots) + availableSpawnPoints.Remove(spawnPoint); + occupiedSpawnPoints.Add(spawnPoint, client); + return spawnLocations[spawnPoint - 1]; + } + + int IAssignSpawnPoints.SpawnPointForPlayer(Player player) + { + foreach (var kv in occupiedSpawnPoints) + if (kv.Value.Index == player.ClientIndex) + return kv.Key; + + return 0; + } + + void IWorldLoaded.WorldLoaded(World world, WorldRenderer wr) + { + foreach (var p in world.Players) { - var player = FindPlayerInSlot(world, kv.Key); - if (player == null) continue; + if (!p.Playable) + continue; - var client = world.LobbyInfo.ClientInSlot(kv.Key); - var spid = (client == null || client.SpawnPoint == 0) - ? ChooseSpawnPoint(world, available, taken) - : spawns[client.SpawnPoint - 1]; + if (p == world.LocalPlayer) + wr.Viewport.Center(world.Map.CenterOfCell(p.HomeLocation)); - Start.Add(player, spid); + var cells = Shroud.ProjectedCellsInRange(world.Map, p.HomeLocation, info.InitialExploreRange) + .ToList(); - player.SpawnPoint = (client == null || client.SpawnPoint == 0) - ? spawns.IndexOf(spid) + 1 - : client.SpawnPoint; - } - - // Explore allied shroud - var map = world.Map; - foreach (var p in Start.Keys) - { - var cells = Shroud.ProjectedCellsInRange(map, Start[p], info.InitialExploreRange); foreach (var q in world.Players) if (p.IsAlliedWith(q)) q.Shroud.ExploreProjectedCells(world, cells); } - - // Set viewport - if (world.LocalPlayer != null && Start.ContainsKey(world.LocalPlayer)) - wr.Viewport.Center(map.CenterOfCell(Start[world.LocalPlayer])); - } - - static Player FindPlayerInSlot(World world, string pr) - { - return world.Players.FirstOrDefault(p => p.PlayerReference.Name == pr); - } - - CPos ChooseSpawnPoint(World world, List available, List taken) - { - if (available.Count == 0) - throw new InvalidOperationException("No free spawnpoint."); - - var n = taken.Count == 0 || !separateTeamSpawns - ? world.SharedRandom.Next(available.Count) - : available // pick the most distant spawnpoint from everyone else - .Select((k, i) => (Cell: k, Index: i)) - .MaxBy(a => taken.Sum(t => (t - a.Cell).LengthSquared)).Index; - - var sp = available[n]; - available.RemoveAt(n); - taken.Add(sp); - return sp; } } } diff --git a/OpenRA.Mods.Common/Traits/World/SpawnMPUnits.cs b/OpenRA.Mods.Common/Traits/World/SpawnMPUnits.cs index ef40ba58ef..1a984f2e4f 100644 --- a/OpenRA.Mods.Common/Traits/World/SpawnMPUnits.cs +++ b/OpenRA.Mods.Common/Traits/World/SpawnMPUnits.cs @@ -19,7 +19,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Spawn base actor at the spawnpoint and support units in an annulus around the base actor. Both are defined at MPStartUnits. Attach this to the world actor.")] - public class SpawnMPUnitsInfo : TraitInfo, Requires, Requires, ILobbyOptions + public class SpawnMPUnitsInfo : TraitInfo, Requires, ILobbyOptions { public readonly string StartingUnitsClass = "none"; @@ -67,11 +67,12 @@ namespace OpenRA.Mods.Common.Traits public void WorldLoaded(World world, WorldRenderer wr) { - foreach (var s in world.WorldActor.Trait().Start) - SpawnUnitsForPlayer(world, s.Key, s.Value); + foreach (var p in world.Players) + if (p.Playable) + SpawnUnitsForPlayer(world, p); } - void SpawnUnitsForPlayer(World w, Player p, CPos sp) + void SpawnUnitsForPlayer(World w, Player p) { var spawnClass = p.PlayerReference.StartingUnitsClass ?? w.LobbyInfo.GlobalSettings .OptionOrDefault("startingunits", info.StartingUnitsClass); @@ -88,7 +89,7 @@ namespace OpenRA.Mods.Common.Traits var facing = unitGroup.BaseActorFacing.HasValue ? unitGroup.BaseActorFacing.Value : new WAngle(w.SharedRandom.Next(1024)); w.CreateActor(unitGroup.BaseActor.ToLowerInvariant(), new TypeDictionary { - new LocationInit(sp + unitGroup.BaseActorOffset), + new LocationInit(p.HomeLocation + unitGroup.BaseActorOffset), new OwnerInit(p), new SkipMakeAnimsInit(), new FacingInit(facing), @@ -98,7 +99,7 @@ namespace OpenRA.Mods.Common.Traits if (!unitGroup.SupportActors.Any()) return; - var supportSpawnCells = w.Map.FindTilesInAnnulus(sp, unitGroup.InnerSupportRadius + 1, unitGroup.OuterSupportRadius); + var supportSpawnCells = w.Map.FindTilesInAnnulus(p.HomeLocation, unitGroup.InnerSupportRadius + 1, unitGroup.OuterSupportRadius); foreach (var s in unitGroup.SupportActors) {