From a537346580d8ceeeb4c1e7ed78ee94e8ef84f81c Mon Sep 17 00:00:00 2001 From: Ivaylo Draganov Date: Sun, 19 Sep 2021 21:44:05 +0300 Subject: [PATCH] Move selection hotkeys out of world interaction widget - Split SelectionUtils for selecting actors in the world to a static class - Split selection hotkeys into their own logic classes --- .../Hotkeys/SelectAllUnitsHotkeyLogic.cs | 66 +++++++++ .../Hotkeys/SelectUnitsByTypeHotkeyLogic.cs | 88 +++++++++++ OpenRA.Mods.Common/Widgets/SelectionUtils.cs | 83 +++++++++++ .../WorldInteractionControllerWidget.cs | 140 +----------------- mods/cnc/chrome/ingame.yaml | 6 +- mods/common/chrome/ingame.yaml | 6 +- 6 files changed, 248 insertions(+), 141 deletions(-) create mode 100644 OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectAllUnitsHotkeyLogic.cs create mode 100644 OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectUnitsByTypeHotkeyLogic.cs create mode 100644 OpenRA.Mods.Common/Widgets/SelectionUtils.cs diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectAllUnitsHotkeyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectAllUnitsHotkeyLogic.cs new file mode 100644 index 0000000000..9b96fb51b5 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectAllUnitsHotkeyLogic.cs @@ -0,0 +1,66 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Lint; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic.Ingame +{ + [ChromeLogicArgsHotkeys("SelectAllUnitsKey")] + public class SelectAllUnitsHotkeyLogic : SingleHotkeyBaseLogic + { + readonly World world; + readonly WorldRenderer worldRenderer; + readonly ISelection selection; + + public readonly string ClickSound = ChromeMetrics.Get("ClickSound"); + + [ObjectCreator.UseCtor] + public SelectAllUnitsHotkeyLogic(Widget widget, ModData modData, WorldRenderer worldRenderer, World world, Dictionary logicArgs) + : base(widget, modData, "SelectAllUnitsKey", "WORLD_KEYHANDLER", logicArgs) + { + this.world = world; + this.worldRenderer = worldRenderer; + selection = world.Selection; + } + + protected override bool OnHotkeyActivated(KeyInput e) + { + if (world.IsGameOver) + return false; + + var eligiblePlayers = SelectionUtils.GetPlayersToIncludeInSelection(world); + + // Select actors on the screen which belong to the current player(s) + var ownUnitsOnScreen = SelectionUtils.SelectActorsOnScreen(world, worldRenderer, null, eligiblePlayers).SubsetWithHighestSelectionPriority(e.Modifiers).ToList(); + + // Check if selecting actors on the screen has selected new units + if (ownUnitsOnScreen.Count > selection.Actors.Count()) + TextNotificationsManager.AddFeedbackLine("Selected across screen."); + else + { + // Select actors in the world that have highest selection priority + ownUnitsOnScreen = SelectionUtils.SelectActorsInWorld(world, null, eligiblePlayers).SubsetWithHighestSelectionPriority(e.Modifiers).ToList(); + TextNotificationsManager.AddFeedbackLine("Selected across map."); + } + + selection.Combine(world, ownUnitsOnScreen, false, false); + + Game.Sound.PlayNotification(world.Map.Rules, world.LocalPlayer, "Sounds", ClickSound, null); + + return true; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectUnitsByTypeHotkeyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectUnitsByTypeHotkeyLogic.cs new file mode 100644 index 0000000000..7055d2b723 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/Hotkeys/SelectUnitsByTypeHotkeyLogic.cs @@ -0,0 +1,88 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Lint; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets.Logic.Ingame +{ + [ChromeLogicArgsHotkeys("SelectUnitsByTypeKey")] + public class SelectUnitsByTypeHotkeyLogic : SingleHotkeyBaseLogic + { + readonly World world; + readonly WorldRenderer worldRenderer; + readonly ISelection selection; + + public readonly string ClickSound = ChromeMetrics.Get("ClickSound"); + public readonly string ClickDisabledSound = ChromeMetrics.Get("ClickDisabledSound"); + + [ObjectCreator.UseCtor] + public SelectUnitsByTypeHotkeyLogic(Widget widget, ModData modData, WorldRenderer worldRenderer, World world, Dictionary logicArgs) + : base(widget, modData, "SelectUnitsByTypeKey", "WORLD_KEYHANDLER", logicArgs) + { + this.world = world; + this.worldRenderer = worldRenderer; + selection = world.Selection; + } + + protected override bool OnHotkeyActivated(KeyInput e) + { + if (world.IsGameOver) + return false; + + if (!selection.Actors.Any()) + { + TextNotificationsManager.AddFeedbackLine("Nothing selected."); + Game.Sound.PlayNotification(world.Map.Rules, world.LocalPlayer, "Sounds", ClickDisabledSound, null); + + return false; + } + + var eligiblePlayers = SelectionUtils.GetPlayersToIncludeInSelection(world); + + var ownedActors = selection.Actors + .Where(x => !x.IsDead && eligiblePlayers.Contains(x.Owner)) + .ToList(); + + if (!ownedActors.Any()) + return false; + + // Get all the selected actors' selection classes + var selectedClasses = ownedActors + .Select(a => a.Trait().Class) + .ToHashSet(); + + // Select actors on the screen that have the same selection class as one of the already selected actors + var newSelection = SelectionUtils.SelectActorsOnScreen(world, worldRenderer, selectedClasses, eligiblePlayers).ToList(); + + // Check if selecting actors on the screen has selected new units + if (newSelection.Count > selection.Actors.Count()) + TextNotificationsManager.AddFeedbackLine("Selected across screen."); + else + { + // Select actors in the world that have the same selection class as one of the already selected actors + newSelection = SelectionUtils.SelectActorsInWorld(world, selectedClasses, eligiblePlayers).ToList(); + TextNotificationsManager.AddFeedbackLine("Selected across map."); + } + + selection.Combine(world, newSelection, true, false); + + Game.Sound.PlayNotification(world.Map.Rules, world.LocalPlayer, "Sounds", ClickSound, null); + + return true; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/SelectionUtils.cs b/OpenRA.Mods.Common/Widgets/SelectionUtils.cs new file mode 100644 index 0000000000..09d095af42 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/SelectionUtils.cs @@ -0,0 +1,83 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 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, 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.Traits; + +namespace OpenRA.Mods.Common.Widgets +{ + public static class SelectionUtils + { + public static IEnumerable SelectActorsOnScreen(World world, WorldRenderer wr, IEnumerable selectionClasses, IEnumerable players) + { + var actors = world.ScreenMap.ActorsInMouseBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight).Select(a => a.Actor); + return SelectActorsByOwnerAndSelectionClass(actors, players, selectionClasses); + } + + public static IEnumerable SelectActorsInWorld(World world, IEnumerable selectionClasses, IEnumerable players) + { + return SelectActorsByOwnerAndSelectionClass(world.Actors.Where(a => a.IsInWorld), players, selectionClasses); + } + + public static IEnumerable SelectActorsByOwnerAndSelectionClass(IEnumerable actors, IEnumerable owners, IEnumerable selectionClasses) + { + return actors.Where(a => + { + if (!owners.Contains(a.Owner)) + return false; + + var s = a.TraitOrDefault(); + + // selectionClasses == null means that units, that meet all other criteria, get selected + return s != null && (selectionClasses == null || selectionClasses.Contains(s.Class)); + }); + } + + public static IEnumerable SelectHighestPriorityActorAtPoint(World world, int2 a, Modifiers modifiers) + { + var selected = world.ScreenMap.ActorsAtMouse(a) + .Where(x => x.Actor.Info.HasTraitInfo() && (x.Actor.Owner.IsAlliedWith(world.RenderPlayer) || !world.FogObscures(x.Actor))) + .WithHighestSelectionPriority(a, modifiers); + + if (selected != null) + yield return selected; + } + + public static IEnumerable SelectActorsInBoxWithDeadzone(World world, int2 a, int2 b, Modifiers modifiers) + { + // For dragboxes that are too small, shrink the dragbox to a single point (point b) + if ((a - b).Length <= Game.Settings.Game.SelectionDeadzone) + a = b; + + if (a == b) + return SelectHighestPriorityActorAtPoint(world, a, modifiers); + + return world.ScreenMap.ActorsInMouseBox(a, b) + .Select(x => x.Actor) + .Where(x => x.Info.HasTraitInfo() && (x.Owner.IsAlliedWith(world.RenderPlayer) || !world.FogObscures(x))) + .SubsetWithHighestSelectionPriority(modifiers); + } + + public static Player[] GetPlayersToIncludeInSelection(World world) + { + // Players to be included in the selection (the viewer or all players in "Disable shroud" / "All players" mode) + var viewer = world.RenderPlayer ?? world.LocalPlayer; + var isShroudDisabled = viewer == null || (world.RenderPlayer == null && world.LocalPlayer.Spectating); + var isEveryone = viewer != null && viewer.NonCombatant && viewer.Spectating; + + return isShroudDisabled || isEveryone ? world.Players : new[] { viewer }; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs b/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs index 73b1812d6c..2579c19184 100644 --- a/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/WorldInteractionControllerWidget.cs @@ -22,9 +22,6 @@ namespace OpenRA.Mods.Common.Widgets { public class WorldInteractionControllerWidget : Widget { - public readonly HotkeyReference SelectAllKey = new HotkeyReference(); - public readonly HotkeyReference SelectSameTypeKey = new HotkeyReference(); - protected readonly World World; readonly WorldRenderer worldRenderer; readonly Color normalSelectionColor; @@ -72,12 +69,12 @@ namespace OpenRA.Mods.Common.Widgets Game.Renderer.RgbaColorRenderer.DrawRect(a, b, 1, color); // Render actors in the dragbox - rollover = SelectActorsInBoxWithDeadzone(World, dragStart, mousePos, modifiers); + rollover = SelectionUtils.SelectActorsInBoxWithDeadzone(World, dragStart, mousePos, modifiers); } else { // Render actors under the mouse pointer - rollover = SelectActorsInBoxWithDeadzone(World, mousePos, mousePos, modifiers); + rollover = SelectionUtils.SelectActorsInBoxWithDeadzone(World, mousePos, mousePos, modifiers); } worldRenderer.World.Selection.SetRollover(rollover); @@ -127,11 +124,7 @@ namespace OpenRA.Mods.Common.Widgets var unit = World.ScreenMap.ActorsAtMouse(mousePos) .WithHighestSelectionPriority(mousePos, mi.Modifiers); - // Players to be included in the selection (the viewer or all players in "Disable shroud" / "All players" mode) - var viewer = World.RenderPlayer ?? World.LocalPlayer; - var isShroudDisabled = viewer == null || (World.RenderPlayer == null && World.LocalPlayer.Spectating); - var isEveryone = viewer != null && viewer.NonCombatant && viewer.Spectating; - var eligiblePlayers = isShroudDisabled || isEveryone ? World.Players : new[] { viewer }; + var eligiblePlayers = SelectionUtils.GetPlayersToIncludeInSelection(World); if (unit != null && eligiblePlayers.Contains(unit.Owner)) { @@ -139,7 +132,7 @@ namespace OpenRA.Mods.Common.Widgets if (s != null) { // Select actors on the screen that have the same selection class as the actor under the mouse cursor - var newSelection = SelectActorsOnScreen(World, worldRenderer, new HashSet { s.Class }, eligiblePlayers); + var newSelection = SelectionUtils.SelectActorsOnScreen(World, worldRenderer, new HashSet { s.Class }, eligiblePlayers); World.Selection.Combine(World, newSelection, true, false); } @@ -158,7 +151,7 @@ namespace OpenRA.Mods.Common.Widgets */ if (isDragging && (uog.ClearSelectionOnLeftClick || IsValidDragbox)) { - var newSelection = SelectActorsInBoxWithDeadzone(World, dragStart, mousePos, mi.Modifiers); + var newSelection = SelectionUtils.SelectActorsInBoxWithDeadzone(World, dragStart, mousePos, mi.Modifiers); World.Selection.Combine(World, newSelection, mi.Modifiers.HasModifier(Modifiers.Shift), dragStart == mousePos); } } @@ -233,128 +226,5 @@ namespace OpenRA.Mods.Common.Widgets return World.OrderGenerator.GetCursor(World, cell, worldPixel, mi); }); } - - public override bool HandleKeyPress(KeyInput e) - { - if (e.Event == KeyInputEvent.Down) - { - // Players to be included in the selection (the viewer or all players in "Disable shroud" / "All players" mode) - var viewer = World.RenderPlayer ?? World.LocalPlayer; - var isShroudDisabled = viewer == null || (World.RenderPlayer == null && World.LocalPlayer.Spectating); - var isEveryone = viewer != null && viewer.NonCombatant && viewer.Spectating; - var eligiblePlayers = isShroudDisabled || isEveryone ? World.Players : new[] { viewer }; - - if (SelectAllKey.IsActivatedBy(e) && !World.IsGameOver) - { - // Select actors on the screen which belong to the current player(s) - var ownUnitsOnScreen = SelectActorsOnScreen(World, worldRenderer, null, eligiblePlayers).SubsetWithHighestSelectionPriority(e.Modifiers).ToList(); - - // Check if selecting actors on the screen has selected new units - if (ownUnitsOnScreen.Count > World.Selection.Actors.Count()) - TextNotificationsManager.AddFeedbackLine("Selected across screen."); - else - { - // Select actors in the world that have highest selection priority - ownUnitsOnScreen = SelectActorsInWorld(World, null, eligiblePlayers).SubsetWithHighestSelectionPriority(e.Modifiers).ToList(); - TextNotificationsManager.AddFeedbackLine("Selected across map."); - } - - World.Selection.Combine(World, ownUnitsOnScreen, false, false); - - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null); - } - else if (SelectSameTypeKey.IsActivatedBy(e) && !World.IsGameOver) - { - if (!World.Selection.Actors.Any()) - { - TextNotificationsManager.AddFeedbackLine("Nothing selected."); - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickDisabledSound, null); - - return false; - } - - var ownedActors = World.Selection.Actors - .Where(x => !x.IsDead && eligiblePlayers.Contains(x.Owner)) - .ToList(); - - if (!ownedActors.Any()) - return false; - - // Get all the selected actors' selection classes - var selectedClasses = ownedActors - .Select(a => a.Trait().Class) - .ToHashSet(); - - // Select actors on the screen that have the same selection class as one of the already selected actors - var newSelection = SelectActorsOnScreen(World, worldRenderer, selectedClasses, eligiblePlayers).ToList(); - - // Check if selecting actors on the screen has selected new units - if (newSelection.Count > World.Selection.Actors.Count()) - TextNotificationsManager.AddFeedbackLine("Selected across screen."); - else - { - // Select actors in the world that have the same selection class as one of the already selected actors - newSelection = SelectActorsInWorld(World, selectedClasses, eligiblePlayers).ToList(); - TextNotificationsManager.AddFeedbackLine("Selected across map."); - } - - World.Selection.Combine(World, newSelection, true, false); - - Game.Sound.PlayNotification(World.Map.Rules, World.LocalPlayer, "Sounds", ClickSound, null); - } - } - - return false; - } - - static IEnumerable SelectActorsOnScreen(World world, WorldRenderer wr, IEnumerable selectionClasses, IEnumerable players) - { - var actors = world.ScreenMap.ActorsInMouseBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight).Select(a => a.Actor); - return SelectActorsByOwnerAndSelectionClass(actors, players, selectionClasses); - } - - static IEnumerable SelectActorsInWorld(World world, IEnumerable selectionClasses, IEnumerable players) - { - return SelectActorsByOwnerAndSelectionClass(world.Actors.Where(a => a.IsInWorld), players, selectionClasses); - } - - static IEnumerable SelectActorsByOwnerAndSelectionClass(IEnumerable actors, IEnumerable owners, IEnumerable selectionClasses) - { - return actors.Where(a => - { - if (!owners.Contains(a.Owner)) - return false; - - var s = a.TraitOrDefault(); - - // selectionClasses == null means that units, that meet all other criteria, get selected - return s != null && (selectionClasses == null || selectionClasses.Contains(s.Class)); - }); - } - - static IEnumerable SelectHighestPriorityActorAtPoint(World world, int2 a, Modifiers modifiers) - { - var selected = world.ScreenMap.ActorsAtMouse(a) - .Where(x => x.Actor.Info.HasTraitInfo() && (x.Actor.Owner.IsAlliedWith(world.RenderPlayer) || !world.FogObscures(x.Actor))) - .WithHighestSelectionPriority(a, modifiers); - - if (selected != null) - yield return selected; - } - - static IEnumerable SelectActorsInBoxWithDeadzone(World world, int2 a, int2 b, Modifiers modifiers) - { - // For dragboxes that are too small, shrink the dragbox to a single point (point b) - if ((a - b).Length <= Game.Settings.Game.SelectionDeadzone) - a = b; - - if (a == b) - return SelectHighestPriorityActorAtPoint(world, a, modifiers); - - return world.ScreenMap.ActorsInMouseBox(a, b) - .Select(x => x.Actor) - .Where(x => x.Info.HasTraitInfo() && (x.Owner.IsAlliedWith(world.RenderPlayer) || !world.FogObscures(x))) - .SubsetWithHighestSelectionPriority(modifiers); - } } } diff --git a/mods/cnc/chrome/ingame.yaml b/mods/cnc/chrome/ingame.yaml index 17cddf2034..3c4c674dd9 100644 --- a/mods/cnc/chrome/ingame.yaml +++ b/mods/cnc/chrome/ingame.yaml @@ -10,7 +10,7 @@ Container@INGAME_ROOT: TakeScreenshotKey: TakeScreenshot MuteAudioKey: ToggleMute LogicKeyListener@WORLD_KEYHANDLER: - Logic: CycleBasesHotkeyLogic, CycleProductionActorsHotkeyLogic, CycleHarvestersHotkeyLogic, JumpToLastEventHotkeyLogic, JumpToSelectedActorsHotkeyLogic, ResetZoomHotkeyLogic, TogglePlayerStanceColorHotkeyLogic, CycleStatusBarsHotkeyLogic, PauseHotkeyLogic, RemoveFromControlGroupHotkeyLogic + Logic: CycleBasesHotkeyLogic, CycleProductionActorsHotkeyLogic, CycleHarvestersHotkeyLogic, JumpToLastEventHotkeyLogic, JumpToSelectedActorsHotkeyLogic, ResetZoomHotkeyLogic, TogglePlayerStanceColorHotkeyLogic, CycleStatusBarsHotkeyLogic, PauseHotkeyLogic, RemoveFromControlGroupHotkeyLogic, SelectUnitsByTypeHotkeyLogic, SelectAllUnitsHotkeyLogic RemoveFromControlGroupKey: RemoveFromControlGroup CycleBasesKey: CycleBase CycleProductionActorsKey: CycleProductionBuildings @@ -21,6 +21,8 @@ Container@INGAME_ROOT: TogglePlayerStanceColorKey: TogglePlayerStanceColor CycleStatusBarsKey: CycleStatusBars PauseKey: Pause + SelectAllUnitsKey: SelectAllUnits + SelectUnitsByTypeKey: SelectUnitsByType Container@WORLD_ROOT: Children: LogicTicker@DISCONNECT_WATCHER: @@ -39,8 +41,6 @@ Container@INGAME_ROOT: WorldInteractionController@INTERACTION_CONTROLLER: Width: WINDOW_RIGHT Height: WINDOW_BOTTOM - SelectAllKey: SelectAllUnits - SelectSameTypeKey: SelectUnitsByType Container@PLAYER_ROOT: Container@MENU_ROOT: TooltipContainer@TOOLTIP_CONTAINER: diff --git a/mods/common/chrome/ingame.yaml b/mods/common/chrome/ingame.yaml index 037edb5c9b..e90afa85c1 100644 --- a/mods/common/chrome/ingame.yaml +++ b/mods/common/chrome/ingame.yaml @@ -10,7 +10,7 @@ Container@INGAME_ROOT: TakeScreenshotKey: TakeScreenshot MuteAudioKey: ToggleMute LogicKeyListener@WORLD_KEYHANDLER: - Logic: CycleBasesHotkeyLogic, CycleProductionActorsHotkeyLogic, CycleHarvestersHotkeyLogic, JumpToLastEventHotkeyLogic, JumpToSelectedActorsHotkeyLogic, ResetZoomHotkeyLogic, TogglePlayerStanceColorHotkeyLogic, CycleStatusBarsHotkeyLogic, PauseHotkeyLogic, RemoveFromControlGroupHotkeyLogic + Logic: CycleBasesHotkeyLogic, CycleProductionActorsHotkeyLogic, CycleHarvestersHotkeyLogic, JumpToLastEventHotkeyLogic, JumpToSelectedActorsHotkeyLogic, ResetZoomHotkeyLogic, TogglePlayerStanceColorHotkeyLogic, CycleStatusBarsHotkeyLogic, PauseHotkeyLogic, RemoveFromControlGroupHotkeyLogic, SelectUnitsByTypeHotkeyLogic, SelectAllUnitsHotkeyLogic RemoveFromControlGroupKey: RemoveFromControlGroup CycleBasesKey: CycleBase CycleProductionActorsKey: CycleProductionBuildings @@ -21,6 +21,8 @@ Container@INGAME_ROOT: TogglePlayerStanceColorKey: TogglePlayerStanceColor CycleStatusBarsKey: CycleStatusBars PauseKey: Pause + SelectAllUnitsKey: SelectAllUnits + SelectUnitsByTypeKey: SelectUnitsByType Container@WORLD_ROOT: Logic: LoadIngamePerfLogic Children: @@ -37,8 +39,6 @@ Container@INGAME_ROOT: WorldInteractionController@INTERACTION_CONTROLLER: Width: WINDOW_RIGHT Height: WINDOW_BOTTOM - SelectAllKey: SelectAllUnits - SelectSameTypeKey: SelectUnitsByType ViewportController: Width: WINDOW_RIGHT Height: WINDOW_BOTTOM