From a081321790c005d88dd72bbe190f1031bedf5e4c Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 4 Nov 2015 19:45:20 +0000 Subject: [PATCH] Add copy/paste to the map editor. --- .../EditorBrushes/EditorCopyPasteBrush.cs | 174 ++++++++++++++++++ OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 2 + .../Traits/World/EditorActorPreview.cs | 5 + .../Traits/World/EditorSelectionLayer.cs | 105 +++++++++++ .../Widgets/Logic/Editor/MapEditorLogic.cs | 9 + mods/cnc/chrome/editor.yaml | 12 +- mods/cnc/rules/world.yaml | 1 + mods/cnc/sequences/misc.yaml | 6 + mods/d2k/rules/world.yaml | 1 + mods/d2k/sequences/misc.yaml | 20 +- mods/ra/chrome/editor.yaml | 10 +- mods/ra/rules/world.yaml | 1 + mods/ra/sequences/misc.yaml | 6 + mods/ts/rules/world.yaml | 2 + mods/ts/sequences/misc.yaml | 7 + 15 files changed, 348 insertions(+), 13 deletions(-) create mode 100644 OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs create mode 100644 OpenRA.Mods.Common/Traits/World/EditorSelectionLayer.cs diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs b/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs new file mode 100644 index 0000000000..12ce5d98ad --- /dev/null +++ b/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs @@ -0,0 +1,174 @@ +#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.Drawing; +using System.Linq; +using OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Orders; +using OpenRA.Primitives; +using OpenRA.Traits; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets +{ + public sealed class EditorCopyPasteBrush : IEditorBrush + { + enum State { SelectFirst, SelectSecond, Paste } + + readonly WorldRenderer worldRenderer; + readonly EditorViewportControllerWidget editorWidget; + readonly EditorSelectionLayer selectionLayer; + readonly EditorActorLayer editorLayer; + + State state; + CPos start; + CPos end; + + public EditorCopyPasteBrush(EditorViewportControllerWidget editorWidget, WorldRenderer wr) + { + this.editorWidget = editorWidget; + worldRenderer = wr; + + selectionLayer = wr.World.WorldActor.Trait(); + editorLayer = wr.World.WorldActor.Trait(); + } + + public bool HandleMouseInput(MouseInput mi) + { + // Exclusively uses left and right mouse buttons, but nothing else + if (mi.Button != MouseButton.Left && mi.Button != MouseButton.Right) + return false; + + if (mi.Button == MouseButton.Right) + { + if (mi.Event == MouseInputEvent.Up) + { + editorWidget.ClearBrush(); + return true; + } + + return false; + } + + if (mi.Button == MouseButton.Left && (mi.Event == MouseInputEvent.Up || mi.Event == MouseInputEvent.Down)) + { + var cell = worldRenderer.Viewport.ViewToWorld(mi.Location); + switch (state) + { + case State.SelectFirst: + if (mi.Event != MouseInputEvent.Down) + break; + start = cell; + selectionLayer.SetCopyRegion(start, end); + state = State.SelectSecond; + break; + case State.SelectSecond: + if (mi.Event != MouseInputEvent.Up) + break; + end = cell; + selectionLayer.SetCopyRegion(start, end); + state = State.Paste; + break; + case State.Paste: + { + var gridType = worldRenderer.World.Map.Grid.Type; + var source = CellRegion.BoundingRegion(gridType, new[] { start, end }); + Copy(source, cell - end); + editorWidget.ClearBrush(); + break; + } + } + + return true; + } + + return false; + } + + void Copy(CellRegion source, CVec offset) + { + var gridType = worldRenderer.World.Map.Grid.Type; + var mapTiles = worldRenderer.World.Map.MapTiles.Value; + var mapHeight = worldRenderer.World.Map.MapHeight.Value; + var mapResources = worldRenderer.World.Map.MapResources.Value; + + var dest = new CellRegion(gridType, source.TopLeft + offset, source.BottomRight + offset); + + var previews = new Dictionary(); + var tiles = new Dictionary>(); + + foreach (var cell in source) + { + if (!mapTiles.Contains(cell) || !mapTiles.Contains(cell + offset)) + continue; + + tiles.Add(cell + offset, Tuple.Create(mapTiles[cell], mapResources[cell], mapHeight[cell])); + + foreach (var preview in editorLayer.PreviewsAt(cell)) + { + if (previews.ContainsKey(preview.ID)) + continue; + + var copy = preview.Export(); + if (copy.InitDict.Contains()) + { + var location = copy.InitDict.Get(); + copy.InitDict.Remove(location); + copy.InitDict.Add(new LocationInit(location.Value(worldRenderer.World) + offset)); + } + + previews.Add(preview.ID, copy); + } + } + + foreach (var kv in tiles) + { + mapTiles[kv.Key] = kv.Value.Item1; + mapResources[kv.Key] = kv.Value.Item2; + mapHeight[kv.Key] = kv.Value.Item3; + } + + var removeActors = dest.SelectMany(editorLayer.PreviewsAt).Distinct().ToList(); + foreach (var preview in removeActors) + editorLayer.Remove(preview); + + foreach (var kv in previews) + editorLayer.Add(kv.Value); + } + + public void Tick() + { + var cell = worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos); + if (state == State.Paste) + { + selectionLayer.SetPasteRegion(cell + (start - end), cell); + return; + } + + if (state == State.SelectFirst) + start = end = cell; + else if (state == State.SelectSecond) + end = cell; + + selectionLayer.SetCopyRegion(start, end); + } + + public void Dispose() + { + selectionLayer.Clear(); + } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index ecc12a91e3..25653a76b8 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -708,6 +708,8 @@ + + diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs b/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs index 9c75a1b77d..1d0a5d5722 100644 --- a/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs +++ b/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs @@ -161,5 +161,10 @@ namespace OpenRA.Mods.Common.Traits .SelectMany(rpi => rpi.RenderPreview(init)) .ToArray(); } + + public ActorReference Export() + { + return new ActorReference(actor.Type, actor.Save().ToDictionary()); + } } } diff --git a/OpenRA.Mods.Common/Traits/World/EditorSelectionLayer.cs b/OpenRA.Mods.Common/Traits/World/EditorSelectionLayer.cs new file mode 100644 index 0000000000..74eb68d455 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/EditorSelectionLayer.cs @@ -0,0 +1,105 @@ +#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.Drawing; +using System.IO; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Required for the map editor to work. Attach this to the world actor.")] + public class EditorSelectionLayerInfo : ITraitInfo + { + [PaletteReference] + [Desc("Palette to use for rendering the placement sprite.")] + public readonly string Palette = "terrain"; + + [Desc("Sequence image where the selection overlay types are defined.")] + public readonly string Image = "editor-overlay"; + + [SequenceReference("Image")] + [Desc("Sequence to use for the copy overlay.")] + public readonly string CopySequence = "copy"; + + [SequenceReference("Image")] + [Desc("Sequence to use for the paste overlay.")] + public readonly string PasteSequence = "paste"; + + public virtual object Create(ActorInitializer init) { return new EditorSelectionLayer(init.Self, this); } + } + + public class EditorSelectionLayer : IWorldLoaded, IPostRender + { + readonly EditorSelectionLayerInfo info; + readonly Map map; + readonly Sprite copySprite; + readonly Sprite pasteSprite; + PaletteReference palette; + + public CellRegion CopyRegion { get; private set; } + public CellRegion PasteRegion { get; private set; } + + public EditorSelectionLayer(Actor self, EditorSelectionLayerInfo info) + { + if (self.World.Type != WorldType.Editor) + return; + + this.info = info; + map = self.World.Map; + copySprite = map.SequenceProvider.GetSequence(info.Image, info.CopySequence).GetSprite(0); + pasteSprite = map.SequenceProvider.GetSequence(info.Image, info.PasteSequence).GetSprite(0); + } + + public void WorldLoaded(World w, WorldRenderer wr) + { + if (w.Type != WorldType.Editor) + return; + + palette = wr.Palette(info.Palette); + } + + public void SetCopyRegion(CPos start, CPos end) + { + CopyRegion = CellRegion.BoundingRegion(map.Grid.Type, new[] { start, end }); + } + + public void SetPasteRegion(CPos start, CPos end) + { + PasteRegion = CellRegion.BoundingRegion(map.Grid.Type, new[] { start, end }); + } + + public void Clear() + { + CopyRegion = PasteRegion = null; + } + + public void RenderAfterWorld(WorldRenderer wr, Actor self) + { + if (wr.World.Type != WorldType.Editor) + return; + + if (CopyRegion != null) + foreach (var c in CopyRegion) + new SpriteRenderable(copySprite, wr.World.Map.CenterOfCell(c), + WVec.Zero, -511, palette, 1f, true).PrepareRender(wr).Render(wr); + + if (PasteRegion != null) + foreach (var c in PasteRegion) + new SpriteRenderable(pasteSprite, wr.World.Map.CenterOfCell(c), + WVec.Zero, -511, palette, 1f, true).PrepareRender(wr).Render(wr); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorLogic.cs index b6ef4a6048..30c5c3c888 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorLogic.cs @@ -21,6 +21,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic [ObjectCreator.UseCtor] public MapEditorLogic(Widget widget, World world, WorldRenderer worldRenderer) { + var editorViewport = widget.Get("MAP_EDITOR"); + var gridButton = widget.GetOrNull("GRID_BUTTON"); var terrainGeometryTrait = world.WorldActor.Trait(); @@ -63,6 +65,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic }; } + var copypasteButton = widget.GetOrNull("COPYPASTE_BUTTON"); + if (copypasteButton != null) + { + copypasteButton.OnClick = () => editorViewport.SetBrush(new EditorCopyPasteBrush(editorViewport, worldRenderer)); + copypasteButton.IsHighlighted = () => editorViewport.CurrentBrush is EditorCopyPasteBrush; + } + var coordinateLabel = widget.GetOrNull("COORDINATE_LABEL"); if (coordinateLabel != null) coordinateLabel.GetText = () => worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos).ToString(); diff --git a/mods/cnc/chrome/editor.yaml b/mods/cnc/chrome/editor.yaml index f1ea147ec7..cde46073b4 100644 --- a/mods/cnc/chrome/editor.yaml +++ b/mods/cnc/chrome/editor.yaml @@ -362,7 +362,7 @@ Container@EDITOR_WORLD_ROOT: Text: Actors Font: Bold Button@GRID_BUTTON: - X: WINDOW_RIGHT - 420 + X: WINDOW_RIGHT - 500 Y: 5 Width: 100 Height: 25 @@ -373,7 +373,7 @@ Container@EDITOR_WORLD_ROOT: TooltipText: Toggle the terrain grid TooltipContainer: TOOLTIP_CONTAINER Label@ZOOM_LABEL: - X: WINDOW_RIGHT - 500 - 55 + X: WINDOW_RIGHT - 580 - 55 Y: 5 Width: 50 Height: 25 @@ -382,11 +382,17 @@ Container@EDITOR_WORLD_ROOT: Font: Bold Contrast: true DropDownButton@ZOOM_BUTTON: - X: WINDOW_RIGHT - 500 + X: WINDOW_RIGHT - 580 Y: 5 Width: 70 Height: 25 Font: Bold + Button@COPYPASTE_BUTTON: + X: WINDOW_RIGHT-390 + Y: 5 + Width: 96 + Height: 25 + Text: Copy/Paste Label@COORDINATE_LABEL: X: 10 Width: 50 diff --git a/mods/cnc/rules/world.yaml b/mods/cnc/rules/world.yaml index 4051884d12..b5e423de1c 100644 --- a/mods/cnc/rules/world.yaml +++ b/mods/cnc/rules/world.yaml @@ -154,4 +154,5 @@ EditorWorld: Inherits: ^BaseWorld EditorActorLayer: EditorResourceLayer: + EditorSelectionLayer: diff --git a/mods/cnc/sequences/misc.yaml b/mods/cnc/sequences/misc.yaml index 251fa4e16b..7428e20ab5 100644 --- a/mods/cnc/sequences/misc.yaml +++ b/mods/cnc/sequences/misc.yaml @@ -240,6 +240,12 @@ overlay: target-invalid: Start: 1 +editor-overlay: + copy: overlay + Start: 0 + paste: overlay + Start: 1 + poweroff: offline: Length: * diff --git a/mods/d2k/rules/world.yaml b/mods/d2k/rules/world.yaml index a3eacff199..28ed10147f 100644 --- a/mods/d2k/rules/world.yaml +++ b/mods/d2k/rules/world.yaml @@ -148,4 +148,5 @@ EditorWorld: Inherits: ^BaseWorld EditorActorLayer: D2kEditorResourceLayer: + EditorSelectionLayer: diff --git a/mods/d2k/sequences/misc.yaml b/mods/d2k/sequences/misc.yaml index 756bf5dba3..f07d31c416 100644 --- a/mods/d2k/sequences/misc.yaml +++ b/mods/d2k/sequences/misc.yaml @@ -126,19 +126,23 @@ rank: Length: * overlay: - build-valid-arrakis: DATA.R8 + Defaults: DATA.R8 Offset: -16,-16 - build-invalid: DATA.R8 + build-valid-arrakis: + build-invalid: Start: 1 - Offset: -16,-16 - target-select: DATA.R8 + target-select: Start: 2 - Offset: -16,-16 - target-valid-arrakis: DATA.R8 - Offset: -16,-16 - target-invalid: DATA.R8 + target-valid-arrakis: + target-invalid: Start: 1 + +editor-overlay: + Defaults: DATA.R8 Offset: -16,-16 + copy: + paste: + Start: 1 rallypoint: flag: flagfly.shp diff --git a/mods/ra/chrome/editor.yaml b/mods/ra/chrome/editor.yaml index d3a06af92f..864561c3b8 100644 --- a/mods/ra/chrome/editor.yaml +++ b/mods/ra/chrome/editor.yaml @@ -351,9 +351,15 @@ Container@EDITOR_WORLD_ROOT: TooltipContainer: TOOLTIP_CONTAINER Font: Bold Key: escape + Button@COPYPASTE_BUTTON: + X: 170 + Width: 90 + Height: 25 + Text: Copy/Paste + Font: Bold Button@GRID_BUTTON: - X: 180 - Width: 160 + X: 270 + Width: 70 Height: 25 Text: Grid TooltipTemplate: BUTTON_TOOLTIP diff --git a/mods/ra/rules/world.yaml b/mods/ra/rules/world.yaml index 58c6e5e76e..9070b44477 100644 --- a/mods/ra/rules/world.yaml +++ b/mods/ra/rules/world.yaml @@ -173,4 +173,5 @@ EditorWorld: Inherits: ^BaseWorld EditorActorLayer: EditorResourceLayer: + EditorSelectionLayer: diff --git a/mods/ra/sequences/misc.yaml b/mods/ra/sequences/misc.yaml index daeca8dd39..64e3d2ac88 100644 --- a/mods/ra/sequences/misc.yaml +++ b/mods/ra/sequences/misc.yaml @@ -474,6 +474,12 @@ overlay: target-invalid: Start: 1 +editor-overlay: + copy: overlay + Start: 0 + paste: overlay + Start: 1 + resources: Defaults: Length: * diff --git a/mods/ts/rules/world.yaml b/mods/ts/rules/world.yaml index a1d7e7b726..07810d10ae 100644 --- a/mods/ts/rules/world.yaml +++ b/mods/ts/rules/world.yaml @@ -175,4 +175,6 @@ EditorWorld: Inherits: ^BaseWorld EditorActorLayer: EditorResourceLayer: + EditorSelectionLayer: + Palette: placebuilding diff --git a/mods/ts/sequences/misc.yaml b/mods/ts/sequences/misc.yaml index b9877ce5c7..f5028579f1 100644 --- a/mods/ts/sequences/misc.yaml +++ b/mods/ts/sequences/misc.yaml @@ -7,6 +7,13 @@ overlay: Start: 1 target-select: +editor-overlay: + Defaults: place + Offset: 0, -12 + copy: + paste: + Start: 1 + poweroff: offline: Length: *