/*
# Soma.Sprite DLL
# Myth of Soma: Sword of the Lion
# http://www.swordofthelion.net
# ----------------------------------------------------
# File: Sprite.cs
# Version: 3.0.0 21/05/2010
# Date(s): 06/03/2009 Original (C++)
# 24/08/2009 Rewritten (C++)
# 14/04/2010 Rewritten (C#)
# Author(s): Matthew Bowen
#
# Contains the Sprite class which is used to load
# and manage Myth of Soma game sprites. Soma sprites
# are held in SPL, MRF and OBM file-types.
# ----------------------------------------------------
*/
using System;
using System.Drawing; // Bitmaps
using System.IO; // File IO
using System.Security.Permissions; // File Read/Write permissions
using System.Text; // Strings from byte arrays
// Disable warning of unused variables.
// Used to supress the warning of unused exception variables.
#pragma warning disable 168
namespace Soma
{
// XNA also defines Color - specify which we use to avoid ambiguity
using Color = System.Drawing.Color;
/// <summary>
/// Represents a Myth of Soma game sprite.
/// </summary>
public class Sprite
{
#region Constants
private const int MaxPaletteSize = 256;
private readonly Color DefaultTransparency = Color.FromArgb(0xf8, 0x00, 0xf8);
#endregion
#region Structs
/// <summary>
/// Represents the header of a Myth of Soma SPL file.
/// </summary>
private struct SplHeader
{
// NOTE: TO ADD: color array of palette data
// Size of all SPL header structs in bytes (as contained in an SPL file)
public const int StructSize = 148;
private string type; // SPLN or SPL8
private string notes; // Can be used to store a short note on the file
private Color transparency; // Colour to use as transparency
private uint frameQty; // Number of frames in sprite
// Maximum length for the notes string
// (this value differs from Soma as I have combined szRemark with szBMPFN
// as the latter is unnecessary)
private const ushort maxNotesLength = 128;
/// <summary>
/// Creates a new SPL header.
/// </summary>
/// <param name="splType">Either "SPLN" (uncompressed) or "SPL8" (compressed).</param>
/// <param name="splNotes">Optional notes on the file.</param>
/// <param name="ck">Colour key representing transparency.</param>
/// <param name="frames">Number of frames in the sprite.</param>
public SplHeader(string splType, string splNotes, Color ck, uint frames)
{
type = splType;
notes = splNotes;
transparency = ck;
frameQty = frames;
}
/// <summary>
/// Gets or sets the SPL's type - use either SPLN or SPL8.
/// </summary>
public string Type
{
get { return type; }
set
{
// Only two types of SPL accepted
if (value == "SPLN" || value == "SPL8")
type = value;
}
}
/// <summary>
/// Gets a value indicating whether the SPL is compressed (is a SPL8 file).
/// </summary>
public bool IsCompressed
{
get { return (type == "SPL8"); }
}
/// <summary>
/// Gets or sets the short 128-character note on the file.
/// </summary>
public string Notes
{
get { return notes; }
set
{
if (value.Length <= maxNotesLength)
notes = value;
}
}
/// <summary>
/// Gets or sets the colour to be used as the transparency colour.
/// </summary>
public Color ColourKey
{
get { return transparency; }
set { transparency = value; }
}
/// <summary>
/// Gets a value representing the number of frames in this SPL.
/// </summary>
public uint Frames
{
get { return frameQty; }
}
}
/// <summary>
/// Represents the header of a Myth of Soma SPL frame.
/// </summary>
public struct SplFrameHeader
{
// Size of all SPL frame header structs in bytes (as contained in an SPL file)
public const int StructSize = 28;
// Size of the frame data in the SPL file in bytes
private int size;
// Area in pixels occupied by the frame
// Left and Top store the offsets used to draw this frame relative to the location
// on the screen where this frame is to be drawn (used so that frames of different
// dimensions animate smoothly with the image appearing to be drawn in the same place)
private Rectangle area;
/// <summary>
/// Creates a new SPL frame header.
/// </summary>
/// <param name="rect">Rectangle representing the area used by the frame.</param>
/// <param name="dataSize">Size of the frame data in the SPL file in bytes.</param>
public SplFrameHeader(Rectangle rect, int dataSize)
{
area = rect;
size = dataSize;
}
/// <summary>
/// Gets a Rectangle object representing the area occupied by the frame.
/// </summary>
public Rectangle Area
{
get
{
return area;
}
}
/// <summary>
/// Gets a value representing the number of bytes required to store this frame in the SPL.
/// </summary>
public int DataSize
{
get { return size; }
}
}
/// <summary>
/// Represents a single sprite frame.
/// </summary>
public struct Frame
{
private SplFrameHeader header;
private byte[] data; // Frame's image data read from file
private IntPtr texture; // Pointer to DirectX texture
/// <summary>
/// Gets this frame's DirectX texture
/// </summary>
public SplFrameHeader Header
{
get { return header; }
}
/// <summary>
/// Gets or sets the pointer (LPDIRECT3DTEXTURE9) to the DirectX texture for this frame.
/// </summary>
public IntPtr Texture
{
get { return texture; }
set { texture = value; }
}
/// <summary>
/// Gets or sets the frame's image data as read from the file.
/// </summary>
public byte[] ImageData
{
get { return data; }
set { data = value; }
}
/// <summary>
/// Gets a value representing the total number of pixels in the frame (width * height).
/// </summary>
public int TotalPixels
{
get { return (header.Area.Width * header.Area.Height); }
}
/// <summary>
/// Creates a new sprite frame.
/// </summary>
/// <param name="frameHeader">Header data of the frame</param>
public Frame(SplFrameHeader frameHeader)
{
header = frameHeader;
data = null;
texture = IntPtr.Zero;
}
/*/// <summary>
/// Saves the frame as a bitmap file.
/// </summary>
/// <param name="filePath">A string the contains the name of the file to save this frame as.</param>
/// <param name="pxFormat">The pixel format of the bitmap to be saved.
/// Generally, for SPLN use Format24bppRgb and for SPL8 use Format16bppRgb565.</param>
public void Save(string filePath, System.Drawing.Imaging.PixelFormat pxFormat)
{
// The bitmap will have had its width and height increased to be powers of 2
// - we want to crop out only the image we are interested in
Rectangle cropArea = new Rectangle(0, 0, header.Area.Width, header.Area.Height);
Bitmap bmp = this.Bitmap.Clone(cropArea, pxFormat);
bmp.Save(filePath);
}*/
}
#endregion
#region Fields
private bool isLoaded = false; // True after loading a sprite
private string error; // Used to store a description of the last error
private SplHeader splHeader;
private Color[] palette;
private Frame[] frames;
/// <summary>
/// Gets a message detailing the last failed operation.
/// </summary>
public string Error
{
get { return error; }
}
/// <summary>
/// Gets a value indicating whether the object holds sprite data.
/// </summary>
public bool IsLoaded
{
get { return isLoaded; }
}
/// <summary>
/// Gets the array of colours used by the sprite. Only used with SPL8.
/// </summary>
public Color[] Palette
{
get { return palette; }
}
/// <summary>
/// Gets the number of frames the sprite contains.
/// </summary>
public int FrameCount
{
get { return (int)splHeader.Frames; }
}
/// <summary>
/// Gets a Frame object containing the image and data of a given frame in the sprite.
/// </summary>
/// <param name="index">ID of the frame to return.</param>
/// <returns>A Frame object containing the data held on a given frame.</returns>
public Frame this[int index]
{
get
{
if (index >= 0 && index < splHeader.Frames)
return frames[index];
else
{
error = "Frame index " + index + " does not exist.";
return new Frame();
}
}
}
#endregion
#region Constructors/Destructors
/// <summary>
/// Creates an empty sprite object.
/// </summary>
public Sprite() { }
/// <summary>
/// Creates a sprite object from the given file path.
/// </summary>
/// <param name="d3d">Direct3D instance to use for textures.</param>
/// <param name="filePath">String containing complete file path of the sprite to load.</param>
public Sprite(ref Soma.DirectX.Direct3D d3d, string filePath)
{
Load(ref d3d, filePath);
}
/// <summary>
/// Releases all memory held by the sprite.
/// </summary>
public void Release()
{
if (!IsLoaded)
return;
// Release from memory all textures held
for (int i = 0; i < splHeader.Frames; i++)
{
if (frames[i].Texture != IntPtr.Zero)
Soma.DirectX.Direct3D.ReleaseTexture(frames[i].Texture);
}
isLoaded = false;
}
/// <summary>
/// Destructor to make sure textures are released from memory.
/// </summary>
~Sprite()
{
Release();
}
#endregion
#region Public methods
/// <summary>
/// Loads a sprite from a file.
/// </summary>
/// <param name="d3d">Direct3D instance to use for textures.</param>
/// <param name="filePath">Full path to the file to load.</param>
/// <returns>True on operation success, false on failure.</returns>
public bool Load(ref Soma.DirectX.Direct3D d3d, string filePath)
{
isLoaded = false;
byte[] fileData; // Byte array to store the file
try
{
// Check for write-access in-case we don't have it as we will want to edit the file later.
// Note: file-access permissions may change at any time - the best we can do is check we
// have write-access now and offer a Save As option later.
new FileIOPermission(FileIOPermissionAccess.Write, filePath);
// Read the file into a byte array
fileData = File.ReadAllBytes(filePath);
}
catch (UnauthorizedAccessException ex)
{
error = "Read/write access to sprite file is denied. (" + filePath + ")";
return false;
}
catch (FileNotFoundException ex)
{
error = "Sprite file could not be found at the specified location. (" + filePath + ")";
return false;
}
catch (Exception ex)
{
// For any other exceptions that may occur attempting to read from file
error = "An error occured while opening sprite file. (" + filePath + ")";
return false;
}
// Get first 3 bytes - should be either SPL or BMP
string fileType = Encoding.UTF8.GetString(fileData, 0, 3);
switch (fileType)
{
case "SPL":
if (LoadSPL(ref fileData))
{
GenerateTextures(ref d3d);
return true;
}
else return false;
case "BMP":
return LoadBMP(ref fileData);
default:
error = "Unrecognized sprite type.";
return false;
}
// Unreachable
}
/// <summary>
/// Creates/regenerates the textures for each frame.
/// </summary>
public void GenerateTextures(ref Soma.DirectX.Direct3D d3d)
{
if (!IsLoaded)
return;
for (int i = 0; i < splHeader.Frames; i++)
{
if (frames[i].Texture != IntPtr.Zero)
{
// Delete from memory first
Soma.DirectX.Direct3D.ReleaseTexture(frames[i].Texture);
}
frames[i].Texture = (IntPtr)d3d.CreateTextureFromSplFrame(
frames[i].Header.Area.Width, frames[i].Header.Area.Height,
frames[i].ImageData, Palette, splHeader.IsCompressed);
}
}
#endregion
#region Private methods
/// <summary>
/// Loads a Myth of Soma SPL-format sprite, such as SPL, MRF or
/// map OBM file types.
/// </summary>
/// <param name="fileData">Byte array containing entire file.</param>
/// <returns>True on load success, false on failure.</returns>
private bool LoadSPL(ref byte[] fileData)
{
// Current read-in location in the array
int offset = 0;
// "SPLN" or "SPL8"
string type = Encoding.UTF8.GetString(fileData, offset, 4);
offset += 4;
if (type != "SPLN" && type != "SPL8")
{
error = "Attempt to load an unrecognized SPL format.";
return false;
}
// 128-length char array that may contain a short note on the file
string notes = Encoding.UTF8.GetString(fileData, offset, 128);
offset += 128;
// Space intended for sprite's width and height (2 ints), but not used
offset += sizeof(uint) * 2;
// Transparency colour (possibility these are in reverse order)
byte blue = fileData[offset]; offset++;
byte green = fileData[offset]; offset++;
byte red = fileData[offset]; offset++;
offset++; // RGBQUADs have a reserved bit following the RGB value
Color transparency = Color.FromArgb((int)red, (int)green, (int)blue);
// Number of frames in sprite
int frameQty = BitConverter.ToInt32(fileData, offset);
offset += sizeof(int);
if (frameQty <= 0)
{
error = "SPL file has an invalid number of frames (" + frameQty + ").";
return false;
}
// Create new SPL header
splHeader = new SplHeader(type, notes, transparency, (uint)frameQty);
// Move offset ahead to where the data on the first frame begins
// Note that at this point offset == SplHeader.StructSize
offset = SplHeader.StructSize + (frameQty * 24);
if (splHeader.IsCompressed)
{
// Type SPL8 - read in the colour palette
offset += 4;
// Colour count located at offset 148 + (frames * 24) + 4
short colourCount = BitConverter.ToInt16(fileData, offset);
offset += sizeof(short);
// Check the number of palette colours is ok
if (colourCount <= 0 || colourCount > MaxPaletteSize)
{
error = "Sprite has an invalid number of colours in its palette ";
error += "(" + colourCount + "/" + MaxPaletteSize + ").";
return false;
}
// Add space for the transparency colour if there is room
palette = new Color[colourCount];
for (int i = 0; i < colourCount; i++)
{
// Read in the colour (RGB 565 format)
ushort colour = BitConverter.ToUInt16(fileData, offset);
offset += sizeof(ushort);
// Convert it to 32-bit (ARGB 8888 format)
palette[i] = Soma.DirectX.Direct3D.ColorFrom16Bit(colour);
}
// All SPL8s use the default transparency - make sure we use it
splHeader.ColourKey = DefaultTransparency;
}
// Create array to store all the frames
frames = new Frame[splHeader.Frames];
// For every frame in the file, store its header data
for (uint i = 0; i < splHeader.Frames; i++)
{
// Load in the area for this frame
int left = BitConverter.ToInt32(fileData, offset); offset += sizeof(int);
int top = BitConverter.ToInt32(fileData, offset); offset += sizeof(int);
int right = BitConverter.ToInt32(fileData, offset); offset += sizeof(int);
int bottom = BitConverter.ToInt32(fileData, offset); offset += sizeof(int);
Rectangle area = Rectangle.FromLTRB(left, top, right, bottom);
// The next int is unnecessary - it is the height of the frame which we already know
offset += sizeof(int);
// Size of the frame in bytes
int dataSize = BitConverter.ToInt32(fileData, offset);
offset += sizeof(int);
// Check frame data is valid
if (dataSize <= 0 || area.Width <= 0 || area.Height <= 0)
{
// File is invalid/corrupt and cannot be read from
error = "Frame " + i + " of the SPL has invalid size data or is corrupt.";
return false;
}
// Unused int here
offset += sizeof(int);
// Create a frame header from this data
SplFrameHeader header = new SplFrameHeader(area, dataSize);
// Add frame header to frame array
frames[i] = new Frame(header);
}
// For every frame, load its data
for (int i = 0; i < splHeader.Frames; i++)
{
// Copy frame's image data to Frame object
frames[i].ImageData = new byte[frames[i].Header.DataSize];
Buffer.BlockCopy(fileData, offset, frames[i].ImageData, 0, frames[i].Header.DataSize);
offset += frames[i].Header.DataSize;
}
// Success
isLoaded = true;
return true;
}
/// <summary>
/// Loads a Myth of Soma BMP-format OBM sprite, such as those containing
/// images of inventory items. Does not load actual .bmp files.
/// </summary>
/// <param name="fileData">Byte array containing entire file.</param>
/// <returns>True on operation success, false on failure.</returns>
private bool LoadBMP(ref byte[] fileData)
{
error = "Loading of OBM BMPs has not been implemented yet.";
return false;
}
/// <summary>
/// Converts the image data from an SPL frame into a Bitmap object stored in frames[frameId].
/// </summary>
/// <param name="frameId">ID of the frame to convert.</param>
/// <param name="palette">Array of colours the sprite uses if the sprite is in SPL8 format.
/// For SPLN type sprites, use null for this parameter.</param>
/// <returns>A Bitmap object containing the frame as an image.</returns>
private bool SplFrameToBmp(int frameId, Color[] palette)
{
if (frameId < 0 || frameId >= splHeader.Frames)
{
error = "Attempt to convert non-existant frame: " + frameId;
return false;
}
else if (frames[frameId].ImageData.Length <= 0)
{
error = "Attempt to convert frame " + frameId + " but its ImageData is empty.";
return false;
}
int bmpWidth, bmpHeight, bmpTotalPixels;
// The width and height of the bitmap should be powers of 2
// - not all graphics cards support textures that have dimensions that are not
// powers of 2 and result in wasted memory and stretched textures.
for (bmpWidth = 1; bmpWidth < frames[frameId].Header.Area.Width; bmpWidth *= 2) ;
for (bmpHeight = 1; bmpHeight < frames[frameId].Header.Area.Height; bmpHeight *= 2) ;
bmpTotalPixels = (bmpWidth * bmpHeight);
Bitmap bmp = new Bitmap(bmpWidth, bmpHeight, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
// Lock the bitmap's bits to edit the bitmap data directly
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat);
// Get the address of the first line of pixels
IntPtr ptrScan0 = bmpData.Scan0;
// Declare an array to hold the bytes of the bitmap pixel data
int bmpLength = bmpData.Stride * bmp.Height;
byte[] rgbValues = new byte[bmpLength];
// Copy the RGB values into the array
System.Runtime.InteropServices.Marshal.Copy(ptrScan0, rgbValues, 0, bmpLength);
int offset = 0; // Byte offset from ImageData
short nodesInRow, pixelsToSkip, pixelsInNode;
ushort colour;
// For calculating how many transparent pixels to draw before and after each node
int pxTransparentStart, pxTransparentEnd;
// Loop to assign pixels to bitmap
for (int y = 0, x = 0; y < frames[frameId].Header.Area.Height; y++, x = 0)
{
// Number of nodes on this row of the bitmap
// - a node is a consecutive line of pixel data that contains no transparent pixels
nodesInRow = BitConverter.ToInt16(frames[frameId].ImageData, offset);
offset += sizeof(short);
for (int node = 0; node < nodesInRow; node++)
{
// How many pixels to skip before the node starts
pixelsToSkip = BitConverter.ToInt16(frames[frameId].ImageData, offset);
offset += sizeof(short);
// Make all pixels up to start of the node transparent
pxTransparentStart = (y * bmpData.Stride) + (x * 3);
pxTransparentEnd = pxTransparentStart + (pixelsToSkip * 3);
for (int px = pxTransparentStart; px < pxTransparentEnd; )
{
rgbValues[px++] = splHeader.ColourKey.B;
rgbValues[px++] = splHeader.ColourKey.G;
rgbValues[px++] = splHeader.ColourKey.R;
//rgbValues[px++] = byte.MaxValue;
}
x += pixelsToSkip;
// How many pixels are in this node
pixelsInNode = BitConverter.ToInt16(frames[frameId].ImageData, offset);
offset += sizeof(short);
if ((x + pixelsInNode) > frames[frameId].Header.Area.Width)
{
// Number of pixels in this node is greater than the width of the image!
error = "Number of pixels in node " + node + " of frame " + frameId
+ " runs passed the width of the image.";
return false;
}
// Location to write pixel colour in the rgbValues array
int pxLocation = (y * bmpData.Stride) + (x * 3);
// Read in each pixel in the node
if (splHeader.IsCompressed)
{
for (int px = 0; px < pixelsInNode; px++)
{
// Colour of pixel is a reference to the palette
rgbValues[pxLocation++] = palette[frames[frameId].ImageData[offset]].B;
rgbValues[pxLocation++] = palette[frames[frameId].ImageData[offset]].G;
rgbValues[pxLocation++] = palette[frames[frameId].ImageData[offset]].R;
//rgbValues[pxLocation++] = byte.MaxValue;
offset++;
x++;
}
}
else
{
Color pixel;
for (int px = 0; px < pixelsInNode; px++)
{
// Colour of the pixel is in RGB 565 format
colour = BitConverter.ToUInt16(frames[frameId].ImageData, offset);
offset += sizeof(ushort);
// Convert it into a .NET Color
pixel = Soma.DirectX.Direct3D.ColorFrom16Bit(colour);
// Write it to array
rgbValues[pxLocation++] = pixel.B;
rgbValues[pxLocation++] = pixel.G;
rgbValues[pxLocation++] = pixel.R;
//rgbValues[pxLocation++] = byte.MaxValue;
x++;
}
}
}
// Make all pixels after the nodes on this row transparent
pxTransparentStart = (y * bmpData.Stride) + (x * 3);
pxTransparentEnd = (y + 1) * bmpData.Stride;
for (int px = pxTransparentStart; px < pxTransparentEnd; )
{
rgbValues[px++] = splHeader.ColourKey.B;
rgbValues[px++] = splHeader.ColourKey.G;
rgbValues[px++] = splHeader.ColourKey.R;
//rgbValues[px++] = byte.MaxValue;
}
}
// Copy the RGB values back to the bitmap
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptrScan0, bmpLength);
// Unlock bitmap
bmp.UnlockBits(bmpData);
// Success!
// this used to be here frames[frameId].Bitmap = bmp;
return true;
}
#endregion
}
}