Introduce MapCache and MapPreview for improved UI map previews.

This commit is contained in:
Paul Chote
2014-03-14 13:30:03 +13:00
parent 63068d5a7c
commit c30b18a9d6
20 changed files with 407 additions and 290 deletions

View File

@@ -370,7 +370,7 @@ namespace OpenRA
modData = new ModData(mod);
Renderer.InitializeFonts(modData.Manifest);
modData.InitializeLoaders();
modData.LoadMaps();
modData.MapCache.LoadMaps();
PerfHistory.items["render"].hasNormalTick = false;
PerfHistory.items["batches"].hasNormalTick = false;
@@ -401,7 +401,7 @@ namespace OpenRA
if (Settings.Server.DedicatedLoop)
{
Console.WriteLine("Starting a new server instance...");
modData.LoadMaps();
modData.MapCache.LoadMaps();
continue;
}
@@ -426,13 +426,14 @@ namespace OpenRA
static string ChooseShellmap()
{
var shellmaps = modData.AvailableMaps
.Where(m => m.Value.UseAsShellmap);
var shellmaps = modData.MapCache
.Where(m => m.Status == MapStatus.Available && m.Map.UseAsShellmap)
.Select(m => m.Uid);
if (!shellmaps.Any())
throw new InvalidDataException("No valid shellmaps available");
return shellmaps.Random(CosmeticRandom).Key;
return shellmaps.Random(CosmeticRandom);
}
static bool quit;
@@ -553,7 +554,7 @@ namespace OpenRA
WebClient webClient = new WebClient();
webClient.DownloadFile(url, tempFile);
File.Move(tempFile, mapPath);
Game.modData.AvailableMaps.Add(mapHash, new Map(mapPath));
Game.modData.MapCache[mapHash].UpdateFromMap(new Map(mapPath));
Log.Write("debug", "New map has been downloaded to '{0}'", mapPath);
return true;

View File

@@ -67,6 +67,14 @@ namespace OpenRA.Graphics
return rect;
}
public Sprite Add(Bitmap src)
{
var rect = Allocate(src.Size);
Util.FastCopyIntoSprite(rect, src);
current.CommitData();
return rect;
}
public Sprite Add(Size size, byte paletteIndex)
{
var data = new byte[size.Width * size.Height];

View File

@@ -9,6 +9,8 @@
#endregion
using System;
using System.Drawing;
using System.Drawing.Imaging;
using OpenRA.FileFormats.Graphics;
namespace OpenRA.Graphics
@@ -59,6 +61,36 @@ namespace OpenRA.Graphics
}
}
public static void FastCopyIntoSprite(Sprite dest, Bitmap src)
{
var destStride = dest.sheet.Size.Width;
var width = dest.bounds.Width;
var height = dest.bounds.Height;
var srcData = src.LockBits(src.Bounds(),
ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
unsafe
{
var c = (int*)srcData.Scan0;
// Cast the data to an int array so we can copy the src data directly
fixed (byte* bd = &dest.sheet.Data[0])
{
var data = (int*)bd;
var x = dest.bounds.Left;
var y = dest.bounds.Top;
for (var j = 0; j < height; j++)
for (var i = 0; i < width; i++)
data[(y + j) * destStride + x + i] = *(c + (j * srcData.Stride >> 2) + i);
}
}
src.UnlockBits(srcData);
}
public static float[] IdentityMatrix()
{
return Exts.MakeArray(16, j => (j % 5 == 0) ? 1.0f : 0);

148
OpenRA.Game/MapCache.cs Executable file
View File

@@ -0,0 +1,148 @@
#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;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using OpenRA.FileFormats;
using OpenRA.Graphics;
using OpenRA.Widgets;
namespace OpenRA
{
public class MapCache : IEnumerable<MapPreview>
{
public static readonly MapPreview UnknownMap = new MapPreview(null, null);
readonly Cache<string, MapPreview> previews;
readonly Manifest manifest;
readonly SheetBuilder sheetBuilder;
Thread previewLoaderThread;
object syncRoot = new object();
Queue<MapPreview> generateMinimap = new Queue<MapPreview>();
public MapCache(Manifest m)
{
manifest = m;
previews = new Cache<string, MapPreview>(uid => new MapPreview(uid, this));
sheetBuilder = new SheetBuilder(SheetType.BGRA);
}
public void LoadMaps()
{
var paths = manifest.MapFolders.SelectMany(f => FindMapsIn(f));
foreach (var path in paths)
{
try
{
var map = new Map(path, manifest.Mod.Id);
if (manifest.MapCompatibility.Contains(map.RequiresMod))
previews[map.Uid].UpdateFromMap(map);
}
catch (Exception e)
{
Console.WriteLine("Failed to load map: {0}", path);
Console.WriteLine("Details: {0}", e);
}
}
}
static IEnumerable<string> FindMapsIn(string dir)
{
string[] noMaps = { };
// Ignore optional flag
if (dir.StartsWith("~"))
dir = dir.Substring(1);
// Paths starting with ^ are relative to the user directory
if (dir.StartsWith("^"))
dir = Platform.SupportDir + dir.Substring(1);
if (!Directory.Exists(dir))
return noMaps;
var dirsWithMaps = Directory.GetDirectories(dir)
.Where(d => Directory.GetFiles(d, "map.yaml").Any() && Directory.GetFiles(d, "map.bin").Any());
return dirsWithMaps.Concat(Directory.GetFiles(dir, "*.oramap"));
}
void LoadAsyncInternal()
{
for (;;)
{
MapPreview p;
lock (syncRoot)
{
if (generateMinimap.Count == 0)
break;
p = generateMinimap.Peek();
// Preview already exists
if (p.Minimap != null)
{
generateMinimap.Dequeue();
continue;
}
}
// Render the minimap into the shared sheet
// Note: this is not generally thread-safe, but it works here because:
// (a) This worker is the only thread writing to this sheet
// (b) The main thread is the only thread reading this sheet
// (c) The sheet is marked dirty after the write is completed,
// which causes the main thread to copy this to the texture during
// the next render cycle.
// (d) Any partially written bytes from the next minimap is in an
// unallocated area, and will be committed in the next cycle.
var bitmap = Minimap.RenderMapPreview(p.Map, true);
p.Minimap = sheetBuilder.Add(bitmap);
lock (syncRoot)
generateMinimap.Dequeue();
// Yuck... But this helps the UI Jank when opening the map selector significantly.
Thread.Sleep(50);
}
}
public void CacheMinimap(MapPreview preview)
{
lock (syncRoot)
generateMinimap.Enqueue(preview);
if (previewLoaderThread == null || !previewLoaderThread.IsAlive)
{
previewLoaderThread = new Thread(LoadAsyncInternal);
previewLoaderThread.Start();
}
}
public MapPreview this[string key]
{
get { return previews[key]; }
}
public IEnumerator<MapPreview> GetEnumerator()
{
return previews.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

91
OpenRA.Game/MapPreview.cs Executable file
View File

@@ -0,0 +1,91 @@
#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;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using OpenRA.FileFormats;
using OpenRA.Graphics;
using OpenRA.Widgets;
namespace OpenRA
{
public enum MapStatus { Available, Unavailable }
public class MapPreview
{
static readonly List<CPos> NoSpawns = new List<CPos>();
public readonly string Uid;
public string Title { get; private set; }
public string Type { get; private set; }
public string Author { get; private set; }
public int PlayerCount { get; private set; }
public List<CPos> SpawnPoints { get; private set; }
public Rectangle Bounds { get; private set; }
public Map Map { get; private set; }
public MapStatus Status { get; private set; }
Sprite minimap;
bool generatingMinimap;
public Sprite Minimap
{
get
{
if (minimap != null)
return minimap;
if (!generatingMinimap && Status == MapStatus.Available)
{
generatingMinimap = true;
cache.CacheMinimap(this);
}
return null;
}
set
{
minimap = value;
generatingMinimap = false;
}
}
MapCache cache;
public MapPreview(string uid, MapCache cache)
{
this.cache = cache;
Uid = uid;
Title = "Unknown Map";
Type = "Unknown";
Author = "Unknown Author";
PlayerCount = 0;
Bounds = Rectangle.Empty;
SpawnPoints = NoSpawns;
Status = MapStatus.Unavailable;
}
public void UpdateFromMap(Map m)
{
Map = m;
Title = m.Title;
Type = m.Type;
Type = m.Type;
Author = m.Author;
PlayerCount = m.Players.Count(x => x.Value.Playable);
Bounds = m.Bounds;
SpawnPoints = m.GetSpawnPoints().ToList();
Status = MapStatus.Available;
}
}
}

View File

@@ -22,34 +22,13 @@ namespace OpenRA
{
public readonly Manifest Manifest;
public readonly ObjectCreator ObjectCreator;
public Dictionary<string, Map> AvailableMaps { get; private set; }
public readonly WidgetLoader WidgetLoader;
public readonly MapCache MapCache;
public ILoadScreen LoadScreen = null;
public SheetBuilder SheetBuilder;
public SpriteLoader SpriteLoader;
public VoxelLoader VoxelLoader;
public static IEnumerable<string> FindMapsIn(string dir)
{
string[] noMaps = { };
// ignore optional flag
if (dir.StartsWith("~"))
dir = dir.Substring(1);
// paths starting with ^ are relative to the user directory
if (dir.StartsWith("^"))
dir = Platform.SupportDir + dir.Substring(1);
if (!Directory.Exists(dir))
return noMaps;
var dirsWithMaps = Directory.GetDirectories(dir)
.Where(d => Directory.GetFiles(d, "map.yaml").Any() && Directory.GetFiles(d, "map.bin").Any());
return dirsWithMaps.Concat(Directory.GetFiles(dir, "*.oramap"));
}
public ModData(string mod)
{
Languages = new string[0];
@@ -59,6 +38,7 @@ namespace OpenRA
LoadScreen.Init(Manifest, Manifest.LoadScreen.NodesDict.ToDictionary(x => x.Key, x => x.Value.Value));
LoadScreen.Display();
WidgetLoader = new WidgetLoader(this);
MapCache = new MapCache(Manifest);
// HACK: Mount only local folders so we have a half-working environment for the asset installer
FileSystem.UnmountAll();
@@ -119,17 +99,13 @@ namespace OpenRA
FieldLoader.Translations = translations;
}
public void LoadMaps()
{
AvailableMaps = FindMaps();
}
public Map PrepareMap(string uid)
{
LoadScreen.Display();
if (!AvailableMaps.ContainsKey(uid))
var map = MapCache[uid].Map;
if (map == null)
throw new InvalidDataException("Invalid map uid: {0}".F(uid));
var map = new Map(AvailableMaps[uid].Path);
LoadTranslations(map);
@@ -148,33 +124,6 @@ namespace OpenRA
VoxelProvider.Initialize(Manifest.VoxelSequences, map.VoxelSequences);
return map;
}
public Dictionary<string, Map> FindMaps()
{
var paths = Manifest.MapFolders.SelectMany(f => FindMapsIn(f));
var ret = new Dictionary<string, Map>();
foreach (var path in paths)
{
try
{
var map = new Map(path, Manifest.Mod.Id);
if (Manifest.MapCompatibility.Contains(map.RequiresMod))
ret.Add(map.Uid, map);
}
catch (Exception e)
{
Console.WriteLine("Failed to load map: {0}", path);
Console.WriteLine("Details: {0}", e);
}
}
return ret;
}
public Map FindMapByUid(string uid)
{
return AvailableMaps.ContainsKey(uid) ? AvailableMaps[uid] : null;
}
}
public interface ILoadScreen

View File

@@ -37,7 +37,7 @@ namespace OpenRA.Network
// Don't have the map locally
// TODO: We allow joining, then drop on game start if the map isn't available
if (!Game.modData.AvailableMaps.ContainsKey(Map) && !Game.Settings.Game.AllowDownloading)
if (Game.modData.MapCache[Map].Status != MapStatus.Available && !Game.Settings.Game.AllowDownloading)
return false;
return true;

View File

@@ -35,10 +35,7 @@ namespace OpenRA.Network
return null;
var map = LobbyInfo.GlobalSettings.Map;
if (!Game.modData.AvailableMaps.ContainsKey(map))
return null;
return Game.modData.AvailableMaps[map];
return Game.modData.MapCache[map].Map;
}
}
}

View File

@@ -232,6 +232,8 @@
<Compile Include="Traits\World\ActorMap.cs" />
<Compile Include="Widgets\HotkeyEntryWidget.cs" />
<Compile Include="Effects\MoveFlash.cs" />
<Compile Include="MapCache.cs" />
<Compile Include="MapPreview.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRA.FileFormats\OpenRA.FileFormats.csproj">

View File

@@ -21,7 +21,7 @@ namespace OpenRA.Widgets
{
public class MapPreviewWidget : Widget
{
public Func<Map> Map = () => null;
public Func<MapPreview> Preview = () => null;
public Func<Dictionary<CPos, Session.Client>> SpawnClients = () => new Dictionary<CPos, Session.Client>();
public Action<MouseInput> OnMouseDown = _ => {};
public bool IgnoreMouseInput = false;
@@ -32,6 +32,9 @@ namespace OpenRA.Widgets
Lazy<TooltipContainerWidget> tooltipContainer;
public int TooltipSpawnIndex = -1;
Rectangle MapRect;
float PreviewScale = 0;
public MapPreviewWidget()
{
tooltipContainer = Lazy.New(() => Ui.Root.Get<TooltipContainerWidget>(TooltipContainer));
@@ -40,8 +43,7 @@ namespace OpenRA.Widgets
protected MapPreviewWidget(MapPreviewWidget other)
: base(other)
{
lastMap = other.lastMap;
Map = other.Map;
Preview = other.Preview;
SpawnClients = other.SpawnClients;
ShowSpawnPoints = other.ShowSpawnPoints;
TooltipTemplate = other.TooltipTemplate;
@@ -65,74 +67,51 @@ namespace OpenRA.Widgets
public override void MouseEntered()
{
if (TooltipContainer == null) return;
tooltipContainer.Value.SetTooltip(TooltipTemplate, new WidgetArgs() {{ "preview", this }});
if (TooltipContainer != null)
tooltipContainer.Value.SetTooltip(TooltipTemplate, new WidgetArgs() {{ "preview", this }});
}
public override void MouseExited()
{
if (TooltipContainer == null) return;
tooltipContainer.Value.RemoveTooltip();
if (TooltipContainer != null)
tooltipContainer.Value.RemoveTooltip();
}
public int2 ConvertToPreview(CPos point)
{
var map = Map();
return new int2(MapRect.X + (int)(PreviewScale*(point.X - map.Bounds.Left)) , MapRect.Y + (int)(PreviewScale*(point.Y - map.Bounds.Top)));
var preview = Preview();
return new int2(MapRect.X + (int)(PreviewScale*(point.X - preview.Bounds.Left)) , MapRect.Y + (int)(PreviewScale*(point.Y - preview.Bounds.Top)));
}
Sheet mapChooserSheet;
Sprite mapChooserSprite;
Map lastMap;
Rectangle MapRect;
float PreviewScale = 0;
Sprite minimap;
public override void Draw()
{
var map = Map();
if (map == null)
var preview = Preview();
if (preview == null)
return;
// Preview unavailable
if (!Loaded)
{
GeneratePreview();
// Stash a copy of the minimap to ensure consistency
// (it may be modified by another thread)
minimap = preview.Minimap;
if (minimap == null)
return;
}
if (lastMap != map)
{
lastMap = map;
// Update image data
Bitmap preview;
lock (syncRoot)
preview = Previews[map.Uid];
if (mapChooserSheet == null || mapChooserSheet.Size.Width != preview.Width || mapChooserSheet.Size.Height != preview.Height)
mapChooserSheet = new Sheet(new Size(preview.Width, preview.Height));
mapChooserSheet.Texture.SetData(preview);
mapChooserSprite = new Sprite(mapChooserSheet, new Rectangle(0, 0, map.Bounds.Width, map.Bounds.Height), TextureChannel.Alpha);
}
// Update map rect
PreviewScale = Math.Min(RenderBounds.Width * 1.0f / map.Bounds.Width, RenderBounds.Height * 1.0f / map.Bounds.Height);
var size = Math.Max(map.Bounds.Width, map.Bounds.Height);
var dw = (int)(PreviewScale * (size - map.Bounds.Width)) / 2;
var dh = (int)(PreviewScale * (size - map.Bounds.Height)) / 2;
MapRect = new Rectangle(RenderBounds.X + dw, RenderBounds.Y + dh, (int)(map.Bounds.Width * PreviewScale), (int)(map.Bounds.Height * PreviewScale));
PreviewScale = Math.Min(RenderBounds.Width / minimap.size.X, RenderBounds.Height / minimap.size.Y);
var w = (int)(PreviewScale * minimap.size.X);
var h = (int)(PreviewScale * minimap.size.Y);
var x = RenderBounds.X + (RenderBounds.Width - w) / 2;
var y = RenderBounds.Y + (RenderBounds.Height - h) / 2;
MapRect = new Rectangle(x, y, w, h);
Game.Renderer.RgbaSpriteRenderer.DrawSprite(mapChooserSprite,
new float2(MapRect.Location),
new float2(MapRect.Size));
Game.Renderer.RgbaSpriteRenderer.DrawSprite(minimap, new float2(MapRect.Location), new float2(MapRect.Size));
TooltipSpawnIndex = -1;
if (ShowSpawnPoints)
{
var colors = SpawnClients().ToDictionary(c => c.Key, c => c.Value.Color.RGB);
var spawnPoints = map.GetSpawnPoints().ToList();
var spawnPoints = preview.SpawnPoints;
foreach (var p in spawnPoints)
{
var owned = colors.ContainsKey(p);
@@ -151,79 +130,6 @@ namespace OpenRA.Widgets
}
}
// Async map preview generation bits
enum PreviewStatus { Invalid, Uncached, Generating, Cached }
static Thread previewLoaderThread;
static object syncRoot = new object();
static Queue<string> cacheUids = new Queue<string>();
static readonly Dictionary<string, Bitmap> Previews = new Dictionary<string, Bitmap>();
void LoadAsyncInternal()
{
for (;;)
{
string uid;
lock (syncRoot)
{
if (cacheUids.Count == 0)
break;
uid = cacheUids.Peek();
}
var bitmap = Minimap.RenderMapPreview(Game.modData.AvailableMaps[uid], false);
lock (syncRoot)
{
// TODO: We should add previews to a sheet here (with multiple previews per sheet)
Previews.Add(uid, bitmap);
cacheUids.Dequeue();
}
// Yuck... But this helps the UI Jank when opening the map selector significantly.
Thread.Sleep(50);
}
}
bool compatibleTileset;
void GeneratePreview()
{
var m = Map();
if (m == null)
return;
compatibleTileset = Rules.TileSets.Values.Any(t => t.Id == m.Tileset);
var status = Status(m);
if (status == PreviewStatus.Uncached)
lock (syncRoot)
cacheUids.Enqueue(m.Uid);
if (previewLoaderThread == null || !previewLoaderThread.IsAlive)
{
previewLoaderThread = new Thread(LoadAsyncInternal);
previewLoaderThread.Start();
}
}
PreviewStatus Status(Map m)
{
if (m == null)
return PreviewStatus.Invalid;
lock (syncRoot)
{
if (Previews.ContainsKey(m.Uid))
return PreviewStatus.Cached;
if (cacheUids.Contains(m.Uid))
return PreviewStatus.Generating;
if (!compatibleTileset)
return PreviewStatus.Invalid;
}
return PreviewStatus.Uncached;
}
public bool Loaded { get { return Status(Map()) == PreviewStatus.Cached; } }
public bool Loaded { get { return minimap != null; } }
}
}

View File

@@ -225,14 +225,13 @@ namespace OpenRA.Widgets
public static Action Once( Action a ) { return () => { if (a != null) { a(); a = null; } }; }
public static string ChooseInitialMap(string map)
public static string ChooseInitialMap(string initialUid)
{
var availableMaps = Game.modData.AvailableMaps;
if (string.IsNullOrEmpty(map) || !availableMaps.ContainsKey(map))
if (string.IsNullOrEmpty(initialUid) || Game.modData.MapCache[initialUid].Status != MapStatus.Available)
{
Func<Map, bool> isIdealMap = m =>
Func<MapPreview, bool> isIdealMap = m =>
{
if (!m.Selectable)
if (m.Status != MapStatus.Available || !m.Map.Selectable)
return false;
// Other map types may have confusing settings or gameplay
@@ -240,22 +239,22 @@ namespace OpenRA.Widgets
return false;
// Maps with bots disabled confuse new players
if (m.Players.Any(s => !s.Value.AllowBots))
if (m.Map.Players.Any(s => !s.Value.AllowBots))
return false;
// Large maps expose unfortunate performance problems
if (m.MapSize.X > 128 || m.MapSize.Y > 128)
if (m.Bounds.Width > 128 || m.Bounds.Height > 128)
return false;
return true;
};
var selected = availableMaps.Values.Where(m => isIdealMap(m)).RandomOrDefault(Game.CosmeticRandom) ??
availableMaps.Values.First(m => m.Selectable);
var selected = Game.modData.MapCache.Where(m => isIdealMap(m)).RandomOrDefault(Game.CosmeticRandom) ??
Game.modData.MapCache.First(m => m.Status == MapStatus.Available && m.Map.Selectable);
return selected.Uid;
}
return map;
return initialUid;
}
}