#region Copyright & License Information /* * Copyright 2007-2014 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.Drawing.Imaging; using System.Linq; using System.Windows.Forms; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Traits; using SGraphics = System.Drawing.Graphics; namespace OpenRA.Editor { static class ActorReferenceExts { public static CPos Location(this ActorReference ar) { return (CPos)ar.InitDict.Get().Value(null); } public static void DrawStringContrast(this SGraphics g, Font f, string s, int x, int y, Brush fg, Brush bg) { g.DrawString(s, f, bg, x - 1, y - 1); g.DrawString(s, f, bg, x + 1, y - 1); g.DrawString(s, f, bg, x - 1, y + 1); g.DrawString(s, f, bg, x + 1, y + 1); g.DrawString(s, f, fg, x, y); } } class Surface : Control { public Map Map { get; private set; } public TileSet TileSet { get; private set; } public TileSetRenderer TileSetRenderer { get; private set; } public IPalette Palette { get; private set; } public IPalette PlayerPalette { get; private set; } public int2 Offset; public int2 GetOffset() { return Offset; } public float Zoom = 1.0f; ITool currentTool; public bool IsPanning; public bool IsErasing; public bool ShowActorNames; public bool ShowGrid; public bool ShowRuler; public bool IsPaste { get { return TileSelection != null && ResourceSelection != null; } } public TerrainTile[,] TileSelection; public ResourceTile[,] ResourceSelection; public CPos SelectionStart; public CPos SelectionEnd; public string NewActorOwner; public event Action AfterChange = () => { }; public event Action MousePositionChanged = _ => { }; public event Action> ActorDoubleClicked = _ => { }; Dictionary actorTemplates = new Dictionary(); public Dictionary ResourceTemplates = new Dictionary(); static readonly Font MarkerFont = new Font(FontFamily.GenericSansSerif, 12.0f, FontStyle.Regular); static readonly SolidBrush TextBrush = new SolidBrush(Color.Red); public Keys GetModifiers() { return ModifierKeys; } public void Bind(Map m, TileSet ts, TileSetRenderer tsr, IPalette p, IPalette pp) { Map = m; TileSet = ts; TileSetRenderer = tsr; Palette = p; PlayerPalette = pp; playerPalettes = null; Chunks.Clear(); currentTool = null; } public void SetTool(ITool tool) { currentTool = tool; ClearSelection(); } public void BindActorTemplates(IEnumerable templates) { actorTemplates = templates.ToDictionary(a => a.Info.Name.ToLowerInvariant()); } public void BindResourceTemplates(IEnumerable templates) { ResourceTemplates = templates.ToDictionary(a => a.Info.ResourceType); } public Dictionary Chunks = new Dictionary(); public Surface() { BackColor = Color.Black; SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.ResizeRedraw, true); UpdateStyles(); } static readonly Pen SelectionPen = new Pen(Color.Blue); static readonly Pen PastePen = new Pen(Color.Green); static readonly Pen CordonPen = new Pen(Color.Red); int2 mousePos; public void Scroll(int2 dx) { Offset -= dx; Invalidate(); } protected override void OnDoubleClick(EventArgs e) { base.OnDoubleClick(e); var x = Map.Actors.Value.FirstOrDefault(a => a.Value.Location() == GetBrushLocation()); if (x.Key != null) ActorDoubleClicked(x); } protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); if (Map == null) return; Zoom *= e.Delta > 0 ? 4.0f / 3.0f : .75f; Invalidate(); } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); this.Parent.Focus(); Invalidate(); } protected override void OnMouseEnter(EventArgs e) { base.OnMouseLeave(e); this.Focus(); Invalidate(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (Map == null) return; var oldMousePos = mousePos; mousePos = new int2(e.Location); MousePositionChanged(GetBrushLocation().ToString()); if (e.Button == MouseButtons.Middle || (e.Button != MouseButtons.None && IsPanning)) Scroll(oldMousePos - mousePos); else { if (e.Button == MouseButtons.Right || (IsErasing && e.Button == MouseButtons.Left)) Erase(); if (e.Button == MouseButtons.Left && !IsErasing) Draw(); Invalidate(); } } void Erase() { // Crash preventing var brushLocation = GetBrushLocation(); if (Map == null || brushLocation.X >= Map.MapSize.X || brushLocation.Y >= Map.MapSize.Y || brushLocation.X < 0 || brushLocation.Y < 0) return; currentTool = null; var key = Map.Actors.Value.FirstOrDefault(a => a.Value.Location() == brushLocation); if (key.Key != null) Map.Actors.Value.Remove(key.Key); if (Map.MapResources.Value[brushLocation].Type != 0) { Map.MapResources.Value[brushLocation] = new ResourceTile(0, 0); var ch = new int2(brushLocation.X / ChunkSize, brushLocation.Y / ChunkSize); if (Chunks.ContainsKey(ch)) { Chunks[ch].Dispose(); Chunks.Remove(ch); } } AfterChange(); ClearSelection(); } void Draw() { if (currentTool != null) { currentTool.Apply(this); AfterChange(); } else if (IsPaste) PasteSelection(); else SelectionEnd = GetBrushLocationBR(); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (Map == null) return; if (!IsPanning) { if (e.Button == MouseButtons.Right) Erase(); if (e.Button == MouseButtons.Left && !IsErasing) { Draw(); if (!IsPaste) SelectionStart = SelectionEnd = GetBrushLocation(); } } Invalidate(); } public const int ChunkSize = 8; // 8x8 chunks ==> 192x192 bitmaps. Bitmap RenderChunk(int u, int v) { var bitmap = new Bitmap(ChunkSize * TileSetRenderer.TileSize, ChunkSize * TileSetRenderer.TileSize); var data = bitmap.LockBits(bitmap.Bounds(), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); unsafe { var p = (int*)data.Scan0.ToPointer(); var stride = data.Stride >> 2; for (var i = 0; i < ChunkSize; i++) for (var j = 0; j < ChunkSize; j++) { var ui = u * ChunkSize + i; var vj = v * ChunkSize + j; var tr = Map.MapTiles.Value[ui, vj]; var tile = TileSetRenderer.Data(tr.Type); if (tile == null) continue; var index = (tr.Index < tile.Length) ? tr.Index : (byte)0; var rawImage = tile[index]; for (var x = 0; x < TileSetRenderer.TileSize; x++) for (var y = 0; y < TileSetRenderer.TileSize; y++) p[(j * TileSetRenderer.TileSize + y) * stride + i * TileSetRenderer.TileSize + x] = Palette.GetColor(rawImage[x + TileSetRenderer.TileSize * y]).ToArgb(); if (Map.MapResources.Value[ui, vj].Type != 0) { var resourceImage = ResourceTemplates[Map.MapResources.Value[ui, vj].Type].Bitmap; var srcdata = resourceImage.LockBits(resourceImage.Bounds(), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); var q = (int*)srcdata.Scan0.ToPointer(); var srcstride = srcdata.Stride >> 2; for (var x = 0; x < TileSetRenderer.TileSize; x++) for (var y = 0; y < TileSetRenderer.TileSize; y++) { var c = q[y * srcstride + x]; if ((c & 0xff000000) != 0) /* quick & dirty, i cbf doing real alpha */ p[(j * TileSetRenderer.TileSize + y) * stride + i * TileSetRenderer.TileSize + x] = c; } resourceImage.UnlockBits(srcdata); } } } bitmap.UnlockBits(data); if (ShowGrid) { using (var g = SGraphics.FromImage(bitmap)) { var ts = Game.ModData.Manifest.TileSize; var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); ControlPaint.DrawGrid(g, rect, new Size(2, ts.Height), Color.DarkRed); ControlPaint.DrawGrid(g, rect, new Size(ts.Width, 2), Color.DarkRed); ControlPaint.DrawGrid(g, rect, new Size(ts.Width, ts.Height), Color.Red); } } return bitmap; } public CPos GetBrushLocation() { var vX = (int)Math.Floor((mousePos.X - Offset.X) / Zoom); var vY = (int)Math.Floor((mousePos.Y - Offset.Y) / Zoom); return new CPos(vX / TileSetRenderer.TileSize, vY / TileSetRenderer.TileSize); } public CPos GetBrushLocationBR() { var vX = (int)Math.Floor((mousePos.X - Offset.X) / Zoom); var vY = (int)Math.Floor((mousePos.Y - Offset.Y) / Zoom); return new CPos( (vX + TileSetRenderer.TileSize - 1) / TileSetRenderer.TileSize, (vY + TileSetRenderer.TileSize - 1) / TileSetRenderer.TileSize); } public void DrawActor(SGraphics g, CPos p, ActorTemplate t, ColorPalette cp) { var centered = t.Appearance == null || !t.Appearance.RelativeToTopLeft; var actorPalette = cp; if (t.Appearance != null && t.Appearance.UseTerrainPalette) actorPalette = Palette.AsSystemPalette(); DrawImage(g, t.Bitmap, p, centered, actorPalette); } float2 GetDrawPosition(CPos location, Bitmap bmp, bool centered) { float offsetX = centered ? bmp.Width / 2 - TileSetRenderer.TileSize / 2 : 0; var drawX = TileSetRenderer.TileSize * location.X * Zoom + Offset.X - offsetX; float offsetY = centered ? bmp.Height / 2 - TileSetRenderer.TileSize / 2 : 0; var drawY = TileSetRenderer.TileSize * location.Y * Zoom + Offset.Y - offsetY; return new float2(drawX, drawY); } public void DrawImage(SGraphics g, Bitmap bmp, CPos location, bool centered, ColorPalette cp) { var drawPos = GetDrawPosition(location, bmp, centered); var sourceRect = new RectangleF(0, 0, bmp.Width, bmp.Height); var destRect = new RectangleF(drawPos.X, drawPos.Y, bmp.Width * Zoom, bmp.Height * Zoom); var restorePalette = bmp.Palette; if (cp != null) bmp.Palette = cp; g.DrawImage(bmp, destRect, sourceRect, GraphicsUnit.Pixel); if (cp != null) bmp.Palette = restorePalette; } void DrawActorBorder(SGraphics g, CPos p, ActorTemplate t) { var centered = t.Appearance == null || !t.Appearance.RelativeToTopLeft; var drawPos = GetDrawPosition(p, t.Bitmap, centered); g.DrawRectangle(CordonPen, drawPos.X, drawPos.Y, t.Bitmap.Width * Zoom, t.Bitmap.Height * Zoom); } ColorPalette GetPaletteForPlayerInner(string name) { var pr = Map.Players[name]; var pcpi = Program.Rules.Actors["player"].Traits.Get(); var remap = new PlayerColorRemap(pcpi.RemapIndex, pr.Color, pcpi.Ramp); return new ImmutablePalette(PlayerPalette, remap).AsSystemPalette(); } Cache playerPalettes; public ColorPalette GetPaletteForPlayer(string player) { if (playerPalettes == null) playerPalettes = new Cache(GetPaletteForPlayerInner); return playerPalettes[player]; } ColorPalette GetPaletteForActor(ActorReference ar) { var ownerInit = ar.InitDict.GetOrDefault(); if (ownerInit == null) return null; return GetPaletteForPlayer(ownerInit.PlayerName); } protected override void OnPaint(PaintEventArgs e) { if (Map == null) return; if (TileSet == null) return; for (var u = 0; u < Map.MapSize.X; u += ChunkSize) for (var v = 0; v < Map.MapSize.Y; v += ChunkSize) { var x = new int2(u / ChunkSize, v / ChunkSize); if (!Chunks.ContainsKey(x)) Chunks[x] = RenderChunk(u / ChunkSize, v / ChunkSize); var bmp = Chunks[x]; var drawX = TileSetRenderer.TileSize * (float)ChunkSize * (float)x.X * Zoom + Offset.X; var drawY = TileSetRenderer.TileSize * (float)ChunkSize * (float)x.Y * Zoom + Offset.Y; var sourceRect = new RectangleF(0, 0, bmp.Width, bmp.Height); var destRect = new RectangleF(drawX, drawY, bmp.Width * Zoom, bmp.Height * Zoom); e.Graphics.DrawImage(bmp, destRect, sourceRect, GraphicsUnit.Pixel); } e.Graphics.DrawRectangle(CordonPen, Map.Bounds.Left * TileSetRenderer.TileSize * Zoom + Offset.X, Map.Bounds.Top * TileSetRenderer.TileSize * Zoom + Offset.Y, Map.Bounds.Width * TileSetRenderer.TileSize * Zoom, Map.Bounds.Height * TileSetRenderer.TileSize * Zoom); e.Graphics.DrawRectangle(SelectionPen, (SelectionStart.X * TileSetRenderer.TileSize * Zoom) + Offset.X, (SelectionStart.Y * TileSetRenderer.TileSize * Zoom) + Offset.Y, (SelectionEnd - SelectionStart).X * TileSetRenderer.TileSize * Zoom, (SelectionEnd - SelectionStart).Y * TileSetRenderer.TileSize * Zoom); if (IsPaste) { var loc = GetBrushLocation(); var width = Math.Abs((SelectionStart - SelectionEnd).X); var height = Math.Abs((SelectionStart - SelectionEnd).Y); e.Graphics.DrawRectangle(PastePen, (loc.X * TileSetRenderer.TileSize * Zoom) + Offset.X, (loc.Y * TileSetRenderer.TileSize * Zoom) + Offset.Y, width * (TileSetRenderer.TileSize * Zoom), height * (TileSetRenderer.TileSize * Zoom)); } foreach (var ar in Map.Actors.Value) { if (actorTemplates.ContainsKey(ar.Value.Type)) DrawActor(e.Graphics, ar.Value.Location(), actorTemplates[ar.Value.Type], GetPaletteForActor(ar.Value)); else Console.WriteLine("Warning: Unknown or excluded actor: {0}", ar.Value.Type); } if (ShowActorNames) foreach (var ar in Map.Actors.Value) if (!ar.Key.StartsWith("Actor")) // if it has a custom name e.Graphics.DrawStringContrast(Font, ar.Key, (int)(ar.Value.Location().X * TileSetRenderer.TileSize * Zoom + Offset.X), (int)(ar.Value.Location().Y * TileSetRenderer.TileSize * Zoom + Offset.Y), Brushes.White, Brushes.Black); if (ShowRuler && Zoom > 0.2) { for (var i = Map.Bounds.Left; i <= Map.Bounds.Right; i += 8) { if (i % 8 == 0) { var point = new PointF(i * TileSetRenderer.TileSize * Zoom + Offset.X, (Map.Bounds.Top - 8) * TileSetRenderer.TileSize * Zoom + Offset.Y); e.Graphics.DrawString((i - Map.Bounds.Left).ToString(), MarkerFont, TextBrush, point); } } for (var i = Map.Bounds.Top; i <= Map.Bounds.Bottom; i += 8) { if (i % 8 == 0) { var point = new PointF((Map.Bounds.Left - 8) * TileSetRenderer.TileSize * Zoom + Offset.X, i * TileSetRenderer.TileSize * Zoom + Offset.Y); e.Graphics.DrawString((i - Map.Bounds.Left).ToString(), MarkerFont, TextBrush, point); } } } if (currentTool != null) currentTool.Preview(this, e.Graphics); if (currentTool == null) { var x = Map.Actors.Value.FirstOrDefault(a => a.Value.Location() == GetBrushLocation()); if (x.Key != null && actorTemplates.ContainsKey(x.Value.Type)) DrawActorBorder(e.Graphics, x.Value.Location(), actorTemplates[x.Value.Type]); } } public void CopySelection() { // Grab tiles and resources within selection (doesn't do actors) var start = SelectionStart; var end = SelectionEnd; if (start == end) return; var width = Math.Abs((start - end).X); var height = Math.Abs((start - end).Y); TileSelection = new TerrainTile[width, height]; ResourceSelection = new ResourceTile[width, height]; for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { // TODO: crash prevention var cell = new CPos(start.X + x, start.Y + y); TileSelection[x, y] = Map.MapTiles.Value[cell]; ResourceSelection[x, y] = Map.MapResources.Value[cell]; } } } void PasteSelection() { var loc = GetBrushLocation(); var width = Math.Abs((SelectionStart - SelectionEnd).X); var height = Math.Abs((SelectionStart - SelectionEnd).Y); for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { var mapX = loc.X + x; var mapY = loc.Y + y; var cell = new CPos(mapX, mapY); // TODO: crash prevention for outside of bounds Map.MapTiles.Value[cell] = TileSelection[x, y]; Map.MapResources.Value[cell] = ResourceSelection[x, y]; var ch = new int2(mapX / ChunkSize, mapY / ChunkSize); if (Chunks.ContainsKey(ch)) { Chunks[ch].Dispose(); Chunks.Remove(ch); } } } AfterChange(); } void ClearSelection() { SelectionStart = CPos.Zero; SelectionEnd = CPos.Zero; TileSelection = null; ResourceSelection = null; } } }