Define a consistent interface for sprite loading. Fixes #4176.

This commit is contained in:
Paul Chote
2013-11-29 18:57:14 +13:00
parent 20a6c75ba4
commit f92ce8bf51
12 changed files with 323 additions and 183 deletions

View File

@@ -12,17 +12,16 @@ using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
namespace OpenRA.FileFormats
{
public class R8Image
class R8Image : ISpriteFrame
{
public readonly Size Size;
public readonly int2 Offset;
public readonly byte[] Image;
// Legacy variable. Can be removed when the utility command is made sensible.
public readonly Size FrameSize;
public Size Size { get; private set; }
public Size FrameSize { get; private set; }
public float2 Offset { get; private set; }
public byte[] Data { get; set; }
public R8Image(Stream s)
{
@@ -52,7 +51,7 @@ namespace OpenRA.FileFormats
// Skip alignment byte
s.ReadUInt8();
Image = s.ReadBytes(width*height);
Data = s.ReadBytes(width*height);
// Ignore palette
if (type == 1 && paletteOffset != 0)
@@ -60,33 +59,19 @@ namespace OpenRA.FileFormats
}
}
public class R8Reader : IEnumerable<R8Image>
public class R8Reader : ISpriteSource
{
readonly List<R8Image> headers = new List<R8Image>();
readonly List<R8Image> frames = new List<R8Image>();
public IEnumerable<ISpriteFrame> Frames { get { return frames.Cast<ISpriteFrame>(); } }
public readonly int Frames;
public readonly int ImageCount;
public R8Reader(Stream stream)
{
while (stream.Position < stream.Length)
{
headers.Add(new R8Image(stream));
Frames++;
frames.Add(new R8Image(stream));
ImageCount++;
}
}
public R8Image this[int index]
{
get { return headers[index]; }
}
public IEnumerator<R8Image> GetEnumerator()
{
return headers.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -15,23 +15,28 @@ using System.Linq;
namespace OpenRA.FileFormats
{
public class ImageHeader
class ImageHeader : ISpriteFrame
{
public uint Offset;
public Size Size { get; private set; }
public Size FrameSize { get { return Size; } }
public float2 Offset { get { return float2.Zero; } }
public byte[] Data { get; set; }
public uint FileOffset;
public Format Format;
public uint RefOffset;
public Format RefFormat;
public ImageHeader RefImage;
public byte[] Image;
// Used by ShpWriter
public ImageHeader() { }
public ImageHeader( BinaryReader reader )
public ImageHeader(BinaryReader reader, Size size)
{
var data = reader.ReadUInt32();
Offset = data & 0xffffff;
Size = size;
FileOffset = data & 0xffffff;
Format = (Format)(data >> 24);
RefOffset = reader.ReadUInt16();
@@ -42,7 +47,7 @@ namespace OpenRA.FileFormats
public void WriteTo(BinaryWriter writer)
{
writer.Write(Offset | ((uint)Format << 24));
writer.Write(FileOffset | ((uint)Format << 24));
writer.Write((ushort)RefOffset);
writer.Write((ushort)RefFormat);
}
@@ -50,7 +55,7 @@ namespace OpenRA.FileFormats
public enum Format { Format20 = 0x20, Format40 = 0x40, Format80 = 0x80 }
public class ShpReader
public class ShpReader : ISpriteSource
{
public readonly int ImageCount;
public readonly ushort Width;
@@ -59,6 +64,7 @@ namespace OpenRA.FileFormats
public Size Size { get { return new Size(Width, Height); } }
readonly List<ImageHeader> headers = new List<ImageHeader>();
public IEnumerable<ISpriteFrame> Frames { get { return headers.Cast<ISpriteFrame>(); } }
int recurseDepth = 0;
@@ -73,13 +79,14 @@ namespace OpenRA.FileFormats
Height = reader.ReadUInt16();
reader.ReadUInt32();
var size = new Size(Width, Height);
for (int i = 0 ; i < ImageCount ; i++)
headers.Add(new ImageHeader(reader));
headers.Add(new ImageHeader(reader, size));
new ImageHeader(reader); // end-of-file header
new ImageHeader(reader); // all-zeroes header
new ImageHeader(reader, size); // end-of-file header
new ImageHeader(reader, size); // all-zeroes header
var offsets = headers.ToDictionary(h => h.Offset, h =>h);
var offsets = headers.ToDictionary(h => h.FileOffset, h =>h);
for (int i = 0 ; i < ImageCount ; i++)
{
@@ -89,7 +96,7 @@ namespace OpenRA.FileFormats
else if (h.Format == Format.Format40)
if (!offsets.TryGetValue(h.RefOffset, out h.RefImage))
throw new InvalidDataException("Reference doesnt point to image data {0}->{1}".F(h.Offset, h.RefOffset));
throw new InvalidDataException("Reference doesnt point to image data {0}->{1}".F(h.FileOffset, h.RefOffset));
}
foreach (ImageHeader h in headers)
@@ -97,11 +104,6 @@ namespace OpenRA.FileFormats
}
}
public ImageHeader this[int index]
{
get { return headers[index]; }
}
void Decompress(Stream stream, ImageHeader h)
{
if (recurseDepth > ImageCount)
@@ -112,22 +114,22 @@ namespace OpenRA.FileFormats
case Format.Format20:
case Format.Format40:
{
if (h.RefImage.Image == null)
if (h.RefImage.Data == null)
{
++recurseDepth;
Decompress(stream, h.RefImage);
--recurseDepth;
}
h.Image = CopyImageData(h.RefImage.Image);
Format40.DecodeInto(ReadCompressedData(stream, h), h.Image);
h.Data = CopyImageData(h.RefImage.Data);
Format40.DecodeInto(ReadCompressedData(stream, h), h.Data);
break;
}
case Format.Format80:
{
var imageBytes = new byte[Width * Height];
Format80.DecodeInto(ReadCompressedData(stream, h), imageBytes);
h.Image = imageBytes;
h.Data = imageBytes;
break;
}
default:
@@ -137,7 +139,7 @@ namespace OpenRA.FileFormats
static byte[] ReadCompressedData(Stream stream, ImageHeader h)
{
stream.Position = h.Offset;
stream.Position = h.FileOffset;
// TODO: Actually, far too big. There's no length field with the correct length though :(
var compressedLength = (int)(stream.Length - stream.Position);
@@ -156,12 +158,45 @@ namespace OpenRA.FileFormats
return imageData;
}
public IEnumerable<ImageHeader> Frames { get { return headers; } }
public static ShpReader Load(string filename)
{
using (var s = File.OpenRead(filename))
return new ShpReader(s);
}
public static void Write(Stream s, int width, int height, IEnumerable<byte[]> frames)
{
var compressedFrames = frames.Select(f => Format80.Encode(f)).ToArray();
// note: end-of-file and all-zeroes headers
var dataOffset = 14 + (compressedFrames.Length + 2) * ImageHeader.SizeOnDisk;
using (var bw = new BinaryWriter(s))
{
bw.Write((ushort)compressedFrames.Length);
bw.Write((ushort)0); // unused
bw.Write((ushort)0); // unused
bw.Write((ushort)width);
bw.Write((ushort)height);
bw.Write((uint)0); // unused
foreach (var f in compressedFrames)
{
var ih = new ImageHeader { Format = Format.Format80, FileOffset = (uint)dataOffset };
dataOffset += f.Length;
ih.WriteTo(bw);
}
var eof = new ImageHeader { FileOffset = (uint)dataOffset };
eof.WriteTo(bw);
var allZeroes = new ImageHeader { };
allZeroes.WriteTo(bw);
foreach (var f in compressedFrames)
bw.Write(f);
}
}
}
}

View File

@@ -15,17 +15,17 @@ using System.Linq;
namespace OpenRA.FileFormats
{
public class TSImageHeader
class FrameHeader : ISpriteFrame
{
public readonly Size Size;
public readonly float2 Offset;
public Size Size { get; private set; }
public Size FrameSize { get; private set; }
public float2 Offset { get; private set; }
public byte[] Data { get; set; }
public readonly uint FileOffset;
public readonly byte Format;
public byte[] Image;
public TSImageHeader(Stream stream, Size frameSize)
public FrameHeader(Stream stream, Size frameSize)
{
var x = stream.ReadUInt16();
var y = stream.ReadUInt16();
@@ -34,6 +34,7 @@ namespace OpenRA.FileFormats
Offset = new float2(x + 0.5f * (width - frameSize.Width), y + 0.5f * (height - frameSize.Height));
Size = new Size(width, height);
FrameSize = frameSize;
Format = stream.ReadUInt8();
stream.Position += 11;
@@ -41,13 +42,13 @@ namespace OpenRA.FileFormats
}
}
public class ShpTSReader
public class ShpTSReader : ISpriteSource
{
public readonly int ImageCount;
public readonly Size Size;
readonly List<TSImageHeader> frames = new List<TSImageHeader>();
public IEnumerable<TSImageHeader> Frames { get { return frames; } }
readonly List<FrameHeader> frames = new List<FrameHeader>();
public IEnumerable<ISpriteFrame> Frames { get { return frames.Cast<ISpriteFrame>(); } }
public ShpTSReader(Stream stream)
{
@@ -58,7 +59,7 @@ namespace OpenRA.FileFormats
ImageCount = stream.ReadUInt16();
for (var i = 0; i < ImageCount; i++)
frames.Add(new TSImageHeader(stream, Size));
frames.Add(new FrameHeader(stream, Size));
for (var i = 0; i < ImageCount; i++)
{
@@ -70,23 +71,23 @@ namespace OpenRA.FileFormats
// Uncompressed
if (f.Format == 1 || f.Format == 0)
f.Image = stream.ReadBytes(f.Size.Width * f.Size.Height);
f.Data = stream.ReadBytes(f.Size.Width * f.Size.Height);
// Uncompressed scanlines
else if (f.Format == 2)
{
f.Image = new byte[f.Size.Width * f.Size.Height];
f.Data = new byte[f.Size.Width * f.Size.Height];
for (var j = 0; j < f.Size.Height; j++)
{
var length = stream.ReadUInt16() - 2;
stream.Read(f.Image, f.Size.Width * j, length);
stream.Read(f.Data, f.Size.Width * j, length);
}
}
// RLE-zero compressed scanlines
else if (f.Format == 3)
{
f.Image = new byte[f.Size.Width * f.Size.Height];
f.Data = new byte[f.Size.Width * f.Size.Height];
for (var j = 0; j < f.Size.Height; j++)
{
@@ -103,7 +104,7 @@ namespace OpenRA.FileFormats
length--;
}
else
f.Image[k++] = b;
f.Data[k++] = b;
}
}
}

View File

@@ -1,56 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2011 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.Collections.Generic;
using System.IO;
using System.Linq;
namespace OpenRA.FileFormats.Graphics
{
// format80-only SHP writer
public static class ShpWriter
{
public static void Write(Stream s, int width, int height, IEnumerable<byte[]> frames)
{
var compressedFrames = frames.Select(f => Format80.Encode(f)).ToArray();
// note: end-of-file and all-zeroes headers
var dataOffset = 14 + (compressedFrames.Length + 2) * ImageHeader.SizeOnDisk;
using (var bw = new BinaryWriter(s))
{
bw.Write((ushort)compressedFrames.Length);
bw.Write((ushort)0); // unused
bw.Write((ushort)0); // unused
bw.Write((ushort)width);
bw.Write((ushort)height);
bw.Write((uint)0); // unused
foreach (var f in compressedFrames)
{
var ih = new ImageHeader { Format = Format.Format80, Offset = (uint)dataOffset };
dataOffset += f.Length;
ih.WriteTo(bw);
}
var eof = new ImageHeader { Offset = (uint)dataOffset };
eof.WriteTo(bw);
var allZeroes = new ImageHeader { };
allZeroes.WriteTo(bw);
foreach (var f in compressedFrames)
bw.Write(f);
}
}
}
}

View File

@@ -0,0 +1,188 @@
#region Copyright & License Information
/*
* Copyright 2007-2011 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.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
namespace OpenRA.FileFormats
{
public interface ISpriteFrame
{
Size Size { get; }
Size FrameSize { get; }
float2 Offset { get; }
byte[] Data { get; }
}
public interface ISpriteSource
{
IEnumerable<ISpriteFrame> Frames { get; }
}
public enum SpriteType { Unknown, ShpTD, ShpTS, TmpTD, TmpRA, R8 }
public static class SpriteSource
{
static bool IsTmpRA(Stream s)
{
var start = s.Position;
s.Position += 20;
var a = s.ReadUInt32();
s.Position += 2;
var b = s.ReadUInt16();
s.Position = start;
return a == 0 && b == 0x2c73;
}
static bool IsTmpTD(Stream s)
{
var start = s.Position;
s.Position += 16;
var a = s.ReadUInt32();
var b = s.ReadUInt32();
s.Position = start;
return a == 0 && b == 0x0D1AFFFF;
}
static bool IsShpTS(Stream s)
{
var start = s.Position;
// First word is zero
if (s.ReadUInt16() != 0)
{
s.Position = start;
return false;
}
// Sanity Check the image count
s.Position += 4;
var imageCount = s.ReadUInt16();
if (s.Position + 24 * imageCount > s.Length)
{
s.Position = start;
return false;
}
// Check the size and format flag
// Some files define bogus frames, so loop until we find a valid one
s.Position += 4;
ushort w, h, f = 0;
byte type;
do
{
w = s.ReadUInt16();
h = s.ReadUInt16();
type = s.ReadUInt8();
}
while (w == 0 && h == 0 && f++ < imageCount);
s.Position = start;
return type < 4;
}
static bool IsShpTD(Stream s)
{
var start = s.Position;
// First word is the image count
var imageCount = s.ReadUInt16();
if (imageCount == 0)
{
s.Position = start;
return false;
}
// Last offset should point to the end of file
var finalOffset = start + 14 + 8 * imageCount;
if (finalOffset > s.Length)
{
s.Position = start;
return false;
}
s.Position = finalOffset;
var eof = s.ReadUInt32();
if (eof != s.Length)
{
s.Position = start;
return false;
}
// Check the format flag on the first frame
s.Position = start + 17;
var b = s.ReadUInt8();
s.Position = start;
return b == 0x20 || b == 0x40 || b == 0x80;
}
static bool IsR8(Stream s)
{
var start = s.Position;
// First byte is nonzero
if (s.ReadUInt8() == 0)
{
s.Position = start;
return false;
}
// Check the format of the first frame
s.Position = start + 25;
var d = s.ReadUInt8();
s.Position = start;
return d == 8;
}
public static SpriteType DetectSpriteType(Stream s)
{
if (IsShpTD(s))
return SpriteType.ShpTD;
if (IsShpTS(s))
return SpriteType.ShpTS;
if (IsR8(s))
return SpriteType.R8;
if (IsTmpRA(s))
return SpriteType.TmpRA;
if (IsTmpTD(s))
return SpriteType.TmpTD;
return SpriteType.Unknown;
}
public static ISpriteSource LoadSpriteSource(Stream s, string filename)
{
var type = DetectSpriteType(s);
switch (type)
{
case SpriteType.ShpTD:
return new ShpReader(s);
case SpriteType.ShpTS:
return new ShpTSReader(s);
case SpriteType.R8:
return new R8Reader(s);
case SpriteType.Unknown:
default:
throw new InvalidDataException(filename + " is not a valid sprite file");
}
}
}
}

View File

@@ -21,14 +21,13 @@ namespace OpenRA.FileFormats
Dictionary<ushort, List<byte[]>> templates;
public Size TileSize;
List<byte[]> LoadTemplate(string filename, string[] exts, Cache<string, R8Reader> r8cache, int[] frames)
List<byte[]> LoadTemplate(string filename, string[] exts, Cache<string, ISpriteFrame[]> r8cache, int[] frames)
{
if (exts.Contains(".R8") && FileSystem.Exists(filename + ".R8"))
{
var data = new List<byte[]>();
foreach (var f in frames)
data.Add(f >= 0 ? r8cache[filename][f].Image : null);
data.Add(f >= 0 ? r8cache[filename][f].Data : null);
return data;
}
@@ -43,7 +42,7 @@ namespace OpenRA.FileFormats
this.TileSize = tileSize;
templates = new Dictionary<ushort, List<byte[]>>();
var r8cache = new Cache<string, R8Reader>(s => new R8Reader(FileSystem.OpenWithExts(s, ".R8")));
var r8cache = new Cache<string, ISpriteFrame[]>(s => new R8Reader(FileSystem.OpenWithExts(s, ".R8")).Frames.ToArray());
foreach (var t in TileSet.Templates)
templates.Add(t.Key, LoadTemplate(t.Value.Image, tileset.Extensions, r8cache, t.Value.Frames));
}

View File

@@ -95,7 +95,6 @@
<Compile Include="Graphics\IInputHandler.cs" />
<Compile Include="Graphics\PngLoader.cs" />
<Compile Include="Graphics\ShpReader.cs" />
<Compile Include="Graphics\ShpWriter.cs" />
<Compile Include="Graphics\Vertex.cs" />
<Compile Include="Graphics\VqaReader.cs" />
<Compile Include="InstallUtils.cs" />
@@ -153,6 +152,7 @@
<Compile Include="FileSystem\MixFile.cs" />
<Compile Include="FileSystem\ZipFile.cs" />
<Compile Include="FileSystem\D2kSoundResources.cs" />
<Compile Include="Graphics\SpriteSource.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">