/* * 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() { } } }