diff --git a/OpenRA.FileFormats/Exts.cs b/OpenRA.FileFormats/Exts.cs index af2e5d98ef..30a629c59d 100755 --- a/OpenRA.FileFormats/Exts.cs +++ b/OpenRA.FileFormats/Exts.cs @@ -110,6 +110,14 @@ namespace OpenRA return xs[r.Next(xs.Length)]; } + public static T RandomOrDefault(this IEnumerable ts, Thirdparty.Random r) + { + if (!ts.Any()) + return default(T); + + return ts.Random(r); + } + public static float Product(this IEnumerable xs) { return xs.Aggregate(1f, (a, x) => a * x); diff --git a/OpenRA.FileFormats/Map/PlayerReference.cs b/OpenRA.FileFormats/Map/PlayerReference.cs index 5107a913ae..d773161ba8 100644 --- a/OpenRA.FileFormats/Map/PlayerReference.cs +++ b/OpenRA.FileFormats/Map/PlayerReference.cs @@ -21,7 +21,7 @@ namespace OpenRA.FileFormats public bool NonCombatant = false; public bool Playable = false; public string Bot = null; - public bool DefaultStartingUnits = false; + public string StartingUnitsClass = null; public bool AllowBots = true; public bool Required = false; diff --git a/OpenRA.Game/ActorInitializer.cs b/OpenRA.Game/ActorInitializer.cs index 5f8a6bb7b2..71ee1eb43f 100755 --- a/OpenRA.Game/ActorInitializer.cs +++ b/OpenRA.Game/ActorInitializer.cs @@ -75,6 +75,7 @@ namespace OpenRA [FieldFromYamlKey] public readonly int value = 0; public SubCellInit() { } public SubCellInit(int init) { value = init; } + public SubCellInit(SubCell init) { value = (int)init; } public SubCell Value(World world) { return (SubCell)value; } } diff --git a/OpenRA.Game/ActorMap.cs b/OpenRA.Game/ActorMap.cs index 6e276b4262..5fc8ac1f8f 100644 --- a/OpenRA.Game/ActorMap.cs +++ b/OpenRA.Game/ActorMap.cs @@ -64,6 +64,16 @@ namespace OpenRA SubCell.BottomLeft, SubCell.BottomRight }.Any(b => !AnyUnitsAt(a,b)); } + public SubCell? FreeSubCell(CPos a) + { + if (!HasFreeSubCell(a)) + return null; + + return new[]{ SubCell.TopLeft, SubCell.TopRight, SubCell.Center, + SubCell.BottomLeft, SubCell.BottomRight }.First(b => !AnyUnitsAt(a,b)); + } + + public bool AnyUnitsAt(CPos a) { return influence[ a.X, a.Y ] != null; diff --git a/OpenRA.Game/Map.cs b/OpenRA.Game/Map.cs index 9b532ecedf..4fad2b5b5f 100644 --- a/OpenRA.Game/Map.cs +++ b/OpenRA.Game/Map.cs @@ -38,6 +38,7 @@ namespace OpenRA public string Author; public string Tileset; public string[] Difficulties; + public bool AllowStartUnitConfig = true; [FieldLoader.Ignore] public Lazy> Actors; @@ -393,7 +394,6 @@ namespace OpenRA Name = "Multi{0}".F(index), Race = "Random", Playable = true, - DefaultStartingUnits = true, Enemies = new[] { "Creeps" } }; Players.Add(p.Name, p); diff --git a/OpenRA.Game/Network/Session.cs b/OpenRA.Game/Network/Session.cs index 1ba16a8da4..d6e52d77ad 100644 --- a/OpenRA.Game/Network/Session.cs +++ b/OpenRA.Game/Network/Session.cs @@ -92,6 +92,7 @@ namespace OpenRA.Network public bool Dedicated; public string Difficulty; public bool Crates = true; + public string StartingUnitsClass = "default"; public bool AllowVersionMismatch; } diff --git a/OpenRA.Mods.RA/MPStartUnits.cs b/OpenRA.Mods.RA/MPStartUnits.cs new file mode 100644 index 0000000000..dc571d74d1 --- /dev/null +++ b/OpenRA.Mods.RA/MPStartUnits.cs @@ -0,0 +1,32 @@ +#region Copyright & License Information +/* + * Copyright 2007-2011 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 OpenRA.FileFormats; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA +{ + public class MPStartUnitsInfo : TraitInfo + { + public readonly string Class = "default"; + public readonly string[] Races = { }; + + public readonly string BaseActor = null; + public readonly string[] SupportActors = { }; + + [Desc("Inner radius for spawning support actors")] + public readonly int InnerSupportRadius = 2; + + [Desc("Outer radius for spawning support actors")] + public readonly int OuterSupportRadius = 4; + } + + public class MPStartUnits { } +} diff --git a/OpenRA.Mods.RA/Move/Mobile.cs b/OpenRA.Mods.RA/Move/Mobile.cs index df97bc8c3d..f7679b096c 100755 --- a/OpenRA.Mods.RA/Move/Mobile.cs +++ b/OpenRA.Mods.RA/Move/Mobile.cs @@ -109,6 +109,11 @@ namespace OpenRA.Mods.RA.Move return true; } + public bool CanEnterCell(World world, CPos cell) + { + return CanEnterCell(world, null, cell, null, true, true); + } + public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor, bool checkTransientActors, bool blockedByMovers) { if (MovementCostForCell(world, cell) == int.MaxValue) @@ -120,13 +125,13 @@ namespace OpenRA.Mods.RA.Move var blockingActors = world.ActorMap.GetUnitsAt(cell) .Where(x => x != ignoreActor) // Neutral/enemy units are blockers. Allied units that are moving are not blockers. - .Where(x => blockedByMovers || ((self.Owner.Stances[x.Owner] != Stance.Ally) || !IsMovingInMyDirection(self, x))) + .Where(x => blockedByMovers || (self == null || self.Owner.Stances[x.Owner] != Stance.Ally || !IsMovingInMyDirection(self, x))) .ToList(); if (checkTransientActors && blockingActors.Count > 0) { // Non-sharable unit can enter a cell with shareable units only if it can crush all of them - if (Crushes == null) + if (self == null || Crushes == null) return false; if (blockingActors.Any(a => !(a.HasTrait() && diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 249cf6de26..17ab4884c0 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -458,6 +458,7 @@ + diff --git a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs index 67a10b0a20..6b28a1b7a7 100644 --- a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs @@ -399,6 +399,32 @@ namespace OpenRA.Mods.RA.Server server.SyncLobbyInfo(); return true; }}, + { "startingunits", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (!server.Map.AllowStartUnitConfig) + { + server.SendOrderTo(conn, "Message", "Map has disabled start unit configuration"); + return true; + } + + var startUnits = Rules.Info["world"].Traits.WithInterface(); + if (!startUnits.Any(msu => msu.Class == s)) + { + server.SendOrderTo(conn, "Message", "Unknown unit class: {0}".F(s)); + return true; + } + + server.lobbyInfo.GlobalSettings.StartingUnitsClass = s; + server.SyncLobbyInfo(); + return true; + }}, { "kick", s => { diff --git a/OpenRA.Mods.RA/SpawnMPUnits.cs b/OpenRA.Mods.RA/SpawnMPUnits.cs index 061779499c..e7c093b591 100644 --- a/OpenRA.Mods.RA/SpawnMPUnits.cs +++ b/OpenRA.Mods.RA/SpawnMPUnits.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2013 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, @@ -8,45 +8,70 @@ */ #endregion +using System; +using System.Linq; + using OpenRA.FileFormats; +using OpenRA.Mods.RA.Move; using OpenRA.Traits; namespace OpenRA.Mods.RA { - class SpawnMPUnitsInfo : ITraitInfo, Requires + public class SpawnMPUnitsInfo : TraitInfo, Requires, Requires { } + + public class SpawnMPUnits : IWorldLoaded { - public readonly string InitialUnit = "mcv"; - public readonly string Faction = null; - - public object Create (ActorInitializer init) { return new SpawnMPUnits(this); } - } - - class SpawnMPUnits : IWorldLoaded - { - SpawnMPUnitsInfo info; - - public SpawnMPUnits(SpawnMPUnitsInfo info) { this.info = info; } - public void WorldLoaded(World world) { foreach (var s in world.WorldActor.Trait().Start) - SpawnUnitsForPlayer(s.Key, s.Value); + SpawnUnitsForPlayer(world, s.Key, s.Value); } - void SpawnUnitsForPlayer(Player p, CPos sp) + void SpawnUnitsForPlayer(World w, Player p, CPos sp) { - if (!p.PlayerReference.DefaultStartingUnits) - return; /* they don't want an mcv, the map provides something else for them */ + var spawnClass = p.PlayerReference.StartingUnitsClass ?? w.LobbyInfo.GlobalSettings.StartingUnitsClass; + var unitGroup = Rules.Info["world"].Traits.WithInterface() + .Where(g => g.Class == spawnClass && g.Races != null && g.Races.Contains(p.Country.Race)) + .RandomOrDefault(w.SharedRandom); - /* support different starting units for each faction */ - if (info.Faction != null && p.Country.Race != info.Faction) + if (unitGroup == null) + throw new InvalidOperationException("No starting units defined for country {0} with class {1}".F(p.Country.Race, spawnClass)); + + // Spawn base actor at the spawnpoint + if (unitGroup.BaseActor != null) + { + w.CreateActor(unitGroup.BaseActor.ToLowerInvariant(), new TypeDictionary + { + new LocationInit(sp), + new OwnerInit(p), + }); + } + + if (!unitGroup.SupportActors.Any()) return; - p.World.CreateActor(info.InitialUnit, new TypeDictionary + // Spawn support units in an annulus around the base actor + var supportSpawnCells = w.FindTilesInCircle(sp, unitGroup.OuterSupportRadius) + .Except(w.FindTilesInCircle(sp, unitGroup.InnerSupportRadius)); + + foreach (var s in unitGroup.SupportActors) { - new LocationInit( sp ), - new OwnerInit( p ), - }); + var mi = Rules.Info[s.ToLowerInvariant()].Traits.Get(); + var validCells = supportSpawnCells.Where(c => mi.CanEnterCell(w, c)); + if (!validCells.Any()) + throw new InvalidOperationException("No cells available to spawn starting unit {0}".F(s)); + + var cell = validCells.Random(w.SharedRandom); + var subCell = w.ActorMap.FreeSubCell(cell).Value; + + w.CreateActor(s.ToLowerInvariant(), new TypeDictionary + { + new OwnerInit(p), + new LocationInit(cell), + new SubCellInit(subCell), + new FacingInit(w.SharedRandom.Next(256)) + }); + } } } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs index 46670340cc..d8869f06d0 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -318,8 +318,46 @@ namespace OpenRA.Mods.RA.Widgets.Logic difficulty.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count() * 30, options, setupItem); }; - var difficultyDesc = optionsBin.GetOrNull("DIFFICULTY_DESC"); - difficultyDesc.IsVisible = difficulty.IsVisible; + optionsBin.Get("DIFFICULTY_DESC").IsVisible = difficulty.IsVisible; + } + + var startingUnits = optionsBin.GetOrNull("STARTINGUNITS_DROPDOWNBUTTON"); + if (startingUnits != null) + { + var classNames = new Dictionary() + { + {"none", "MCV Only"}, + {"default", "Light Support"}, + {"heavy", "Heavy Support"}, + }; + + Func className = c => classNames.ContainsKey(c) ? classNames[c] : c; + var classes = Rules.Info["world"].Traits.WithInterface() + .Select(a => a.Class).Distinct(); + + startingUnits.IsDisabled = configurationDisabled; + startingUnits.IsVisible = () => Map.AllowStartUnitConfig; + startingUnits.GetText = () => className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass); + startingUnits.OnMouseDown = _ => + { + var options = classes.Select(c => new DropDownOption + { + Title = className(c), + IsSelected = () => orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass == c, + OnClick = () => orderManager.IssueOrder(Order.Command("startingunits {0}".F(c))) + }); + + Func setupItem = (option, template) => + { + var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick); + item.Get("LABEL").GetText = () => option.Title; + return item; + }; + + startingUnits.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count() * 30, options, setupItem); + }; + + optionsBin.Get("STARTINGUNITS_DESC").IsVisible = startingUnits.IsVisible; } var disconnectButton = lobby.Get("DISCONNECT_BUTTON"); diff --git a/mods/cnc/chrome/dialogs.yaml b/mods/cnc/chrome/dialogs.yaml index 4f5f8c78ae..deefa547d7 100644 --- a/mods/cnc/chrome/dialogs.yaml +++ b/mods/cnc/chrome/dialogs.yaml @@ -224,12 +224,18 @@ Background@LOBBY_OPTIONS_BIN: Width:230 Height:20 Text:Enable Crates - Checkbox@FRAGILEALLIANCES_CHECKBOX: + Label@STARTINGUNITS_DESC: X:150 Y:140 - Width:230 - Height:20 - Text:Allow Team Changes + Width:120 + Height:25 + Text:Starting Units: + DropDownButton@STARTINGUNITS_DROPDOWNBUTTON: + X:245 + Y:140 + Width:140 + Height:25 + Font:Bold Label@DIFFICULTY_DESC: X:150 Y:170 diff --git a/mods/cnc/maps/gdi01/map.yaml b/mods/cnc/maps/gdi01/map.yaml index 3bc9b2f4b8..991c77c73e 100644 --- a/mods/cnc/maps/gdi01/map.yaml +++ b/mods/cnc/maps/gdi01/map.yaml @@ -20,6 +20,8 @@ UseAsShellmap: False Type: Campaign +AllowStartUnitConfig: False + Players: PlayerReference@BadGuy: Name: BadGuy diff --git a/mods/cnc/maps/nod01/map.yaml b/mods/cnc/maps/nod01/map.yaml index 5950dca237..ec171cb53b 100644 --- a/mods/cnc/maps/nod01/map.yaml +++ b/mods/cnc/maps/nod01/map.yaml @@ -20,6 +20,8 @@ UseAsShellmap: False Type: Campaign +AllowStartUnitConfig: False + Players: PlayerReference@Neutral: Name: Neutral diff --git a/mods/cnc/rules/system.yaml b/mods/cnc/rules/system.yaml index 1f2920bce5..f3dc876469 100644 --- a/mods/cnc/rules/system.yaml +++ b/mods/cnc/rules/system.yaml @@ -307,9 +307,53 @@ World: Depths:5,5,5,5,5,5 DebugOverlay: SpawnMapActors: - CreateMPPlayers: - SpawnMPUnits: MPStartLocations: + CreateMPPlayers: + MPStartUnits@mcvonly: + Class: none + Races: gdi, nod + BaseActor: mcv + MPStartUnits@defaultgdia: + Races: gdi + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e1,e3,e3,jeep + MPStartUnits@defaultgdib: + Races: gdi + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e1,e1,e3,apc + MPStartUnits@defaultnoda: + Races: nod + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e3,bggy,bike + MPStartUnits@defaultnodb: + Races: nod + BaseActor: mcv + SupportActors: e1,e1,e1,e3,e3,e3,bggy + MPStartUnits@defaultnodc: + Races: nod + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e1,e1,e1,e3,bike + MPStartUnits@heavynoda: + Class: heavy + Races: nod + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e3,e3,ltnk,ltnk,ftnk + MPStartUnits@heavynodb: + Class: heavy + Races: nod + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e1,e3,e3,e3,ftnk,ftnk + MPStartUnits@heavygdia: + Class: heavy + Races: gdi + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e3,e3,jeep,mtnk,mtnk + MPStartUnits@heavygdib: + Class: heavy + Races: gdi + BaseActor: mcv + SupportActors: e1,e1,e1,e1,e1,e2,e2,e2,e3,e3,apc,mtnk + SpawnMPUnits: SpatialBins: BinSize: 4 CrateSpawner: diff --git a/mods/d2k/rules/system.yaml b/mods/d2k/rules/system.yaml index 79b5c49606..639a37d593 100644 --- a/mods/d2k/rules/system.yaml +++ b/mods/d2k/rules/system.yaml @@ -380,15 +380,16 @@ World: SpawnMapActors: CreateMPPlayers: MPStartLocations: - SpawnMPUnits@atreides: - InitialUnit: mcva - Faction: atreides - SpawnMPUnits@harkonnen: - InitialUnit: mcvh - Faction: harkonnen - SpawnMPUnits@ordos: - InitialUnit: mcvo - Faction: ordos + MPStartUnits@atreides: + Races: atreides + BaseActor: mcva + MPStartUnits@harkonnen: + Races: harkonnen + BaseActor: mcvh + MPStartUnits@ordos: + Races: ordos + BaseActor: mcvo + SpawnMPUnits: SpatialBins: BinSize: 4 PathFinder: diff --git a/mods/ra/maps/bomber-john/map.yaml b/mods/ra/maps/bomber-john/map.yaml index ed51811886..fdf8001184 100644 --- a/mods/ra/maps/bomber-john/map.yaml +++ b/mods/ra/maps/bomber-john/map.yaml @@ -759,8 +759,7 @@ Smudges: Rules: World: -CrateDrop: - SpawnMPUnits: - InitialUnit: mnlyr + -SpawnMPUnits: APWR: Buildable: diff --git a/mods/ra/maps/doughnut-hole/map.yaml b/mods/ra/maps/doughnut-hole/map.yaml index 6b0610f4e0..5d8a7e77be 100644 --- a/mods/ra/maps/doughnut-hole/map.yaml +++ b/mods/ra/maps/doughnut-hole/map.yaml @@ -30,24 +30,32 @@ Players: Race: Random Enemies: Creeps AllowBots: False + DefaultStartingUnits: True + StartingUnitsClass: custom PlayerReference@Multi1: Name: Multi1 Playable: True Race: Random Enemies: Creeps AllowBots: False + DefaultStartingUnits: True + StartingUnitsClass: custom PlayerReference@Multi2: Name: Multi2 Playable: True Race: Random Enemies: Creeps AllowBots: False + DefaultStartingUnits: True + StartingUnitsClass: custom PlayerReference@Multi3: Name: Multi3 Playable: True Race: Random Enemies: Creeps AllowBots: False + DefaultStartingUnits: True + StartingUnitsClass: custom PlayerReference@Creeps: Name: Creeps NonCombatant: True @@ -100,18 +108,6 @@ Actors: Actor14: t10 Location: 73,71 Owner: Neutral - Actor15: mpspawn - Location: 49,59 - Owner: Neutral - Actor16: mpspawn - Location: 54,79 - Owner: Neutral - Actor17: mpspawn - Location: 76,75 - Owner: Neutral - Actor18: mpspawn - Location: 71,55 - Owner: Neutral Actor22: cycl Location: 59,67 Owner: Neutral @@ -631,30 +627,18 @@ Actors: Actor191: v14 Location: 110,69 Owner: Neutral - Actor192: mcv + Actor15: mpspawn Location: 48,59 - Owner: Multi0 - Actor193: lst - Location: 45,57 - Owner: Multi0 - Actor194: lst - Location: 50,81 - Owner: Multi1 - Actor195: mcv + Owner: Neutral + Actor16: mpspawn Location: 52,80 - Owner: Multi1 - Actor196: mcv + Owner: Neutral + Actor17: mpspawn Location: 80,76 - Owner: Multi2 - Actor197: lst - Location: 82,77 - Owner: Multi2 - Actor198: lst - Location: 75,49 - Owner: Multi3 - Actor199: mcv + Owner: Neutral + Actor18: mpspawn Location: 74,51 - Owner: Multi3 + Owner: Neutral Actor200: mine Location: 104,71 Owner: Neutral @@ -859,6 +843,14 @@ Rules: NukePower: AllowMultiple: yes + World: + MPStartUnits@custom: + Class: custom + Races: soviet, allies + BaseActor: mcv + SupportActors: lst + InnerSupportRadius: 3 + OuterSupportRadius: 4 Sequences: Weapons: diff --git a/mods/ra/rules/system.yaml b/mods/ra/rules/system.yaml index 4953c644ec..af115dc9e3 100644 --- a/mods/ra/rules/system.yaml +++ b/mods/ra/rules/system.yaml @@ -639,6 +639,9 @@ World: DebugOverlay: SpawnMapActors: CreateMPPlayers: + MPStartUnits: + Races: soviet, allies + BaseActor: mcv MPStartLocations: SpawnMPUnits: SpatialBins: