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