573 lines
16 KiB
C#
573 lines
16 KiB
C#
#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.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<LocationInit>().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<string> MousePositionChanged = _ => { };
|
|
public event Action<KeyValuePair<string, ActorReference>> ActorDoubleClicked = _ => { };
|
|
|
|
Dictionary<string, ActorTemplate> actorTemplates = new Dictionary<string, ActorTemplate>();
|
|
public Dictionary<int, ResourceTemplate> ResourceTemplates = new Dictionary<int, ResourceTemplate>();
|
|
|
|
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<ActorTemplate> templates)
|
|
{
|
|
actorTemplates = templates.ToDictionary(a => a.Info.Name.ToLowerInvariant());
|
|
}
|
|
|
|
public void BindResourceTemplates(IEnumerable<ResourceTemplate> templates)
|
|
{
|
|
ResourceTemplates = templates.ToDictionary(a => a.Info.ResourceType);
|
|
}
|
|
|
|
public Dictionary<int2, Bitmap> Chunks = new Dictionary<int2, Bitmap>();
|
|
|
|
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 uv = new MPos(ui, vj);
|
|
var tr = Map.MapTiles.Value[uv];
|
|
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[uv].Type != 0)
|
|
{
|
|
var resourceImage = ResourceTemplates[Map.MapResources.Value[uv].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<PlayerColorPaletteInfo>();
|
|
var remap = new PlayerColorRemap(pcpi.RemapIndex, pr.Color, pcpi.Ramp);
|
|
return new ImmutablePalette(PlayerPalette, remap).AsSystemPalette();
|
|
}
|
|
|
|
Cache<string, ColorPalette> playerPalettes;
|
|
|
|
public ColorPalette GetPaletteForPlayer(string player)
|
|
{
|
|
if (playerPalettes == null)
|
|
playerPalettes = new Cache<string, ColorPalette>(GetPaletteForPlayerInner);
|
|
|
|
return playerPalettes[player];
|
|
}
|
|
|
|
ColorPalette GetPaletteForActor(ActorReference ar)
|
|
{
|
|
var ownerInit = ar.InitDict.GetOrDefault<OwnerInit>();
|
|
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;
|
|
}
|
|
}
|
|
} |