Support for Dune II SHP files used for cursors in Red Alert.

This commit is contained in:
Matthew Bowra-Dean
2009-10-12 09:45:15 +13:00
parent 99b508956e
commit 99ad09ddc2
7 changed files with 475 additions and 221 deletions

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.IO;
using System.Drawing;
namespace OpenRa.FileFormats
{
public enum Dune2ImageFlags : int
{
F80_F2 = 0,
F2 = 2,
L16_F80_F2_1 = 1,
L16_F80_F2_2 = 3,
Ln_F80_F2 = 5
}
public class Dune2ImageHeader
{
public readonly Dune2ImageFlags Flags;
public readonly int Width;
public readonly int Height;
public readonly int Slices;
public readonly int FileSize;
public readonly int DataSize;
public readonly byte[] LookupTable;
public byte[] Image;
public Dune2ImageHeader(BinaryReader reader)
{
Flags = (Dune2ImageFlags)reader.ReadUInt16();
Slices = reader.ReadByte();
Width = reader.ReadUInt16();
Height = reader.ReadByte();
FileSize = reader.ReadUInt16();
DataSize = reader.ReadUInt16();
if (Flags == Dune2ImageFlags.L16_F80_F2_1 ||
Flags == Dune2ImageFlags.L16_F80_F2_2 ||
Flags == Dune2ImageFlags.Ln_F80_F2)
{
int n = Flags == Dune2ImageFlags.Ln_F80_F2 ? reader.ReadByte() : 16;
LookupTable = new byte[n];
for (int i = 0; i < n; i++)
LookupTable[i] = reader.ReadByte();
}
}
public Size Size
{
get { return new Size(Width, Height); }
}
}
public class Dune2ShpReader : IEnumerable<Dune2ImageHeader>
{
public readonly int ImageCount;
List<Dune2ImageHeader> headers = new List<Dune2ImageHeader>();
public Dune2ShpReader(Stream stream)
{
BinaryReader reader = new BinaryReader(stream);
ImageCount = reader.ReadUInt16();
//Last offset is pointer to end of file.
uint[] offsets = new uint[ImageCount + 1];
uint temp = reader.ReadUInt32();
//If fourth byte in file is non-zero, the offsets are two bytes each.
bool twoByteOffsets = (temp & 0xFF0000) > 0;
if (twoByteOffsets)
{
offsets[0] = ((temp & 0xFFFF0000) >> 16) + 2; //Offset does not account for image count bytes
offsets[1] = (temp & 0xFFFF) + 2;
}
else
offsets[0] = temp + 2;
for (int i = twoByteOffsets ? 2 : 1; i < ImageCount + 1; i++)
offsets[i] = (twoByteOffsets ? reader.ReadUInt16() : reader.ReadUInt32()) + 2;
for (int i = 0; i < ImageCount; i++)
{
reader.BaseStream.Seek(offsets[i], SeekOrigin.Begin);
Dune2ImageHeader header = new Dune2ImageHeader(reader);
byte[] imgData = reader.ReadBytes(header.FileSize);
header.Image = new byte[header.Height * header.Width];
//Decode image data
if (header.Flags != Dune2ImageFlags.F2)
{
byte[] tempData = new byte[header.DataSize];
Format80.DecodeInto(imgData, tempData);
Format2.DecodeInto(tempData, header.Image);
}
else
Format2.DecodeInto(imgData, header.Image);
//Lookup values in lookup table
if (header.LookupTable != null)
for (int j = 0; j < header.Image.Length; j++)
header.Image[j] = header.LookupTable[header.Image[j]];
headers.Add(header);
}
}
public Dune2ImageHeader this[int index]
{
get { return headers[index]; }
}
public IEnumerator<Dune2ImageHeader> GetEnumerator()
{
return headers.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenRa.FileFormats
{
public static class Format2
{
public static int DecodeInto(byte[] src, byte[] dest)
{
FastByteReader r = new FastByteReader(src);
int i = 0;
while (!r.Done())
{
byte cmd = r.ReadByte();
if (cmd == 0)
{
byte count = r.ReadByte();
while (count-- > 0)
dest[i++] = 0;
}
else
dest[i++] = cmd;
}
return i;
}
}
}

View File

@@ -46,8 +46,10 @@
<ItemGroup> <ItemGroup>
<Compile Include="Blowfish.cs" /> <Compile Include="Blowfish.cs" />
<Compile Include="BlowfishKeyProvider.cs" /> <Compile Include="BlowfishKeyProvider.cs" />
<Compile Include="Dune2ShpReader.cs" />
<Compile Include="FileSystem.cs" /> <Compile Include="FileSystem.cs" />
<Compile Include="Folder.cs" /> <Compile Include="Folder.cs" />
<Compile Include="Format2.cs" />
<Compile Include="Format40.cs" /> <Compile Include="Format40.cs" />
<Compile Include="Format80.cs" /> <Compile Include="Format80.cs" />
<Compile Include="IniFile.cs" /> <Compile Include="IniFile.cs" />

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRa.FileFormats;
namespace OpenRa.Game.Graphics
{
static class CursorSheetBuilder
{
static Dictionary<string, Sprite[]> cursors =
new Dictionary<string, Sprite[]>();
public static Sprite LoadSprite(string filename, params string[] exts)
{
return LoadAllSprites(filename, exts)[0];
}
public static Sprite[] LoadAllSprites(string filename, params string[] exts)
{
Sprite[] value;
if (!cursors.TryGetValue(filename, out value))
{
Dune2ShpReader shp = new Dune2ShpReader(FileSystem.OpenWithExts(filename, exts));
value = new Sprite[shp.ImageCount];
for (int i = 0; i < shp.ImageCount; i++)
value[i] = SheetBuilder.Add(shp[i].Image, shp[i].Size);
cursors.Add(filename, value);
}
return value;
}
}
}

View File

@@ -77,6 +77,7 @@
<Compile Include="GameRules\UnitInfo.cs" /> <Compile Include="GameRules\UnitInfo.cs" />
<Compile Include="Graphics\Animation.cs" /> <Compile Include="Graphics\Animation.cs" />
<Compile Include="Game.cs" /> <Compile Include="Game.cs" />
<Compile Include="Graphics\CursorSheetBuilder.cs" />
<Compile Include="Graphics\LineRenderer.cs" /> <Compile Include="Graphics\LineRenderer.cs" />
<Compile Include="Graphics\OverlayRenderer.cs" /> <Compile Include="Graphics\OverlayRenderer.cs" />
<Compile Include="Graphics\WorldRenderer.cs" /> <Compile Include="Graphics\WorldRenderer.cs" />

View File

@@ -1,222 +1,222 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using OpenRa.FileFormats; using OpenRa.FileFormats;
using OpenRa.Game.Graphics; using OpenRa.Game.Graphics;
using OpenRa.TechTree; using OpenRa.TechTree;
using System.Linq; using System.Linq;
namespace OpenRa.Game namespace OpenRa.Game
{ {
using GRegion = OpenRa.Game.Graphics.Region; using GRegion = OpenRa.Game.Graphics.Region;
class Sidebar class Sidebar
{ {
TechTree.TechTree techTree; TechTree.TechTree techTree;
SpriteRenderer spriteRenderer, clockRenderer; SpriteRenderer spriteRenderer, clockRenderer;
Sprite blank; Sprite blank;
Game game; Game game;
readonly GRegion region; readonly GRegion region;
public GRegion Region { get { return region; } } public GRegion Region { get { return region; } }
public float Width { get { return spriteWidth * 2; } } public float Width { get { return spriteWidth * 2; } }
Dictionary<string, Sprite> sprites = new Dictionary<string,Sprite>(); Dictionary<string, Sprite> sprites = new Dictionary<string,Sprite>();
const int spriteWidth = 64, spriteHeight = 48; const int spriteWidth = 64, spriteHeight = 48;
static string[] groups = new string[] { "building", "vehicle", "boat", "infantry", "plane" }; static string[] groups = new string[] { "building", "vehicle", "boat", "infantry", "plane" };
Dictionary<string, string> itemGroups = new Dictionary<string,string>(); //item->group Dictionary<string, string> itemGroups = new Dictionary<string,string>(); //item->group
Dictionary<string, Animation> clockAnimations = new Dictionary<string,Animation>(); //group->clockAnimation Dictionary<string, Animation> clockAnimations = new Dictionary<string,Animation>(); //group->clockAnimation
Dictionary<string, SidebarItem> selectedItems = new Dictionary<string,SidebarItem>(); //group->selectedItem Dictionary<string, SidebarItem> selectedItems = new Dictionary<string,SidebarItem>(); //group->selectedItem
List<SidebarItem> items = new List<SidebarItem>(); List<SidebarItem> items = new List<SidebarItem>();
public Sidebar( Renderer renderer, Game game ) public Sidebar( Renderer renderer, Game game )
{ {
this.techTree = game.LocalPlayer.TechTree; this.techTree = game.LocalPlayer.TechTree;
this.techTree.BuildableItemsChanged += PopulateItemList; this.techTree.BuildableItemsChanged += PopulateItemList;
this.game = game; this.game = game;
region = GRegion.Create(game.viewport, DockStyle.Right, 128, Paint, MouseHandler); region = GRegion.Create(game.viewport, DockStyle.Right, 128, Paint, MouseHandler);
game.viewport.AddRegion( region ); game.viewport.AddRegion( region );
spriteRenderer = new SpriteRenderer(renderer, false); spriteRenderer = new SpriteRenderer(renderer, false);
clockRenderer = new SpriteRenderer(renderer, true); clockRenderer = new SpriteRenderer(renderer, true);
LoadSprites("buildings.txt"); LoadSprites("buildings.txt");
LoadSprites("vehicles.txt"); LoadSprites("vehicles.txt");
LoadSprites("infantry.txt"); LoadSprites("infantry.txt");
foreach (string s in groups) foreach (string s in groups)
{ {
clockAnimations.Add(s, new Animation("clock")); clockAnimations.Add(s, new Animation("clock"));
clockAnimations[s].PlayRepeating("idle"); clockAnimations[s].PlayRepeating("idle");
selectedItems.Add(s, null); selectedItems.Add(s, null);
} }
blank = SheetBuilder.Add(new Size((int)spriteWidth, (int)spriteHeight), 16); blank = SheetBuilder.Add(new Size((int)spriteWidth, (int)spriteHeight), 16);
} }
public void Build(SidebarItem item) public void Build(SidebarItem item)
{ {
if (item != null) if (item != null)
game.controller.orderGenerator = new PlaceBuilding(game.LocalPlayer, item.techTreeItem.tag.ToLowerInvariant()); game.controller.orderGenerator = new PlaceBuilding(game.LocalPlayer, item.techTreeItem.tag.ToLowerInvariant());
} }
void LoadSprites(string filename) void LoadSprites(string filename)
{ {
foreach (string line in Util.ReadAllLines(FileSystem.Open(filename))) foreach (string line in Util.ReadAllLines(FileSystem.Open(filename)))
{ {
string key = line.Substring(0, line.IndexOf(',')); string key = line.Substring(0, line.IndexOf(','));
int secondComma = line.IndexOf(',', line.IndexOf(',') + 1); int secondComma = line.IndexOf(',', line.IndexOf(',') + 1);
string group = line.Substring(secondComma + 1, line.Length - secondComma - 1); string group = line.Substring(secondComma + 1, line.Length - secondComma - 1);
sprites.Add( key, SpriteSheetBuilder.LoadSprite( key + "icon", ".shp" ) ); sprites.Add( key, SpriteSheetBuilder.LoadSprite( key + "icon", ".shp" ) );
itemGroups.Add(key, group); itemGroups.Add(key, group);
} }
} }
void DrawSprite(Sprite s, ref float2 p) void DrawSprite(Sprite s, ref float2 p)
{ {
spriteRenderer.DrawSprite(s, p, 0); spriteRenderer.DrawSprite(s, p, 0);
p.Y += spriteHeight; p.Y += spriteHeight;
} }
void Fill(float height, float2 p) void Fill(float height, float2 p)
{ {
while (p.Y < height) while (p.Y < height)
DrawSprite(blank, ref p); DrawSprite(blank, ref p);
} }
int buildPos = 0; int buildPos = 0;
int unitPos = 0; int unitPos = 0;
void PopulateItemList() void PopulateItemList()
{ {
buildPos = 0; unitPos = 0; buildPos = 0; unitPos = 0;
items.Clear(); items.Clear();
foreach (Item i in techTree.BuildableItems) foreach (Item i in techTree.BuildableItems)
{ {
Sprite sprite; Sprite sprite;
if (!sprites.TryGetValue(i.tag, out sprite)) continue; if (!sprites.TryGetValue(i.tag, out sprite)) continue;
items.Add(new SidebarItem(sprite, i, i.IsStructure ? buildPos : unitPos)); items.Add(new SidebarItem(sprite, i, i.IsStructure ? buildPos : unitPos));
if (i.IsStructure) if (i.IsStructure)
buildPos += spriteHeight; buildPos += spriteHeight;
else else
unitPos += spriteHeight; unitPos += spriteHeight;
} }
foreach (string g in groups) selectedItems[g] = null; foreach (string g in groups) selectedItems[g] = null;
} }
void Paint() void Paint()
{ {
foreach (SidebarItem i in items) foreach (SidebarItem i in items)
i.Paint(spriteRenderer, region.Location); i.Paint(spriteRenderer, region.Location);
Fill(region.Size.Y + region.Location.Y, new float2(region.Location.X, buildPos + region.Location.Y)); Fill(region.Size.Y + region.Location.Y, new float2(region.Location.X, buildPos + region.Location.Y));
Fill(region.Size.Y + region.Location.Y, new float2(region.Location.X + spriteWidth, unitPos + region.Location.Y)); Fill(region.Size.Y + region.Location.Y, new float2(region.Location.X + spriteWidth, unitPos + region.Location.Y));
spriteRenderer.Flush(); spriteRenderer.Flush();
foreach (var kvp in selectedItems) foreach (var kvp in selectedItems)
{ {
if (kvp.Value != null) if (kvp.Value != null)
{ {
clockRenderer.DrawSprite(clockAnimations[kvp.Key].Image, region.Location.ToFloat2() + kvp.Value.location, 0); clockRenderer.DrawSprite(clockAnimations[kvp.Key].Image, region.Location.ToFloat2() + kvp.Value.location, 0);
clockAnimations[kvp.Key].Tick(1); clockAnimations[kvp.Key].Tick(1);
} }
} }
clockRenderer.Flush(); clockRenderer.Flush();
} }
public SidebarItem GetItem(float2 point) public SidebarItem GetItem(float2 point)
{ {
foreach (SidebarItem i in items) foreach (SidebarItem i in items)
if (i.Clicked(point)) if (i.Clicked(point))
return i; return i;
return null; return null;
} }
void MouseHandler(MouseInput mi) void MouseHandler(MouseInput mi)
{ {
if (mi.Button == MouseButtons.Left && mi.Event == MouseInputEvent.Down) if (mi.Button == MouseButtons.Left && mi.Event == MouseInputEvent.Down)
{ {
var point = new float2(mi.Location.X, mi.Location.Y); var point = new float2(mi.Location.X, mi.Location.Y);
var item = GetItem(point); var item = GetItem(point);
if (item != null) if (item != null)
{ {
string group = itemGroups[item.techTreeItem.tag]; string group = itemGroups[item.techTreeItem.tag];
if (selectedItems[group] == null) if (selectedItems[group] == null)
{ {
selectedItems[group] = item; selectedItems[group] = item;
Build(item); Build(item);
} }
} }
} }
else if( mi.Button == MouseButtons.Right && mi.Event == MouseInputEvent.Down ) else if( mi.Button == MouseButtons.Right && mi.Event == MouseInputEvent.Down )
{ {
var point = new float2(mi.Location.X, mi.Location.Y); var point = new float2(mi.Location.X, mi.Location.Y);
var item = GetItem(point); var item = GetItem(point);
if( item != null ) if( item != null )
{ {
string group = itemGroups[ item.techTreeItem.tag ]; string group = itemGroups[ item.techTreeItem.tag ];
selectedItems[ group ] = null; selectedItems[ group ] = null;
} }
} }
} }
} }
class PlaceBuilding : IOrderGenerator class PlaceBuilding : IOrderGenerator
{ {
public readonly Player Owner; public readonly Player Owner;
public readonly string Name; public readonly string Name;
public PlaceBuilding( Player owner, string name ) public PlaceBuilding( Player owner, string name )
{ {
Owner = owner; Owner = owner;
Name = name; Name = name;
} }
public IEnumerable<Order> Order( Game game, int2 xy ) public IEnumerable<Order> Order( Game game, int2 xy )
{ {
// todo: check that space is free // todo: check that space is free
yield return new PlaceBuildingOrder( this, xy ); yield return new PlaceBuildingOrder( this, xy );
} }
public void PrepareOverlay(Game game, int2 xy) public void PrepareOverlay(Game game, int2 xy)
{ {
game.worldRenderer.uiOverlay.SetCurrentOverlay(xy, Name); game.worldRenderer.uiOverlay.SetCurrentOverlay(xy, Name);
} }
} }
class PlaceBuildingOrder : Order class PlaceBuildingOrder : Order
{ {
PlaceBuilding building; PlaceBuilding building;
int2 xy; int2 xy;
public PlaceBuildingOrder(PlaceBuilding building, int2 xy) public PlaceBuildingOrder(PlaceBuilding building, int2 xy)
{ {
this.building = building; this.building = building;
this.xy = xy; this.xy = xy;
} }
public override void Apply(Game game) public override void Apply(Game game)
{ {
game.world.AddFrameEndTask(_ => game.world.AddFrameEndTask(_ =>
{ {
Log.Write( "Player \"{0}\" builds {1}", building.Owner.PlayerName, building.Name ); Log.Write( "Player \"{0}\" builds {1}", building.Owner.PlayerName, building.Name );
game.world.Add( new Actor( building.Name, xy, building.Owner ) ); game.world.Add( new Actor( building.Name, xy, building.Owner ) );
game.controller.orderGenerator = null; game.controller.orderGenerator = null;
game.worldRenderer.uiOverlay.KillOverlay(); game.worldRenderer.uiOverlay.KillOverlay();
}); });
} }
} }
} }

57
doc/shp dune 2.txt Normal file
View File

@@ -0,0 +1,57 @@
======================
Dune 2 SHP file format
======================
Sourced from Red Horizon Utilities by Emanuel Rabina
http://www.ultraq.net.nz/redhorizon/
The Dune 2 SHP file, is a multiple image filetype, where each image can have
it's own set of dimensions. The file is structured as follows:
File header:
NumImages (2 bytes) - the number of images in the file
Offsets[NumImages + 1] - offset to the image header for an image. The last
(2 or 4 bytes each) offset points to the end of the file. The offsets
don't take into account the NumImages bytes at the
beginning, so add 2 bytes to the offset value to
get the actual position of an image header in the
file
The size of the offsets can be either 2 or 4 bytes. There is no simple way
to determine which it will be, but checking the file's 4th byte to see if
it's 0, seems to be a commonly accepted practice amongst existing Dune 2 SHP
file readers:
eg: A 2-byte offset file: 01 00 06 00 EC 00 45 0A ...
A 4-byte offset file: 01 00 08 00 00 00 EC 00 ...
^^
The marked byte will be 0 in 4-byte offset files, non 0 in 2-byte offset
files.
Lastly, like C&C SHP files, there is an extra offset, pointing to the end of
the file (or what would have been the position of another image header/data
pair).
Following the file header, are a series of image header & image data pairs.
The image header is structured as follows:
Image header:
Flags (2 bytes) - flags to identify the type of data following the
Datasize field, and/or the compression schemes used
Slices (1 byte) - number of Format2 slices used to encode the image
data. Often this is the same as the height of the
image
Width (2 bytes) - width of the image
Height (1 byte) - height of the image
Filesize (2 bytes) - size of the image data in the file
Datasize (2 bytes) - size of the image data when Format2 encoded/compressed
Regarding the flags, there seem to be 4 values:
- 00000000 (0) = Decompress with Format80, then Format2
- 00000001 (1) = (see 3)
- 00000010 (2) = Decompress with Format2
- 00000011 (3) = A small 16-byte lookup table follows, and the image data
should be decompressed with Format80 then Format2.
- 00000101 (5) = The first byte gives the size of the lookup table that
follows, and the image data should be decompressed with
Format80 then Format2.
And after this image header is the image data.