Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
#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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorActorBrush : IEditorBrush
{
public EditorActorPreview Preview;
readonly World world;
readonly EditorActorLayer editorLayer;
readonly EditorActionManager editorActionManager;
readonly EditorViewportControllerWidget editorWidget;
readonly WVec centerOffset;
readonly bool sharesCell;
CPos cell;
SubCell subcell = SubCell.Invalid;
public EditorActorBrush(EditorViewportControllerWidget editorWidget, ActorInfo actor, PlayerReference owner, WorldRenderer wr)
{
this.editorWidget = editorWidget;
world = wr.World;
editorLayer = world.WorldActor.Trait<EditorActorLayer>();
editorActionManager = world.WorldActor.Trait<EditorActionManager>();
var ios = actor.TraitInfoOrDefault<IOccupySpaceInfo>();
centerOffset = (ios as BuildingInfo)?.CenterOffset(world) ?? WVec.Zero;
sharesCell = ios != null && ios.SharesCell;
// Enforce first entry of ValidOwnerNames as owner if the actor has RequiresSpecificOwners.
var ownerName = owner.Name;
var specificOwnerInfo = actor.TraitInfoOrDefault<RequiresSpecificOwnersInfo>();
if (specificOwnerInfo != null && !specificOwnerInfo.ValidOwnerNames.Contains(ownerName))
ownerName = specificOwnerInfo.ValidOwnerNames.First();
var reference = new ActorReference(actor.Name)
{
new OwnerInit(ownerName),
new FactionInit(owner.Faction)
};
var worldPx = wr.Viewport.ViewToWorldPx(Viewport.LastMousePos) - wr.ScreenPxOffset(centerOffset);
cell = wr.Viewport.ViewToWorld(wr.Viewport.WorldToViewPx(worldPx));
reference.Add(new LocationInit(cell));
if (sharesCell)
{
subcell = editorLayer.FreeSubCellAt(cell);
if (subcell != SubCell.Invalid)
reference.Add(new SubCellInit(subcell));
}
if (actor.HasTraitInfo<IFacingInfo>())
reference.Add(new FacingInit(editorLayer.Info.DefaultActorFacing));
Preview = new EditorActorPreview(wr, null, reference, owner);
}
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.Down)
{
// Check the actor is inside the map
if (!Preview.Footprint.All(c => world.Map.Tiles.Contains(c.Key)))
return true;
var action = new AddActorAction(editorLayer, Preview.Export());
editorActionManager.Add(action);
}
return true;
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self)
{
// Offset mouse position by the center offset (in world pixels)
var worldPx = wr.Viewport.ViewToWorldPx(Viewport.LastMousePos) - wr.ScreenPxOffset(centerOffset);
var currentCell = wr.Viewport.ViewToWorld(wr.Viewport.WorldToViewPx(worldPx));
var currentSubcell = sharesCell ? editorLayer.FreeSubCellAt(currentCell) : SubCell.Invalid;
if (cell != currentCell || subcell != currentSubcell)
{
cell = currentCell;
Preview.ReplaceInit(new LocationInit(cell));
if (sharesCell)
{
subcell = editorLayer.FreeSubCellAt(cell);
if (subcell == SubCell.Invalid)
Preview.RemoveInit<SubCellInit>();
else
Preview.ReplaceInit(new SubCellInit(subcell));
}
Preview.UpdateFromMove();
}
}
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr)
{
return Preview.Render().OrderBy(WorldRenderer.RenderableZPositionComparisonKey);
}
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr)
{
return Preview.RenderAnnotations();
}
public void Tick() { }
public void Dispose() { }
}
sealed class AddActorAction : IEditorAction
{
public string Text { get; private set; }
[FluentReference("name", "id")]
const string AddedActor = "notification-added-actor";
readonly EditorActorLayer editorLayer;
readonly ActorReference actor;
EditorActorPreview editorActorPreview;
public AddActorAction(EditorActorLayer editorLayer, ActorReference actor)
{
this.editorLayer = editorLayer;
// Take an immutable copy of the reference.
this.actor = actor.Clone();
}
public void Execute()
{
Do();
}
public void Do()
{
editorActorPreview = editorLayer.Add(actor);
Text = FluentProvider.GetMessage(AddedActor,
"name", editorActorPreview.Info.Name,
"id", editorActorPreview.ID);
}
public void Undo()
{
editorLayer.Remove(editorActorPreview);
}
}
}

View File

@@ -0,0 +1,363 @@
#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 System.Runtime.InteropServices;
using OpenRA.Graphics;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Traits;
using OpenRA.Support;
namespace OpenRA.Mods.Common.EditorBrushes
{
public readonly record struct BlitTile(TerrainTile TerrainTile, ResourceTile ResourceTile, ResourceLayerContents? ResourceLayerContents, byte Height);
public readonly record struct EditorBlitSource(CellCoordsRegion CellCoords, Dictionary<string, EditorActorPreview> Actors, Dictionary<CPos, BlitTile> Tiles);
[Flags]
public enum MapBlitFilters
{
None = 0,
Terrain = 1,
Resources = 2,
Actors = 4,
All = Terrain | Resources | Actors
}
/// <summary>
/// Core implementation for EditorActions which overwrite a region of the map (such as
/// copy-paste).
/// </summary>
public sealed class EditorBlit
{
readonly MapBlitFilters blitFilters;
readonly IResourceLayer resourceLayer;
readonly EditorActorLayer editorActorLayer;
readonly EditorBlitSource commitBlitSource;
readonly EditorBlitSource revertBlitSource;
readonly CPos blitPosition;
readonly Map map;
readonly bool respectBounds;
public EditorBlit(
MapBlitFilters blitFilters,
IResourceLayer resourceLayer,
CPos blitPosition,
Map map,
EditorBlitSource blitSource,
EditorActorLayer editorActorLayer,
bool respectBounds)
{
this.blitFilters = blitFilters;
this.resourceLayer = resourceLayer;
this.blitPosition = blitPosition;
this.editorActorLayer = editorActorLayer;
this.map = map;
this.respectBounds = respectBounds;
var blitSize = blitSource.CellCoords.BottomRight - blitSource.CellCoords.TopLeft;
// Only include into the revert blit stuff which would be modified by the main blit.
var mask = GetBlitSourceMask(
blitSource, blitPosition - blitSource.CellCoords.TopLeft);
commitBlitSource = blitSource;
revertBlitSource = CopyRegionContents(
map,
editorActorLayer,
resourceLayer,
new CellCoordsRegion(blitPosition, blitPosition + blitSize),
blitFilters,
mask);
}
/// <summary>
/// Returns an EditorBlitSource containing the map contents for a given region.
/// If a mask is supplied, only tiles and actors (fully or partially) overlapping the mask
/// are included in the EditorBlitSource.
/// </summary>
public static EditorBlitSource CopyRegionContents(
Map map,
EditorActorLayer editorActorLayer,
IResourceLayer resourceLayer,
CellCoordsRegion region,
MapBlitFilters blitFilters,
IReadOnlySet<CPos> mask = null)
{
var mapTiles = map.Tiles;
var mapHeight = map.Height;
var mapResources = map.Resources;
var previews = new Dictionary<string, EditorActorPreview>();
var tiles = new Dictionary<CPos, BlitTile>();
if (blitFilters.HasFlag(MapBlitFilters.Terrain) || blitFilters.HasFlag(MapBlitFilters.Resources))
{
foreach (var cell in region)
{
if (!mapTiles.Contains(cell) || (mask != null && !mask.Contains(cell)))
continue;
tiles.Add(
cell,
new BlitTile(mapTiles[cell],
mapResources[cell],
resourceLayer?.GetResource(cell),
mapHeight[cell]));
}
}
if (blitFilters.HasFlag(MapBlitFilters.Actors))
foreach (var preview in editorActorLayer.PreviewsInCellRegion(region))
if (mask == null || preview.Footprint.Keys.Any(mask.Contains))
previews.TryAdd(preview.ID, preview);
return new EditorBlitSource(region, previews, tiles);
}
void Blit(bool isRevert)
{
var source = isRevert ? revertBlitSource : commitBlitSource;
var blitPos = isRevert ? source.CellCoords.TopLeft : blitPosition;
var blitVec = blitPos - source.CellCoords.TopLeft;
var blitSize = source.CellCoords.BottomRight - source.CellCoords.TopLeft;
var blitRegion = new CellCoordsRegion(blitPos, blitPos + blitSize);
if (blitFilters.HasFlag(MapBlitFilters.Actors))
{
// Clear any existing actors in the paste cells.
//
// revertBlitSource's mask may be a superset of the commitBlitSource's mask if
// - Its a sparse blit; and
// - The revert actors removed by the commit are partially outside of the commit mask.
// Otherwise, it's a (practically) equal set. (Subject to map bounds.)
//
// This implies that:
// - commitBlitSource's mask will overlap all commit actors.
// - revertBlitSource's mask will overlap all revert actors.
// - commitBlitSource's mask will overlap all and no more than the revert actors.
// - revertBlitSource's mask will overlap all revert actors BUT MAY OVERLAP MORE!
//
// This means we use the commit mask, not the revert one.
var commitBlitVec = blitPosition - commitBlitSource.CellCoords.TopLeft;
var mask = GetBlitSourceMask(commitBlitSource, commitBlitVec);
using (new PerfTimer("RemoveActors", 1))
editorActorLayer.RemoveRegion(blitRegion, mask);
}
foreach (var tileKeyValuePair in source.Tiles)
{
var position = tileKeyValuePair.Key + blitVec;
if (!map.Tiles.Contains(position) || (respectBounds && !map.Contains(position)))
continue;
// Clear any existing resources.
if (resourceLayer != null && blitFilters.HasFlag(MapBlitFilters.Resources))
resourceLayer.ClearResources(position);
var tile = tileKeyValuePair.Value;
var resourceLayerContents = tile.ResourceLayerContents;
if (blitFilters.HasFlag(MapBlitFilters.Terrain))
{
map.Tiles[position] = tile.TerrainTile;
map.Height[position] = tile.Height;
}
if (blitFilters.HasFlag(MapBlitFilters.Resources) &&
resourceLayerContents.HasValue &&
!string.IsNullOrWhiteSpace(resourceLayerContents.Value.Type) &&
resourceLayer.CanAddResource(resourceLayerContents.Value.Type, position))
{
resourceLayer.AddResource(resourceLayerContents.Value.Type, position, resourceLayerContents.Value.Density);
}
}
if (blitFilters.HasFlag(MapBlitFilters.Actors))
{
if (isRevert)
{
// For reverts, just place the original actors back exactly how they were.
using (new PerfTimer("AddActors", 1))
editorActorLayer.AddRange(source.Actors.Values.ToArray().AsSpan());
}
else
{
// Create copies of the original actors, update their locations, and place.
var copies = new List<ActorReference>(source.Actors.Count);
foreach (var actorKeyValuePair in source.Actors)
{
var copy = actorKeyValuePair.Value.Export();
var locationInit = copy.GetOrDefault<LocationInit>();
if (locationInit != null)
{
var actorPosition = locationInit.Value + blitVec;
if (respectBounds && !map.Contains(actorPosition))
continue;
copy.RemoveAll<LocationInit>();
copy.Add(new LocationInit(actorPosition));
}
copies.Add(copy);
}
using (new PerfTimer("AddActors", 1))
editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies));
}
}
}
public static IEnumerable<IRenderable> PreviewBlitSource(
EditorBlitSource blitSource,
MapBlitFilters filters,
CVec offset,
WorldRenderer wr,
bool stickToGround)
{
var world = wr.World;
var map = world.Map;
var mapHeight = map.Height;
var mapGrid = map.Grid;
if (filters.HasFlag(MapBlitFilters.Terrain))
{
var terrainRenderer = world.WorldActor.Trait<ITiledTerrainRenderer>();
foreach (var (pos, tile) in blitSource.Tiles)
{
var cPos = pos + offset;
var height = stickToGround ? (mapHeight.TryGetValue(cPos, out var isoHeight) ? isoHeight : byte.MinValue) : tile.Height;
var wPos = CellLayerUtils.CPosToWPos(cPos, height, mapGrid.Type);
var preview = terrainRenderer.RenderPreview(wr, tile.TerrainTile, wPos);
foreach (var renderable in preview)
yield return renderable;
}
}
if (filters.HasFlag(MapBlitFilters.Resources))
{
var resourceRenderers = world.WorldActor.TraitsImplementing<IResourceRenderer>().ToArray();
var resourceLayer = world.WorldActor.Trait<IResourceLayer>();
foreach (var (pos, tile) in blitSource.Tiles)
{
if (tile.ResourceLayerContents == null || tile.ResourceLayerContents.Value.Type == null)
continue;
var cPos = pos + offset;
if (!filters.HasFlag(MapBlitFilters.Terrain) && !resourceLayer.CanAddResource(tile.ResourceLayerContents.Value.Type, cPos))
continue;
byte height;
if (filters.HasFlag(MapBlitFilters.Terrain) && !stickToGround)
{
// We won't change relative tile height, use the saved value.
height = tile.Height;
}
else
{
if (!mapHeight.TryGetValue(cPos, out height))
height = byte.MinValue;
// If a tile has inherent height, we know it will raise terrain.
if (filters.HasFlag(MapBlitFilters.Terrain) && stickToGround)
height += map.Rules.TerrainInfo.GetTerrainInfo(tile.TerrainTile).Height;
}
var wPos = CellLayerUtils.CPosToWPos(cPos, height, mapGrid.Type);
var preview = resourceRenderers
.SelectMany(r => r.RenderPreview(wr, tile.ResourceLayerContents.Value.Type, wPos));
foreach (var renderable in preview)
yield return renderable;
}
}
if (filters.HasFlag(MapBlitFilters.Actors))
{
foreach (var (_, editorActorPreview) in blitSource.Actors)
{
var useGround = stickToGround;
if (!filters.HasFlag(MapBlitFilters.Terrain) || !blitSource.Tiles.ContainsKey(editorActorPreview.Location))
useGround = true;
var wOffset = CellLayerUtils.CVecToWVec(offset, 0, mapGrid.Type);
if (useGround)
{
var actorPos = editorActorPreview.CenterPosition + wOffset;
wOffset -= new WVec(0, 0, map.DistanceAboveTerrain(actorPos).Length);
}
var preview = editorActorPreview.RenderWithOffset(wOffset)
.OrderBy(WorldRenderer.RenderableZPositionComparisonKey);
foreach (var renderable in preview)
yield return renderable;
}
}
}
/// <summary>
/// Find the set of cells within an EditorBlitSource that are actually occupied by a
/// BlitTile or actor. Note that all tiles must be inside the CellRegion, and actors must
/// be at least partially inside the CellRegion. If an actor partially lies outside of the
/// CellRegion, only cells within the CellRegion are included in the output set.
/// </summary>
static HashSet<CPos> GetBlitSourceMask(
EditorBlitSource blitSource,
CVec offset)
{
var mask = new HashSet<CPos>();
var sourceCellCoords = blitSource.CellCoords;
foreach (var (cpos, _) in blitSource.Tiles)
{
if (!sourceCellCoords.Contains(cpos))
throw new ArgumentException("EditorBlitSource contains a BlitTile outside of its CellRegion");
mask.Add(cpos + offset);
}
foreach (var (_, editorActorPreview) in blitSource.Actors)
{
var anyContained = false;
foreach (var cpos in editorActorPreview.Footprint.Keys)
{
if (sourceCellCoords.Contains(cpos))
{
mask.Add(cpos + offset);
anyContained = true;
}
}
if (!anyContained)
throw new ArgumentException("EditorBlitSource contains an actor entirely outside of its CellRegion");
}
return mask;
}
public void Commit() => Blit(false);
public void Revert() => Blit(true);
public int TileCount()
{
return commitBlitSource.Tiles.Count;
}
public int ActorCount()
{
return commitBlitSource.Actors.Count;
}
}
}

View File

@@ -0,0 +1,174 @@
#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 OpenRA.Graphics;
using OpenRA.Mods.Common.EditorBrushes;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorCopyPasteBrush : IEditorBrush
{
readonly WorldRenderer worldRenderer;
readonly EditorViewportControllerWidget editorWidget;
readonly EditorActorLayer editorActorLayer;
readonly EditorActionManager editorActionManager;
readonly EditorBlitSource clipboard;
readonly IResourceLayer resourceLayer;
readonly Func<MapBlitFilters> getCopyFilters;
public CPos PastePreviewPosition { get; private set; }
public CellCoordsRegion Region => clipboard.CellCoords;
public EditorCopyPasteBrush(
EditorViewportControllerWidget editorWidget,
WorldRenderer wr,
EditorBlitSource clipboard,
IResourceLayer resourceLayer,
Func<MapBlitFilters> getCopyFilters)
{
this.getCopyFilters = getCopyFilters;
this.editorWidget = editorWidget;
this.clipboard = clipboard;
this.resourceLayer = resourceLayer;
worldRenderer = wr;
editorActionManager = wr.World.WorldActor.Trait<EditorActionManager>();
editorActorLayer = wr.World.WorldActor.Trait<EditorActorLayer>();
PastePreviewPosition = worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos);
}
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.Down)
{
var pastePosition = worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos);
var editorBlit = new EditorBlit(
getCopyFilters(),
resourceLayer,
pastePosition,
worldRenderer.World.Map,
clipboard,
editorActorLayer,
true);
var action = new CopyPasteEditorAction(editorBlit);
editorActionManager.Add(action);
return true;
}
return false;
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self) { }
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr)
{
var filters = getCopyFilters();
var stickToGround = !filters.HasFlag(MapBlitFilters.Terrain)
;
var preview = EditorBlit.PreviewBlitSource(
clipboard,
filters,
PastePreviewPosition - Region.TopLeft,
wr,
stickToGround);
foreach (var renderable in preview)
yield return renderable;
}
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr)
{
yield return new EditorSelectionAnnotationRenderable(
Region,
editorWidget.SelectionAltColor,
editorWidget.SelectionAltOffset,
PastePreviewPosition - Region.TopLeft);
yield return new EditorSelectionAnnotationRenderable(
Region,
editorWidget.PasteColor,
int2.Zero,
PastePreviewPosition - Region.TopLeft);
}
public void Tick()
{
PastePreviewPosition = worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos);
}
public void Dispose() { }
}
sealed class CopyPasteEditorAction : IEditorAction
{
[FluentReference("tiles")]
const string CopiedTiles = "notification-copied-tiles";
[FluentReference("actors")]
const string CopiedActors = "notification-copied-actors";
[FluentReference("tiles", "actors")]
const string CopiedTilesAndActors = "notification-copied-tiles-actors";
public string Text { get; }
readonly EditorBlit editorBlit;
public CopyPasteEditorAction(EditorBlit editorBlit)
{
this.editorBlit = editorBlit;
var actors = editorBlit.ActorCount();
var tiles = editorBlit.TileCount();
if (tiles > 0 && actors == 0)
Text = FluentProvider.GetMessage(CopiedTiles, "tiles", tiles);
else if (tiles == 0 && actors > 0)
Text = FluentProvider.GetMessage(CopiedActors, "actors", actors);
else
Text = FluentProvider.GetMessage(CopiedTilesAndActors, "tiles", tiles, "actors", actors);
}
public void Execute()
{
Do();
}
public void Do()
{
editorBlit.Commit();
}
public void Undo()
{
editorBlit.Revert();
}
}
}

View File

@@ -0,0 +1,627 @@
#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.Runtime.InteropServices;
using OpenRA.Graphics;
using OpenRA.Mods.Common.EditorBrushes;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Support;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets
{
public interface IEditorBrush : IDisposable
{
bool HandleMouseInput(MouseInput mi);
void Tick();
void TickRender(WorldRenderer wr, Actor self);
IEnumerable<IRenderable> RenderAboveShroud(Actor self, WorldRenderer wr);
IEnumerable<IRenderable> RenderAnnotations(Actor self, WorldRenderer wr);
}
public class EditorSelection
{
public CellCoordsRegion? Area;
public EditorActorPreview Actor;
public bool HasSelection => Area.HasValue || Actor != null;
}
public sealed class EditorDefaultBrush : IEditorBrush
{
const int MinMouseMoveBeforeDrag = 32;
public event Action SelectionChanged;
public event Action UpdateSelectedTab;
readonly WorldRenderer worldRenderer;
readonly World world;
readonly EditorViewportControllerWidget editorWidget;
readonly EditorActorLayer editorLayer;
readonly EditorActionManager editorActionManager;
readonly IResourceLayer resourceLayer;
readonly EditorActorLayer actorLayer;
public CellCoordsRegion? CurrentDragBounds => selectionBounds ?? Selection.Area;
public EditorSelection Selection { get; private set; } = new();
EditorSelection previousSelection;
CellCoordsRegion? selectionBounds;
int2? selectionStartLocation;
CPos? selectionStartCell;
int2 worldPixel;
bool draggingActor;
MoveActorAction moveAction;
int2 dragPixelOffset;
CVec dragCellOffset;
public EditorDefaultBrush(EditorViewportControllerWidget editorWidget, WorldRenderer wr)
{
this.editorWidget = editorWidget;
worldRenderer = wr;
world = wr.World;
editorLayer = world.WorldActor.Trait<EditorActorLayer>();
editorActionManager = world.WorldActor.Trait<EditorActionManager>();
resourceLayer = world.WorldActor.TraitOrDefault<IResourceLayer>();
actorLayer = world.WorldActor.Trait<EditorActorLayer>();
}
long CalculateActorSelectionPriority(EditorActorPreview actor)
{
var centerPixel = new int2(actor.Bounds.X, actor.Bounds.Y);
var pixelDistance = (centerPixel - worldPixel).Length;
// If 2+ actors have the same pixel position, then the highest appears on top.
var worldZPosition = actor.CenterPosition.Z;
// Sort by pixel distance then in world z position.
return ((long)pixelDistance << 32) + worldZPosition;
}
public void DeleteSelection(MapBlitFilters filters)
{
if (Selection.Area.HasValue)
editorActionManager.Add(new DeleteAreaAction(world.Map, filters, Selection.Area.Value, resourceLayer, actorLayer));
}
public void ClearSelection(bool updateSelectedTab = false)
{
if (Selection.HasSelection)
{
previousSelection = Selection;
SetSelection(new EditorSelection());
editorActionManager.Add(new ChangeSelectionAction(this, Selection, previousSelection));
if (updateSelectedTab)
UpdateSelectedTab?.Invoke();
}
}
public void SetSelection(EditorSelection selection)
{
if (Selection == selection)
return;
if (Selection.Actor != null)
Selection.Actor.Selected = false;
Selection = selection;
if (Selection.Actor != null)
Selection.Actor.Selected = true;
SelectionChanged?.Invoke();
}
public bool HandleMouseInput(MouseInput mi)
{
// Exclusively uses mouse wheel and both mouse buttons, but nothing else.
// Mouse move events are important for tooltips, so we always allow these through.
if (mi.Button != MouseButton.Left && mi.Button != MouseButton.Right
&& mi.Event != MouseInputEvent.Move && mi.Event != MouseInputEvent.Scroll)
return false;
worldPixel = worldRenderer.Viewport.ViewToWorldPx(mi.Location);
var cell = worldRenderer.Viewport.ViewToWorld(mi.Location);
var underCursor = editorLayer.PreviewsAtWorldPixel(worldPixel).MinByOrDefault(CalculateActorSelectionPriority);
var resourceUnderCursor = resourceLayer?.GetResource(cell).Type;
if (underCursor != null)
editorWidget.SetTooltip(underCursor.Tooltip);
else if (resourceUnderCursor != null)
editorWidget.SetTooltip(resourceUnderCursor);
else
editorWidget.SetTooltip(null);
// Actor drag.
if (mi.Button == MouseButton.Left)
{
if (mi.Event == MouseInputEvent.Down && underCursor != null && (mi.Modifiers.HasModifier(Modifiers.Shift) || underCursor == Selection.Actor))
{
var cellViewPx = worldRenderer.Viewport.WorldToViewPx(worldRenderer.ScreenPosition(world.Map.CenterOfCell(cell)));
dragPixelOffset = cellViewPx - mi.Location;
dragCellOffset = underCursor.Location - cell;
moveAction = new MoveActorAction(underCursor, actorLayer);
draggingActor = true;
return false;
}
else if (mi.Event == MouseInputEvent.Up && draggingActor)
{
editorWidget.SetTooltip(null);
draggingActor = false;
if (moveAction.HasMoved)
editorActionManager.Add(moveAction);
moveAction = null;
return false;
}
else if (mi.Event == MouseInputEvent.Move && draggingActor)
{
editorWidget.SetTooltip(null);
var to = worldRenderer.Viewport.ViewToWorld(mi.Location + dragPixelOffset) + dragCellOffset;
moveAction.Move(to);
return false;
}
}
// Selection box drag.
if (mi.Event == MouseInputEvent.Move &&
selectionStartLocation != null &&
(selectionBounds != null || (mi.Location - selectionStartLocation.Value).LengthSquared > MinMouseMoveBeforeDrag))
{
selectionStartCell ??= worldRenderer.Viewport.ViewToWorld(selectionStartLocation.Value);
var topLeft = new CPos(Math.Min(selectionStartCell.Value.X, cell.X), Math.Min(selectionStartCell.Value.Y, cell.Y));
var bottomRight = new CPos(Math.Max(selectionStartCell.Value.X, cell.X), Math.Max(selectionStartCell.Value.Y, cell.Y));
var gridType = worldRenderer.World.Map.Grid.Type;
// We've dragged enough to capture more than one cell, make a selection box.
if (selectionBounds == null)
{
selectionBounds = new CellCoordsRegion(topLeft, bottomRight);
// Lose focus on any search boxes so we can always copy/paste.
Ui.KeyboardFocusWidget = null;
}
else
{
// We already have a drag box; resize it
selectionBounds = new CellCoordsRegion(topLeft, bottomRight);
}
}
// Finished with mouse move events, so let them bubble up the widget tree.
if (mi.Event == MouseInputEvent.Move)
return false;
if (mi.Event == MouseInputEvent.Down && mi.Button == MouseButton.Left && selectionStartLocation == null)
{
// Start area drag.
selectionStartLocation = mi.Location;
}
if (mi.Event == MouseInputEvent.Up)
{
if (mi.Button == MouseButton.Left)
{
editorWidget.SetTooltip(null);
selectionStartLocation = null;
selectionStartCell = null;
// If we've released a bounds drag.
if (selectionBounds != null)
{
// Set this as the editor selection.
previousSelection = Selection;
SetSelection(new EditorSelection
{
Area = selectionBounds
});
selectionBounds = null;
editorActionManager.Add(new ChangeSelectionAction(this, Selection, previousSelection));
UpdateSelectedTab?.Invoke();
}
else if (underCursor != null)
{
// We've clicked on an actor.
if (Selection.Actor != underCursor)
{
previousSelection = Selection;
SetSelection(new EditorSelection
{
Actor = underCursor,
});
editorActionManager.Add(new ChangeSelectionAction(this, Selection, previousSelection));
UpdateSelectedTab?.Invoke();
}
}
else if (Selection.HasSelection)
{
// Released left mouse without dragging or selecting an actor - deselect current.
ClearSelection(updateSelectedTab: true);
}
}
else if (mi.Button == MouseButton.Right)
{
editorWidget.SetTooltip(null);
// Delete actor.
if (underCursor != null && underCursor != Selection.Actor && !draggingActor)
editorActionManager.Add(new RemoveActorAction(editorLayer, underCursor));
// Or delete resource if found under cursor.
if (resourceUnderCursor != null)
editorActionManager.Add(new RemoveResourceAction(resourceLayer, cell, resourceUnderCursor));
}
}
return true;
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self) { }
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr) { yield break; }
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr)
{
if (CurrentDragBounds.HasValue)
{
yield return new EditorSelectionAnnotationRenderable(CurrentDragBounds.Value, editorWidget.SelectionAltColor, editorWidget.SelectionAltOffset, CVec.Zero);
yield return new EditorSelectionAnnotationRenderable(CurrentDragBounds.Value, editorWidget.SelectionMainColor, int2.Zero, CVec.Zero);
}
}
public void Tick() { }
public void Dispose() { }
}
sealed class ChangeSelectionAction : IEditorAction
{
[FluentReference("x", "y", "width", "height")]
const string SelectedArea = "notification-selected-area";
[FluentReference("id")]
const string SelectedActor = "notification-selected-actor";
[FluentReference]
const string ClearedSelection = "notification-cleared-selection";
public string Text { get; }
readonly EditorSelection selection;
readonly EditorSelection previousSelection;
readonly EditorDefaultBrush defaultBrush;
public ChangeSelectionAction(
EditorDefaultBrush defaultBrush,
EditorSelection selection,
EditorSelection previousSelection)
{
this.defaultBrush = defaultBrush;
this.selection = selection;
this.previousSelection = new EditorSelection
{
Actor = previousSelection.Actor,
Area = previousSelection.Area
};
if (selection.Area.HasValue)
Text = FluentProvider.GetMessage(SelectedArea,
"x", selection.Area.Value.TopLeft.X,
"y", selection.Area.Value.TopLeft.Y,
"width", selection.Area.Value.BottomRight.X - selection.Area.Value.TopLeft.X,
"height", selection.Area.Value.BottomRight.Y - selection.Area.Value.TopLeft.Y);
else if (selection.Actor != null)
Text = FluentProvider.GetMessage(SelectedActor, "id", selection.Actor.ID);
else
Text = FluentProvider.GetMessage(ClearedSelection);
}
public void Execute()
{
Do();
}
public void Do()
{
defaultBrush.SetSelection(selection);
}
public void Undo()
{
defaultBrush.SetSelection(previousSelection);
}
}
sealed class DeleteAreaAction : IEditorAction
{
[FluentReference("x", "y", "width", "height")]
const string RemovedArea = "notification-removed-area";
public string Text { get; }
readonly EditorBlitSource editorBlitSource;
readonly MapBlitFilters blitFilters;
readonly IResourceLayer resourceLayer;
readonly EditorActorLayer editorActorLayer;
readonly CellCoordsRegion area;
readonly Map map;
public DeleteAreaAction(Map map, MapBlitFilters blitFilters, CellCoordsRegion area, IResourceLayer resourceLayer, EditorActorLayer editorActorLayer)
{
this.map = map;
this.blitFilters = blitFilters;
this.resourceLayer = resourceLayer;
this.editorActorLayer = editorActorLayer;
this.area = area;
editorBlitSource = EditorBlit.CopyRegionContents(map, editorActorLayer, resourceLayer, area, blitFilters);
Text = FluentProvider.GetMessage(RemovedArea,
"x", area.TopLeft.X,
"y", area.TopLeft.Y,
"width", area.BottomRight.X - area.TopLeft.X,
"height", area.BottomRight.Y - area.TopLeft.Y);
}
public void Execute()
{
Do();
}
public void Do()
{
if (blitFilters.HasFlag(MapBlitFilters.Actors))
{
// Clear any existing actors in the paste cells.
using (new PerfTimer("RemoveActors", 1))
editorActorLayer.RemoveRegion(area);
}
foreach (var tileKeyValuePair in editorBlitSource.Tiles)
{
var position = tileKeyValuePair.Key;
if (!map.Tiles.Contains(position))
continue;
// Clear any existing resources.
if (resourceLayer != null && blitFilters.HasFlag(MapBlitFilters.Resources))
resourceLayer.ClearResources(position);
if (blitFilters.HasFlag(MapBlitFilters.Terrain))
{
map.Tiles[position] = map.Rules.TerrainInfo.DefaultTerrainTile;
map.Height[position] = 0;
}
}
}
public void Undo()
{
foreach (var tileKeyValuePair in editorBlitSource.Tiles)
{
var position = tileKeyValuePair.Key;
if (!map.Tiles.Contains(position))
continue;
var tile = tileKeyValuePair.Value;
var resourceLayerContents = tile.ResourceLayerContents;
if (blitFilters.HasFlag(MapBlitFilters.Terrain))
{
map.Tiles[position] = tile.TerrainTile;
map.Height[position] = tile.Height;
}
if (blitFilters.HasFlag(MapBlitFilters.Resources) &&
resourceLayerContents.HasValue &&
!string.IsNullOrWhiteSpace(resourceLayerContents.Value.Type))
resourceLayer.AddResource(resourceLayerContents.Value.Type, position, resourceLayerContents.Value.Density);
}
if (blitFilters.HasFlag(MapBlitFilters.Actors))
{
// Create copies of the original actors, update their locations, and place.
var copies = new List<ActorReference>(editorBlitSource.Actors.Count);
foreach (var actorKeyValuePair in editorBlitSource.Actors)
{
var copy = actorKeyValuePair.Value.Export();
var locationInit = copy.GetOrDefault<LocationInit>();
if (locationInit != null)
{
if (!map.Tiles.Contains(locationInit.Value))
continue;
copy.RemoveAll<LocationInit>();
copy.Add(new LocationInit(locationInit.Value));
}
copies.Add(copy);
}
editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies));
}
}
}
sealed class RemoveSelectedActorAction : IEditorAction
{
[FluentReference("name", "id")]
const string RemovedActor = "notification-removed-actor";
public string Text { get; }
readonly EditorSelection selection;
readonly EditorDefaultBrush defaultBrush;
readonly EditorActorLayer editorActorLayer;
readonly EditorActorPreview actor;
public RemoveSelectedActorAction(
EditorDefaultBrush defaultBrush,
EditorActorLayer editorActorLayer,
EditorActorPreview actor)
{
this.defaultBrush = defaultBrush;
this.editorActorLayer = editorActorLayer;
this.actor = actor;
selection = new EditorSelection
{
Actor = defaultBrush.Selection.Actor
};
Text = FluentProvider.GetMessage(RemovedActor, "name", actor.Info.Name, "id", actor.ID);
}
public void Execute()
{
Do();
}
public void Do()
{
defaultBrush.SetSelection(new EditorSelection());
editorActorLayer.Remove(actor);
}
public void Undo()
{
editorActorLayer.Add(actor);
defaultBrush.SetSelection(selection);
}
}
sealed class RemoveActorAction : IEditorAction
{
[FluentReference("name", "id")]
const string RemovedActor = "notification-removed-actor";
public string Text { get; }
readonly EditorActorLayer editorActorLayer;
readonly EditorActorPreview actor;
public RemoveActorAction(EditorActorLayer editorActorLayer, EditorActorPreview actor)
{
this.editorActorLayer = editorActorLayer;
this.actor = actor;
Text = FluentProvider.GetMessage(RemovedActor, "name", actor.Info.Name, "id", actor.ID);
}
public void Execute()
{
Do();
}
public void Do()
{
editorActorLayer.Remove(actor);
}
public void Undo()
{
editorActorLayer.Add(actor);
}
}
sealed class MoveActorAction : IEditorAction
{
[FluentReference("id", "x1", "y1", "x2", "y2")]
const string MovedActor = "notification-moved-actor";
public string Text { get; private set; }
readonly EditorActorPreview actor;
readonly EditorActorLayer layer;
readonly CPos from;
CPos to;
public MoveActorAction(
EditorActorPreview actor,
EditorActorLayer layer)
{
this.actor = actor;
this.layer = layer;
from = actor.Location;
to = from;
}
public void Execute() { }
public void Do()
{
layer.MoveActor(actor, to);
}
public void Undo()
{
layer.MoveActor(actor, from);
}
public bool HasMoved => from != to;
public void Move(CPos to)
{
this.to = to;
layer.MoveActor(actor, this.to);
Text = FluentProvider.GetMessage(MovedActor, "id", actor.ID, "x1", from.X, "y1", from.Y, "x2", this.to.X, "y2", this.to.Y);
}
}
sealed class RemoveResourceAction : IEditorAction
{
[FluentReference("type")]
const string RemovedResource = "notification-removed-resource";
public string Text { get; }
readonly IResourceLayer resourceLayer;
readonly CPos cell;
ResourceLayerContents resourceContents;
public RemoveResourceAction(IResourceLayer resourceLayer, CPos cell, string resourceType)
{
this.resourceLayer = resourceLayer;
this.cell = cell;
Text = FluentProvider.GetMessage(RemovedResource, "type", resourceType);
}
public void Execute()
{
Do();
}
public void Do()
{
resourceContents = resourceLayer.GetResource(cell);
resourceLayer.ClearResources(cell);
}
public void Undo()
{
resourceLayer.ClearResources(cell);
resourceLayer.AddResource(resourceContents.Type, cell, resourceContents.Density);
}
}
}

View File

@@ -0,0 +1,265 @@
#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.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorMarkerLayerBrush : IEditorBrush
{
public int? Template;
readonly WorldRenderer worldRenderer;
readonly World world;
readonly EditorActionManager editorActionManager;
readonly MarkerLayerOverlay markerLayerOverlay;
readonly EditorViewportControllerWidget editorWidget;
readonly List<PaintMarkerTile> paintTiles = [];
bool painting;
CPos cell;
public EditorMarkerLayerBrush(EditorViewportControllerWidget editorWidget, int? id, WorldRenderer wr)
{
this.editorWidget = editorWidget;
worldRenderer = wr;
world = wr.World;
editorActionManager = world.WorldActor.Trait<EditorActionManager>();
markerLayerOverlay = world.WorldActor.Trait<MarkerLayerOverlay>();
Template = id;
}
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)
return true;
if (mi.Event == MouseInputEvent.Up)
{
UpdatePreview();
if (paintTiles.Count != 0)
{
editorActionManager.Add(new PaintMarkerTileEditorAction(Template, paintTiles.ToImmutableArray(), markerLayerOverlay));
paintTiles.Clear();
UpdatePreview(true);
}
painting = false;
}
else
{
painting = true;
UpdatePreview();
}
return true;
}
void UpdatePreview(bool forceRefresh = false)
{
var currentCell = worldRenderer.Viewport.ViewToWorld(Viewport.LastMousePos);
if (!forceRefresh && cell == currentCell)
return;
cell = currentCell;
if (!painting)
{
foreach (var paintTile in paintTiles)
markerLayerOverlay.SetTile(paintTile.Cell, paintTile.Previous);
paintTiles.Clear();
}
foreach (var cell in markerLayerOverlay.CalculateMirrorPositions(cell))
{
if (paintTiles.Any(t => t.Cell == cell))
continue;
var existing = markerLayerOverlay.CellLayer[cell];
if (existing == Template)
continue;
paintTiles.Add(new PaintMarkerTile(cell, existing));
markerLayerOverlay.SetTile(cell, Template);
}
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self) { UpdatePreview(); }
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr) { yield break; }
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr) { yield break; }
public void Tick() { }
public void Dispose()
{
foreach (var paintTile in paintTiles)
markerLayerOverlay.SetTile(paintTile.Cell, paintTile.Previous);
}
}
readonly struct PaintMarkerTile
{
public readonly CPos Cell;
public readonly int? Previous;
public PaintMarkerTile(CPos cell, int? previous)
{
Cell = cell;
Previous = previous;
}
}
sealed class PaintMarkerTileEditorAction : IEditorAction
{
[FluentReference("count", "type")]
const string AddedMarkerTiles = "notification-added-marker-tiles";
[FluentReference("count")]
const string RemovedMarkerTiles = "notification-removed-marker-tiles";
public string Text { get; }
readonly int? type;
readonly MarkerLayerOverlay markerLayerOverlay;
readonly ImmutableArray<PaintMarkerTile> paintTiles = [];
public PaintMarkerTileEditorAction(
int? type,
ImmutableArray<PaintMarkerTile> paintTiles,
MarkerLayerOverlay markerLayerOverlay)
{
this.type = type;
this.paintTiles = paintTiles;
this.markerLayerOverlay = markerLayerOverlay;
if (type != null)
{
var typeLabel = FluentProvider.GetMessage(markerLayerOverlay.Info.Colors.ElementAt(type.Value).Key);
Text = FluentProvider.GetMessage(AddedMarkerTiles, "count", paintTiles.Length, "type", typeLabel);
}
else
Text = FluentProvider.GetMessage(RemovedMarkerTiles, "count", paintTiles.Length);
}
public void Execute() { }
public void Do()
{
foreach (var paintTile in paintTiles)
markerLayerOverlay.SetTile(paintTile.Cell, type);
}
public void Undo()
{
foreach (var paintTile in paintTiles)
markerLayerOverlay.SetTile(paintTile.Cell, paintTile.Previous);
}
}
sealed class ClearSelectedMarkerTilesEditorAction : IEditorAction
{
[FluentReference("count", "type")]
const string ClearedSelectedMarkerTiles = "notification-cleared-selected-marker-tiles";
public string Text { get; }
readonly MarkerLayerOverlay markerLayerOverlay;
readonly ImmutableArray<CPos> tiles;
readonly int tile;
public ClearSelectedMarkerTilesEditorAction(
int tile,
MarkerLayerOverlay markerLayerOverlay)
{
this.tile = tile;
this.markerLayerOverlay = markerLayerOverlay;
tiles = markerLayerOverlay.Tiles[tile].ToImmutableArray();
var typeLabel = FluentProvider.GetMessage(markerLayerOverlay.Info.Colors.ElementAt(tile).Key);
Text = FluentProvider.GetMessage(ClearedSelectedMarkerTiles, "count", tiles.Length, "type", typeLabel);
}
public void Execute()
{
Do();
}
public void Do()
{
markerLayerOverlay.ClearSelected(tile);
}
public void Undo()
{
markerLayerOverlay.SetSelected(tile, tiles.AsSpan());
}
}
sealed class ClearAllMarkerTilesEditorAction : IEditorAction
{
[FluentReference("count")]
const string ClearedAllMarkerTiles = "notification-cleared-all-marker-tiles";
public string Text { get; }
readonly MarkerLayerOverlay markerLayerOverlay;
readonly FrozenDictionary<int, ImmutableArray<CPos>> tiles;
public ClearAllMarkerTilesEditorAction(
MarkerLayerOverlay markerLayerOverlay)
{
this.markerLayerOverlay = markerLayerOverlay;
tiles = markerLayerOverlay.Tiles.ToFrozenDictionary(t => t.Key, t => t.Value.ToImmutableArray());
var allTilesCount = tiles.Values.Sum(x => x.Length);
Text = FluentProvider.GetMessage(ClearedAllMarkerTiles, "count", allTilesCount);
}
public void Execute()
{
Do();
}
public void Do()
{
markerLayerOverlay.ClearAll();
}
public void Undo()
{
markerLayerOverlay.SetAll(tiles);
}
}
}

View File

@@ -0,0 +1,161 @@
#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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorResourceBrush : IEditorBrush
{
public readonly string ResourceType;
readonly WorldRenderer worldRenderer;
readonly World world;
readonly EditorViewportControllerWidget editorWidget;
readonly EditorActionManager editorActionManager;
readonly IResourceLayer resourceLayer;
AddResourcesEditorAction action;
bool resourceAdded;
CPos cell;
readonly List<IRenderable> preview = [];
readonly IResourceRenderer[] resourceRenderers;
public EditorResourceBrush(EditorViewportControllerWidget editorWidget, string resourceType, WorldRenderer wr)
{
this.editorWidget = editorWidget;
ResourceType = resourceType;
worldRenderer = wr;
world = wr.World;
editorActionManager = world.WorldActor.Trait<EditorActionManager>();
resourceLayer = world.WorldActor.Trait<IResourceLayer>();
resourceRenderers = world.WorldActor.TraitsImplementing<IResourceRenderer>().ToArray();
cell = wr.Viewport.ViewToWorld(wr.Viewport.WorldToViewPx(Viewport.LastMousePos));
UpdatePreview();
}
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;
}
var cell = worldRenderer.Viewport.ViewToWorld(mi.Location);
if (mi.Button == MouseButton.Left && mi.Event != MouseInputEvent.Up && resourceLayer.CanAddResource(ResourceType, cell))
{
action ??= new AddResourcesEditorAction(ResourceType, resourceLayer);
action.Add(new CellResource(cell, resourceLayer.GetResource(cell)));
resourceAdded = true;
}
else if (resourceAdded && mi.Button == MouseButton.Left && mi.Event == MouseInputEvent.Up)
{
editorActionManager.Add(action);
action = null;
resourceAdded = false;
}
return true;
}
void UpdatePreview()
{
var pos = world.Map.CenterOfCell(cell);
preview.Clear();
preview.AddRange(resourceRenderers.SelectMany(r => r.RenderPreview(worldRenderer, ResourceType, pos)));
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self)
{
var currentCell = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
if (cell != currentCell)
{
cell = currentCell;
UpdatePreview();
}
}
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr) { return action == null ? preview : null; }
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr) { yield break; }
public void Tick() { }
public void Dispose() { }
}
readonly record struct CellResource(CPos Cell, ResourceLayerContents OldResourceTile);
sealed class AddResourcesEditorAction : IEditorAction
{
[FluentReference("count", "type")]
const string AddedResource = "notification-added-resource";
public string Text { get; private set; }
readonly IResourceLayer resourceLayer;
readonly string resourceType;
readonly List<CellResource> cellResources = [];
public AddResourcesEditorAction(string resourceType, IResourceLayer resourceLayer)
{
this.resourceType = resourceType;
this.resourceLayer = resourceLayer;
}
public void Execute()
{
cellResources.TrimExcess();
}
public void Do()
{
foreach (var resourceCell in cellResources)
resourceLayer.AddResource(resourceType, resourceCell.Cell, resourceLayer.GetMaxDensity(resourceType));
}
public void Undo()
{
foreach (var resourceCell in cellResources)
{
// If resources match, simulate a replace command.
if (resourceCell.OldResourceTile.Type == resourceType || resourceCell.OldResourceTile.Type == null)
resourceLayer.ClearResources(resourceCell.Cell);
if (resourceCell.OldResourceTile.Type == resourceType || resourceCell.OldResourceTile.Type != null)
resourceLayer.AddResource(resourceCell.OldResourceTile.Type, resourceCell.Cell, resourceCell.OldResourceTile.Density);
}
}
public void Add(CellResource resourceCell)
{
resourceLayer.AddResource(resourceType, resourceCell.Cell, resourceLayer.GetMaxDensity(resourceType));
cellResources.Add(resourceCell);
Text = FluentProvider.GetMessage(AddedResource, "count", cellResources.Count, "type", resourceType);
}
}
}

View File

@@ -0,0 +1,383 @@
#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.Collections.Generic;
using System.IO;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorTileBrush : IEditorBrush
{
public readonly TerrainTemplateInfo TerrainTemplate;
public readonly ushort Template;
readonly WorldRenderer worldRenderer;
readonly World world;
readonly ITemplatedTerrainInfo terrainInfo;
readonly EditorViewportControllerWidget editorWidget;
readonly EditorActionManager editorActionManager;
bool painting;
readonly ITiledTerrainRenderer terrainRenderer;
CPos cell;
readonly List<IRenderable> preview = [];
public EditorTileBrush(EditorViewportControllerWidget editorWidget, ushort id, WorldRenderer wr)
{
this.editorWidget = editorWidget;
worldRenderer = wr;
world = wr.World;
terrainInfo = world.Map.Rules.TerrainInfo as ITemplatedTerrainInfo;
if (terrainInfo == null)
throw new InvalidDataException($"{nameof(EditorTileBrush)} can only be used with template-based tilesets");
editorActionManager = world.WorldActor.Trait<EditorActionManager>();
terrainRenderer = world.WorldActor.Trait<ITiledTerrainRenderer>();
Template = id;
TerrainTemplate = terrainInfo.Templates[Template];
cell = wr.Viewport.ViewToWorld(wr.Viewport.WorldToViewPx(Viewport.LastMousePos));
UpdatePreview();
}
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)
{
if (mi.Event == MouseInputEvent.Down)
painting = true;
else if (mi.Event == MouseInputEvent.Up)
painting = false;
}
if (!painting)
return true;
if (mi.Event != MouseInputEvent.Down && mi.Event != MouseInputEvent.Move)
return true;
var cell = worldRenderer.Viewport.ViewToWorld(mi.Location);
var isMoving = mi.Event == MouseInputEvent.Move;
if (mi.Modifiers.HasModifier(Modifiers.Shift))
{
FloodFillWithBrush(cell);
painting = false;
}
else
PaintCell(cell, isMoving);
return true;
}
void PaintCell(CPos cell, bool isMoving)
{
var template = terrainInfo.Templates[Template];
if (isMoving && PlacementOverlapsSameTemplate(template, cell))
return;
editorActionManager.Add(new PaintTileEditorAction(Template, world.Map, cell));
}
void FloodFillWithBrush(CPos cell)
{
var map = world.Map;
if (!map.Contains(cell))
return;
var mapTiles = map.Tiles;
var replace = mapTiles[cell];
if (replace.Type == Template)
return;
editorActionManager.Add(new FloodFillEditorAction(Template, map, cell));
}
bool PlacementOverlapsSameTemplate(TerrainTemplateInfo template, CPos cell)
{
var map = world.Map;
var mapTiles = map.Tiles;
var i = 0;
for (var y = 0; y < template.Size.Y; y++)
{
for (var x = 0; x < template.Size.X; x++, i++)
{
if (template.Contains(i) && template[i] != null)
{
var c = cell + new CVec(x, y);
if (mapTiles.Contains(c) && mapTiles[c].Type == template.Id)
return true;
}
}
}
return false;
}
void UpdatePreview()
{
var pos = world.Map.CenterOfCell(cell);
preview.Clear();
preview.AddRange(terrainRenderer.RenderPreview(worldRenderer, TerrainTemplate, pos));
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self)
{
var currentCell = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
if (cell != currentCell)
{
cell = currentCell;
UpdatePreview();
}
}
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr) { return preview; }
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr) { yield break; }
public void Tick() { }
public void Dispose() { }
}
sealed class PaintTileEditorAction : IEditorAction
{
[FluentReference("id")]
const string AddedTile = "notification-added-tile";
public string Text { get; }
readonly ushort template;
readonly Map map;
readonly CPos cell;
readonly Queue<UndoTile> undoTiles = [];
readonly TerrainTemplateInfo terrainTemplate;
public PaintTileEditorAction(ushort template, Map map, CPos cell)
{
this.template = template;
this.map = map;
this.cell = cell;
var terrainInfo = (ITemplatedTerrainInfo)map.Rules.TerrainInfo;
terrainTemplate = terrainInfo.Templates[template];
Text = FluentProvider.GetMessage(AddedTile, "id", terrainTemplate.Id);
}
public void Execute()
{
Do();
}
public void Do()
{
var mapTiles = map.Tiles;
var mapHeight = map.Height;
var baseHeight = mapHeight.Contains(cell) ? mapHeight[cell] : (byte)0;
var i = 0;
for (var y = 0; y < terrainTemplate.Size.Y; y++)
{
for (var x = 0; x < terrainTemplate.Size.X; x++, i++)
{
if (terrainTemplate.Contains(i) && terrainTemplate[i] != null)
{
var index = terrainTemplate.PickAny ? (byte)Game.CosmeticRandom.Next(0, terrainTemplate.TilesCount) : (byte)i;
var c = cell + new CVec(x, y);
if (!mapTiles.Contains(c))
continue;
undoTiles.Enqueue(new UndoTile(c, mapTiles[c], mapHeight[c]));
mapTiles[c] = new TerrainTile(template, index);
mapHeight[c] = (byte)(baseHeight + terrainTemplate[index].Height).Clamp(0, map.Grid.MaximumTerrainHeight);
}
}
}
}
public void Undo()
{
var mapTiles = map.Tiles;
var mapHeight = map.Height;
while (undoTiles.Count > 0)
{
var undoTile = undoTiles.Dequeue();
mapTiles[undoTile.Cell] = undoTile.MapTile;
mapHeight[undoTile.Cell] = undoTile.Height;
}
}
}
sealed class FloodFillEditorAction : IEditorAction
{
[FluentReference("id")]
const string FilledTile = "notification-filled-tile";
public string Text { get; }
readonly ushort template;
readonly Map map;
readonly CPos cell;
readonly Queue<UndoTile> undoTiles = [];
readonly TerrainTemplateInfo terrainTemplate;
public FloodFillEditorAction(ushort template, Map map, CPos cell)
{
this.template = template;
this.map = map;
this.cell = cell;
var terrainInfo = (ITemplatedTerrainInfo)map.Rules.TerrainInfo;
terrainTemplate = terrainInfo.Templates[template];
Text = FluentProvider.GetMessage(FilledTile, "id", terrainTemplate.Id);
}
public void Execute()
{
Do();
}
public void Do()
{
var queue = new Queue<CPos>();
var touched = new CellLayer<bool>(map);
var mapTiles = map.Tiles;
var replace = mapTiles[cell];
void MaybeEnqueue(CPos newCell)
{
if (map.Contains(cell) && !touched[newCell])
{
queue.Enqueue(newCell);
touched[newCell] = true;
}
}
bool ShouldPaint(CPos cellToCheck)
{
for (var y = 0; y < terrainTemplate.Size.Y; y++)
{
for (var x = 0; x < terrainTemplate.Size.X; x++)
{
var c = cellToCheck + new CVec(x, y);
if (!map.Contains(c) || mapTiles[c].Type != replace.Type)
return false;
}
}
return true;
}
CPos FindEdge(CPos refCell, CVec direction)
{
while (true)
{
var newCell = refCell + direction;
if (!ShouldPaint(newCell))
return refCell;
refCell = newCell;
}
}
queue.Enqueue(cell);
while (queue.Count > 0)
{
var queuedCell = queue.Dequeue();
if (!ShouldPaint(queuedCell))
continue;
var previousCell = FindEdge(queuedCell, new CVec(-1 * terrainTemplate.Size.X, 0));
var nextCell = FindEdge(queuedCell, new CVec(1 * terrainTemplate.Size.X, 0));
for (var x = previousCell.X; x <= nextCell.X; x += terrainTemplate.Size.X)
{
PaintSingleCell(new CPos(x, queuedCell.Y));
var upperCell = new CPos(x, queuedCell.Y - 1 * terrainTemplate.Size.Y);
var lowerCell = new CPos(x, queuedCell.Y + 1 * terrainTemplate.Size.Y);
if (ShouldPaint(upperCell))
MaybeEnqueue(upperCell);
if (ShouldPaint(lowerCell))
MaybeEnqueue(lowerCell);
}
}
}
public void Undo()
{
var mapTiles = map.Tiles;
var mapHeight = map.Height;
while (undoTiles.Count > 0)
{
var undoTile = undoTiles.Dequeue();
mapTiles[undoTile.Cell] = undoTile.MapTile;
mapHeight[undoTile.Cell] = undoTile.Height;
}
}
void PaintSingleCell(CPos cellToPaint)
{
var mapTiles = map.Tiles;
var mapHeight = map.Height;
var baseHeight = mapHeight.Contains(cellToPaint) ? mapHeight[cellToPaint] : (byte)0;
var i = 0;
for (var y = 0; y < terrainTemplate.Size.Y; y++)
{
for (var x = 0; x < terrainTemplate.Size.X; x++, i++)
{
if (terrainTemplate.Contains(i) && terrainTemplate[i] != null)
{
var index = terrainTemplate.PickAny ? (byte)Game.CosmeticRandom.Next(0, terrainTemplate.TilesCount) : (byte)i;
var c = cellToPaint + new CVec(x, y);
if (!mapTiles.Contains(c))
continue;
undoTiles.Enqueue(new UndoTile(c, mapTiles[c], mapHeight[c]));
mapTiles[c] = new TerrainTile(template, index);
mapHeight[c] = (byte)(baseHeight + terrainTemplate[index].Height).Clamp(0, map.Grid.MaximumTerrainHeight);
}
}
}
}
}
sealed record UndoTile(CPos Cell, TerrainTile MapTile, byte Height);
}

View File

@@ -0,0 +1,376 @@
#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.EditorBrushes;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
namespace OpenRA.Mods.Common.Widgets
{
public sealed class EditorTilingPathBrush : IEditorBrush
{
readonly TilingPathTool tool;
readonly WorldRenderer worldRenderer;
readonly EditorActionManager editorActionManager;
MouseInput? startingMouseInput = null;
bool isDragging = false;
TilingPathTool.PathPlan previewPlan = null;
public EditorTilingPathBrush(TilingPathTool tool)
{
this.tool = tool;
worldRenderer = tool.WorldRenderer;
editorActionManager = worldRenderer.World.WorldActor.Trait<EditorActionManager>();
}
public bool HandleMouseInput(MouseInput mouseInput)
{
if (mouseInput.Button != MouseButton.Left)
return false;
var isFinal = false;
if (mouseInput.Event == MouseInputEvent.Down)
{
startingMouseInput = mouseInput;
isDragging = false;
}
else if (startingMouseInput != null)
{
if (mouseInput.Event == MouseInputEvent.Up)
{
isFinal = true;
}
}
else
{
return false;
}
CPos ViewToWorldCorner(int2 xy) =>
CellLayerUtils.WPosToCorner(
worldRenderer.ProjectedPosition(
worldRenderer.Viewport.ViewToWorldPx(xy)),
worldRenderer.World.Map.Grid.Type);
var from = ViewToWorldCorner(startingMouseInput.Value.Location);
var to = ViewToWorldCorner(mouseInput.Location);
void UpdatePlan(TilingPathTool.PathPlan newPlan, bool preview)
{
if (isFinal)
{
editorActionManager.Add(
new UpdateTilingPathPlanEditorAction(tool, newPlan));
}
else if (preview)
{
previewPlan = newPlan;
}
}
if (isFinal)
{
previewPlan = null;
startingMouseInput = null;
}
isDragging |= to != from;
var plan = tool.Plan;
if (plan == null)
{
UpdatePlan(new TilingPathTool.PathPlan(to), true);
return true;
}
var points = plan.PointsWithRallyIndex();
(bool IsInside, bool IsRally, int RallyIndex, bool IsStartDirector, bool IsEndDirector)
AssessCPos(CPos cpos)
{
var isInside = points.Select(p => p.CPos).Contains(cpos);
var isRally = plan.Rallies.Contains(cpos);
var rallyIndex =
isRally
? plan.Rallies.TakeWhile(r => r != cpos).Count()
: points
.Where(p => p.CPos == cpos)
.Select(p => p.RallyIndex)
.FirstOrDefault(0);
var isStartDirector =
plan.AutoStart != Direction.None
&& cpos == plan.FirstPoint - plan.AutoStart.ToCVec();
var isEndDirector =
plan.AutoEnd != Direction.None
&& cpos == plan.LastPoint + plan.AutoEnd.ToCVec();
return (isInside, isRally, rallyIndex, isStartDirector, isEndDirector);
}
var (fromIsInside, fromIsRally, fromRallyIndex, fromIsStartDirector, fromIsEndDirector) =
AssessCPos(from);
var (toIsInside, toIsRally, toRallyIndex, toIsStartDirector, toIsEndDirector) =
AssessCPos(to);
if (isDragging)
{
if (fromIsStartDirector)
{
var offset = plan.FirstPoint - to;
var direction =
offset != CVec.Zero
? DirectionExts.ClosestFromCVec(offset)
: Direction.None;
UpdatePlan(plan.WithStart(direction), true);
}
else if (fromIsEndDirector)
{
var offset = to - plan.LastPoint;
var direction =
offset != CVec.Zero
? DirectionExts.ClosestFromCVec(offset)
: Direction.None;
UpdatePlan(plan.WithEnd(direction), true);
}
else if (fromIsInside)
{
if (fromIsRally)
{
if (!toIsRally || to == from)
{
UpdatePlan(plan.WithRallyReplaced(fromRallyIndex, to), true);
}
}
else
{
UpdatePlan(plan.Moved(to - from), true);
}
}
else
{
if (!toIsRally)
{
UpdatePlan(plan.WithRallyAppended(to), true);
}
}
}
else
{
if (toIsInside)
{
if (toIsRally)
{
if (toRallyIndex == 0)
{
UpdatePlan(plan.WithLoop(!plan.Loop), false);
}
else
{
UpdatePlan(plan.WithRallyRemoved(toRallyIndex), false);
}
}
else
{
UpdatePlan(plan.WithRallyInserted(toRallyIndex, to), false);
}
}
else
{
UpdatePlan(plan.WithRallyAppended(to), true);
}
}
return true;
}
void IEditorBrush.TickRender(WorldRenderer wr, Actor self) { }
IEnumerable<IRenderable> IEditorBrush.RenderAboveShroud(Actor self, WorldRenderer wr)
{
if (tool.EditorBlitSource == null)
yield break;
var stickToGround = tool.EditorBlitSource.Value.Tiles.Count == 0;
var preview = EditorBlit.PreviewBlitSource(
tool.EditorBlitSource.Value,
MapBlitFilters.Terrain | MapBlitFilters.Actors,
CVec.Zero,
wr,
stickToGround);
foreach (var renderable in preview)
yield return renderable;
}
IEnumerable<IRenderable> IEditorBrush.RenderAnnotations(Actor self, WorldRenderer wr)
{
var plan = previewPlan ?? tool.Plan;
if (plan == null)
yield break;
var mainColor = tool.EditorBlitSource != null ? Color.Cyan : Color.Red;
var map = worldRenderer.World.Map;
var gridType = map.Grid.Type;
WPos CornerOfCell(CPos cpos) => CellLayerUtils.CornerToWPos(cpos, gridType);
var points = plan.Points();
for (var i = 1; i < points.Length; i++)
{
yield return new CircleAnnotationRenderable(
CornerOfCell(points[i]), new WDist(128), 1, Color.Yellow, false);
yield return new LineAnnotationRenderable(
CornerOfCell(points[i - 1]),
CornerOfCell(points[i]),
1,
Color.Yellow,
Color.Yellow);
}
for (var i = 1; i < plan.Rallies.Length; i++)
{
yield return new CircleAnnotationRenderable(
CornerOfCell(plan.Rallies[i]), new WDist(512), 1, mainColor, false);
yield return new LineAnnotationRenderable(
CornerOfCell(plan.Rallies[i - 1]),
CornerOfCell(plan.Rallies[i]),
1,
mainColor,
mainColor);
}
if (plan.AutoEnd != Direction.None)
yield return new CircleAnnotationRenderable(
CornerOfCell(plan.LastPoint) + map.Offset(plan.AutoEnd.ToCVec(), 0) * 768 / 1024,
new WDist(256),
2,
plan.End != Direction.None ? Color.Magenta : Color.Gray,
false);
if (plan.AutoStart != Direction.None)
yield return new CircleAnnotationRenderable(
CornerOfCell(plan.FirstPoint) - map.Offset(plan.AutoStart.ToCVec(), 0) * 768 / 1024,
new WDist(256),
2,
plan.Start != Direction.None ? Color.Magenta : Color.Gray,
true);
yield return new CircleAnnotationRenderable(
CornerOfCell(plan.Rallies[0]), new WDist(512), 1, mainColor, true);
}
public void Tick() { }
public void Dispose() { }
}
sealed class UpdateTilingPathPlanEditorAction : IEditorAction
{
[FluentReference]
const string StartedPlan = "notification-tiling-path-started";
[FluentReference]
const string UpdatedPlan = "notification-tiling-path-updated";
[FluentReference]
const string ResetPlan = "notification-tiling-path-reset";
public string Text { get; }
readonly TilingPathTool tool;
readonly TilingPathTool.PathPlan oldPlan;
readonly TilingPathTool.PathPlan newPlan;
public UpdateTilingPathPlanEditorAction(
TilingPathTool tool,
TilingPathTool.PathPlan newPlan)
{
this.tool = tool;
oldPlan = tool.Plan;
this.newPlan = newPlan;
if (oldPlan == null && newPlan == null)
throw new ArgumentException("oldPlan and newPlan cannot both be null");
else if (oldPlan == null)
Text = FluentProvider.GetMessage(StartedPlan);
else if (newPlan == null)
Text = FluentProvider.GetMessage(ResetPlan);
else
Text = FluentProvider.GetMessage(UpdatedPlan);
}
public void Execute()
{
Do();
}
public void Do()
{
tool.SetPlan(newPlan);
}
public void Undo()
{
tool.SetPlan(oldPlan);
}
}
sealed class PaintTilingPathEditorAction : IEditorAction
{
[FluentReference]
const string Painted = "notification-tiling-path-painted";
public string Text { get; }
readonly TilingPathTool tool;
readonly TilingPathTool.PathPlan plan;
readonly EditorBlit editorBlit;
public PaintTilingPathEditorAction(TilingPathTool tool)
{
this.tool = tool;
plan = tool.Plan;
Text = FluentProvider.GetMessage(Painted);
var world = tool.World;
var editorActorLayer = world.WorldActor.Trait<EditorActorLayer>();
var blitSource = tool.EditorBlitSource.Value;
editorBlit = new EditorBlit(
MapBlitFilters.Terrain | MapBlitFilters.Actors,
null,
blitSource.CellCoords.TopLeft,
world.Map,
blitSource,
editorActorLayer,
false);
}
public void Execute()
{
Do();
}
public void Do()
{
tool.SetPlan(null);
editorBlit.Commit();
}
public void Undo()
{
editorBlit.Revert();
tool.SetPlan(plan);
}
}
}