/*
* GameGraphics.cs - Handles line drawn and bitmap graphics in a Level 9 game.
*
* Copyright (C) 2004 - 2011 Andreas Scherrer
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace Level9
{
///
/// Handles line drawn and bitmap graphics in a Level 9 game.
///
public class GameGraphics
{
// -- Structs, Enums -------------------------------------------------
///
/// The available graphics modes.
///
public enum GraphicsMode
{
/// No graphics.
None = 0,
/// Line drawn graphics.
LineDrawn = 1,
/// Bitmap graphics.
Bitmap = 2
}
// -- Attributes -----------------------------------------------------
///
/// The current graphics mode.
///
GraphicsMode graphicsMode = GraphicsMode.None;
///
/// A background thread for drawing line drawn graphics.
///
Thread drawingThread = null;
///
/// This property indicates whether the drawing thread should be
/// stopped.
///
bool threadStopSignal = false;
///
/// The current picture size.
///
Size pictureSize;
///
/// A pixel buffer for line drawn graphics.
///
byte[] pixels = new byte[0];
///
/// A palette for line drawn graphics. Each entry is an index in the
/// active palette.
///
int[] palette = new int[8];
///
/// This property indicates whether the title screen shows up in a
/// game with bitmap graphics.
///
bool showTitle = false;
///
/// This property indicates whether bitmaps have been detected in the
/// game directory.
///
bool bitmapsDetected = false;
///
/// The current image number in bitmap graphics mode.
///
int bitmapNumber = -1;
// -- Events ---------------------------------------------------------
///
/// Occurs when a picture has been created.
///
public event PictureCreatedEventHandler PictureCreated;
///
/// Occurs when graphics have been turned off.
///
public event GraphicsOffEventHandler GraphicsOff;
// -- Constructors ---------------------------------------------------
///
/// Initializes a new instance of the GameGraphics class.
///
public GameGraphics()
{
Interpreter.NewGameFile +=
new NewGameFileEventHandler(InterpreterNewGameFile);
Interpreter.OsInput +=
new OsInputEventHandler(InterpreterOsInput);
Interpreter.OsReadchar +=
new OsReadcharEventHandler(InterpreterOsReadchar);
Interpreter.OsGraphics +=
new OsGraphicsEventHandler(InterpreterOsGraphics);
Interpreter.OsClearGraphics +=
new OsClearGraphicsEventHandler(InterpreterOsClearGraphics);
Interpreter.OsDrawLine +=
new OsDrawLineEventHandler(InterpreterOsDrawLine);
Interpreter.OsFill +=
new OsFillEventHandler(InterpreterOsFill);
Interpreter.OsSetColour +=
new OsSetColourEventHandler(InterpreterOsSetColour);
Interpreter.OsShowBitmap +=
new OsShowBitmapEventHandler(InterpreterOsShowBitmap);
Application.ApplicationExit +=
new EventHandler(ApplicationExit);
Config.Instance.ConfigChanged +=
new ConfigChangedEventHandler(ConfigChanged);
}
// -- Event handling -------------------------------------------------
///
/// Invoked when a running game waits for user input.
///
///
/// an OsInputEventArgs that contains the event data
///
void InterpreterOsInput(OsInputEventArgs e)
{
if (graphicsMode == GraphicsMode.LineDrawn
&& drawingThread == null) {
StartDrawingThread();
}
}
///
/// Invoked when a running game waits for a key press.
///
///
/// an OsInputEventArgs that contains the event data
///
void InterpreterOsReadchar(OsReadcharEventArgs e)
{
if (graphicsMode == GraphicsMode.LineDrawn
&& drawingThread == null) {
StartDrawingThread();
}
}
///
/// Invoked when graphics are turned on or off in a running game.
///
///
/// an OsGraphicsEventArgs that contains the event data
///
void InterpreterOsGraphics(OsGraphicsEventArgs e)
{
if (e.Mode == (int)GraphicsMode.LineDrawn) {
graphicsMode = GraphicsMode.LineDrawn;
int width = 0, height = 0;
Interpreter.GetPictureSize(ref width, ref height);
pictureSize = new Size(width, height);
pixels = new byte[width * height];
}
else if (e.Mode == (int)GraphicsMode.Bitmap) {
graphicsMode = GraphicsMode.Bitmap;
if (showTitle) {
Bitmap bitmap = CreateBitmapImage(0);
if (bitmap != null) {
PictureCreated(new PictureCreatedEventArgs(bitmap));
Thread.Sleep(3000);
showTitle = false;
}
}
}
else {
graphicsMode = GraphicsMode.None;
GraphicsOff(new GraphicsOffEventArgs());
}
}
///
/// Invoked when the graphics area must be cleared in a running game.
///
///
/// an OsClearGraphicsEventArgs that contains the event data
///
void InterpreterOsClearGraphics(OsClearGraphicsEventArgs e)
{
if (graphicsMode == GraphicsMode.LineDrawn) {
if (drawingThread != null) {
StopDrawingThread();
}
Array.Clear(pixels, 0, pixels.Length);
}
}
///
/// Invoked when a palette color must be set in a running game.
///
///
/// an OsSetColourEventArgs that contains the event data
///
void InterpreterOsSetColour(OsSetColourEventArgs e)
{
palette[e.Colour] = e.Index;
}
///
/// Invoked when a line must be drawn in a running game.
///
///
/// an OsDrawLineEventArgs that contains the event data
///
void InterpreterOsDrawLine(OsDrawLineEventArgs e)
{
DrawLineIf(e.X1, e.Y1, e.X2, e.Y2, e.Colour1, e.Colour2);
}
///
/// Invoked when an area must be filled in a running game.
///
///
/// an OsFillEventArgs that contains the event data
///
void InterpreterOsFill(OsFillEventArgs e)
{
FloodFillIf(e.X, e.Y, e.Colour1, e.Colour2);
}
///
/// Invoked when a bitmap must be displayed in a running game.
///
///
/// an OsShowBitmapEventArgs that contains the event data
///
void InterpreterOsShowBitmap(OsShowBitmapEventArgs e)
{
if (!bitmapsDetected) {
return;
}
Bitmap bitmap = CreateBitmapImage(e.Number);
if (bitmap != null) {
PictureCreated(new PictureCreatedEventArgs(bitmap));
bitmapNumber = e.Number;
}
}
///
/// Invoked when a new game has been loaded.
///
///
/// a NewGameFileEventArgs that contains the event data
///
void InterpreterNewGameFile(NewGameFileEventArgs e)
{
string filePath = Interpreter.GameFilePath;
string dirPath = Path.GetDirectoryName(filePath) + "\\";
if (Interpreter.DetectBitmaps(dirPath)) {
string filename = Path.GetFileNameWithoutExtension(filePath);
if (filename != null) {
char number = filename[filename.Length - 1];
showTitle = !Char.IsDigit(number) || number == '1';
}
bitmapsDetected = true;
bitmapNumber = -1;
}
else {
showTitle = false;
bitmapsDetected = false;
}
graphicsMode = GraphicsMode.None;
}
///
/// Invoked when the application exits.
///
/// the object that originated the event
/// an EventArgs that contains the event data
void ApplicationExit(object sender, EventArgs e)
{
if (drawingThread != null) {
StopDrawingThread();
}
}
///
/// Invoked when the configuration has been changed.
///
///
/// a ConfigChangedEventArgs that contains the event data
///
void ConfigChanged(ConfigChangedEventArgs e)
{
if (graphicsMode == GraphicsMode.LineDrawn
&& drawingThread == null) {
Bitmap bitmap = CreateLineDrawnImage();
PictureCreated(new PictureCreatedEventArgs(bitmap));
}
else if (graphicsMode == GraphicsMode.Bitmap
&& bitmapNumber >= 0) {
Bitmap bitmap = CreateBitmapImage(bitmapNumber);
PictureCreated(new PictureCreatedEventArgs(bitmap));
}
}
// -- Methods --------------------------------------------------------
///
/// The thread procedure for line drawn graphics. Executes graphics
/// opcodes and notifies the listeners when a picture is ready for
/// display.
///
void DrawingThreadProc()
{
bool gfxCodeExec = false;
while(!threadStopSignal && Interpreter.RunGraphics()) {
gfxCodeExec = true;
Thread.Sleep(0);
}
if (gfxCodeExec) {
Bitmap bitmap = CreateLineDrawnImage();
PictureCreated(new PictureCreatedEventArgs(bitmap));
}
drawingThread = null;
threadStopSignal = false;
}
///
/// Starts a drawing thread in line drawn graphics mode.
///
void StartDrawingThread()
{
if (drawingThread == null) {
drawingThread = new Thread(new ThreadStart(DrawingThreadProc));
drawingThread.Start();
}
}
///
/// Stops a drawing thread in line drawn graphics mode.
///
void StopDrawingThread()
{
if (drawingThread != null) {
threadStopSignal = true;
drawingThread.Join();
}
}
///
/// Creates a scaled bitmap image for the given number.
///
/// the bitmap number
/// the bitmap, or null if an error occurred
unsafe Bitmap CreateBitmapImage(int number)
{
IntPtr l9BitmapPtr = Interpreter.DecodeBitmap(number);
if (l9BitmapPtr == IntPtr.Zero) {
return null;
}
Interpreter.L9BITMAP l9Bitmap = new Interpreter.L9BITMAP();
Marshal.PtrToStructure(l9BitmapPtr, l9Bitmap);
Bitmap bitmap = ScaleBitmap((byte *)l9Bitmap.bitmap.ToPointer(),
l9Bitmap.width, l9Bitmap.height);
if (bitmap == null) {
return null;
}
ColorPalette bmpPalette = bitmap.Palette;
int index = 0;
for (int i = 0; i < l9Bitmap.npalette * 3; i += 3) {
bmpPalette.Entries[index++] = Color.FromArgb(
l9Bitmap.palette[i], l9Bitmap.palette[i + 1],
l9Bitmap.palette[i + 2]);
}
bitmap.Palette = bmpPalette;
return bitmap;
}
///
/// Creates a scaled bitmap from the pixels buffer.
///
/// the bitmap, or null if an error occurred
unsafe Bitmap CreateLineDrawnImage()
{
Bitmap bitmap = null;
fixed (byte *bsrc = pixels) {
bitmap = ScaleBitmap(bsrc, pictureSize.Width,
pictureSize.Height);
}
if (bitmap == null) {
return null;
}
ColorPalette bitmapPalette = bitmap.Palette;
Color[] colors = Config.Instance.GetActivePalette();
for (int i = 0; i < palette.Length; i++) {
bitmapPalette.Entries[i] = colors[palette[i]];
}
bitmap.Palette = bitmapPalette;
return bitmap;
}
///
/// Scales a bitmap according to the configuration settings.
///
/// the bitmap data
/// the bitmap's width
/// the bitmap's height
///
unsafe Bitmap ScaleBitmap(byte *data, int width, int height)
{
int scaleY = Config.Instance.ImageScaling;
int scaleX = scaleY;
// Double width for line-drawn graphics with 160px
if (graphicsMode == GraphicsMode.LineDrawn && width == 160) {
scaleX = scaleX * 2;
}
int widthS = width * scaleX;
int heightS = height * scaleY;
//
// Prepare destination bitmap
//
Bitmap bitmap = new Bitmap(widthS, heightS,
PixelFormat.Format8bppIndexed);
if (bitmap == null) {
return null;
}
BitmapData bmData = bitmap.LockBits(
new Rectangle(0, 0, widthS, heightS), ImageLockMode.WriteOnly,
PixelFormat.Format8bppIndexed);
//
// Transfer pixels
//
int i, j;
int align = widthS % 4 > 0 ? 4 - (widthS % 4) : 0;
byte *bsrc = (byte *)data;
byte *bdest = (byte *)bmData.Scan0.ToPointer();
int *isrc = (int *)bsrc;
int *idest = (int *)bdest;
if (scaleX == 1 && scaleY == 1) {
for (i = 0; i < height; i++) {
isrc = (int *)bsrc;
idest = (int *)bdest;
for (j = 0; j < (width >> 2); j++) {
*(idest++) = *(isrc++);
}
bsrc += j << 2;
bdest += j << 2;
if (align > 0) {
for (j = 0; j < (width % 4); j++) {
*(bdest++) = *(bsrc++);
}
}
bdest += align;
}
}
else {
int[] vectX = new int[widthS];
int k = 0;
for (i = 0; i < width; i++) {
for (j = 0; j < scaleX; j++) {
vectX[k++] = i;
}
}
for (i = 0; i < height; i++) {
isrc = (int *)bdest;
for (j = 0; j < widthS; j++) {
*(bdest++) = *(bsrc + vectX[j]);
}
idest = (int *)(bdest + align);
for (j = 0; j < scaleY - 1; j++) {
for (k = 0; k < (widthS + align) >> 2; k++) {
*(idest++) = *(isrc + k);
}
}
bsrc += width;
bdest = (byte *)idest;
}
}
bitmap.UnlockBits(bmData);
return bitmap;
}
///
/// Draw a line on the graphics bitmap between (x1,y1) and (x2,y2).
/// Note that either point may lie outside of the bitmap, and that it
/// is the responsibility of the routine to clip to the appropriate
/// coordinates.
/// For each point on the line, if the colour at that point is equal
/// to colour2 the pixel's colour should be changed to colour1, else
/// it should not be modified.
///
/// the x coordinate of (x1,y1)
/// the y coordinate of (x1,y1)
/// the x coordinate of (x2,y2)
/// the y coordinate of (x2,y2)
/// the index of colour 1
/// the index of colour 2
void DrawLineIf(int x1, int y1, int x2, int y2, byte colour1,
byte colour2)
{
if (colour1 == colour2) {
return;
}
int d, dx, dy, aincr, bincr, xincr, yincr, offset;
int picWidth = pictureSize.Width;
int picHeight = pictureSize.Height;
if (Math.Abs(x2 - x1) < Math.Abs(y2 - y1)) {
if (y1 > y2) {
x1 ^= x2; x2 ^= x1; x1 ^= x2;
y1 ^= y2; y2 ^= y1; y1 ^= y2;
}
xincr = (x2 > x1) ? 1 : -1;
dy = y2 - y1;
dx = Math.Abs(x2 - x1);
bincr = dx << 1;
d = bincr - dy;
aincr = (dx - dy) << 1;
do {
if (x1 >= 0 && y1 >= 0 && x1 < picWidth && y1 < picHeight) {
offset = y1 * picWidth + x1;
if (pixels[offset] == colour2) {
pixels[offset] = colour1;
}
}
y1++;
if(d > 0) {
x1 += xincr;
d += aincr;
}
else {
d += bincr;
}
} while (y1 <= y2);
}
else {
if (x1 > x2) {
x1 ^= x2; x2 ^= x1; x1 ^= x2;
y1 ^= y2; y2 ^= y1; y1 ^= y2;
}
yincr = (y2 > y1) ? 1 : -1;
dx = x2 - x1;
dy = Math.Abs(y2 - y1);
bincr = dy << 1;
d = bincr - dx;
aincr = (dy - dx) << 1;
do {
if (x1 >= 0 && y1 >= 0 && x1 < picWidth && y1 < picHeight) {
offset = y1 * picWidth + x1;
if (pixels[offset] == colour2) {
pixels[offset] = colour1;
}
}
x1++;
if(d > 0) {
y1 += yincr;
d += aincr;
}
else {
d += bincr;
}
} while (x1 <= x2);
}
}
///
/// If the pixel's colour at (x,y) is equal to colour2, fill the
/// region containing (x,y) with colour1. The boundaries of the
/// region are defined as those areas of the bitmap with a colour
/// other than colour2.
///
/// the x coordinate of pixel (x,y)
/// the y coordinate of pixel (x,y)
/// the index of colour 1
/// the index of colour 2
void FloodFillIf(int x, int y, byte colour1, byte colour2)
{
int picWidth = pictureSize.Width;
int picHeight = pictureSize.Height;
if (x < 0 || y < 0 || x >= picWidth || y >= picHeight
|| pixels[y * picWidth + x] != colour2) {
return;
}
int[,] stack = new int[picWidth * picHeight, 2];
int index = 0;
stack[0, 0] = x;
stack[0, 1] = y;
do {
int px = stack[index, 0];
int py = stack[index, 1];
int offset = py * picWidth + px;
index--;
if (pixels[offset] == colour2) {
pixels[offset] = colour1;
if (py + 1 < picHeight
&& pixels[offset + picWidth] != colour1) {
index++;
stack[index, 0] = px;
stack[index, 1] = py + 1;
}
if (py - 1 >= 0
&& pixels[offset - picWidth] != colour1) {
index++;
stack[index, 0] = px;
stack[index, 1] = py - 1;
}
if (px + 1 < picWidth && pixels[offset + 1] != colour1) {
index++;
stack[index, 0] = px + 1;
stack[index, 1] = py;
}
if (px - 1 >= 0 && pixels[offset - 1] != colour1) {
index++;
stack[index, 0] = px - 1;
stack[index, 1] = py;
}
}
}
while(index >= 0);
}
}
///
/// Represents the method that handles a PictureCreated event.
///
public delegate void PictureCreatedEventHandler(PictureCreatedEventArgs e);
///
/// Provides data for the PictureCreated event.
///
public class PictureCreatedEventArgs : EventArgs
{
// -- Attributes -----------------------------------------------------
///
/// The picture that has been created.
///
Bitmap picture = null;
// -- Constructors ---------------------------------------------------
///
/// Initializes a new instance of the PictureCreatedEventArgs class.
///
/// the picture that has been created
public PictureCreatedEventArgs(Bitmap picture)
{
this.picture = picture;
}
// -- Accessors ------------------------------------------------------
///
/// Gets or sets the picture that has been created.
///
public Bitmap Picture
{
get {
return picture;
}
set {
picture = value;
}
}
}
///
/// Represents the method that handles a GraphicsOff event.
///
public delegate void GraphicsOffEventHandler(GraphicsOffEventArgs e);
///
/// Provides data for the GraphicsOff event.
///
public class GraphicsOffEventArgs : EventArgs
{
// -- Constructors ---------------------------------------------------
///
/// Initializes a new instance of the GraphicsOffEventArgs class.
///
public GraphicsOffEventArgs() { }
}
}