/// <summary> /// Create the VD frame data from the image /// </summary> /// <param name="writer">active binary writer</param> /// <param name="colors">animation colors list (only required for EC animations)</param> /// <param name="centerX">tile center X</param> /// <param name="centerY">tile center X</param> /// <param name="frameImage">Frame image (with the animation size)</param> /// <param name="topX">top X coordinate from the animation frame</param> /// <param name="topY">top Y coordinate from the animation frame</param> public void ExportVDImageData(BinaryWriter writer, DirectBitmap frameImage, short centerX, short centerY, int topX, int topY, List <ColourEntry> colors = null) { // write the image initial X coordinate writer.Write(centerX); // write the image initial Y coordinate writer.Write(centerY); // write the image width writer.Write((ushort)(m_originalVD ? m_width : frameImage.Width)); // write the image height writer.Write((ushort)(m_originalVD ? m_height : frameImage.Height)); // parse the image line by line for (int y = 0; y < m_height; y++) { // index used to search the first used (NON transparent) pixel in the line int i = 0; // current position in the line int x = 0; // we keep cycling until the whole line hans been loaded while (i < m_width) { // scan all the pixels in the line in search of the first one NON transparent for (i = x; i < m_width; i++) { // did we find the first color? if (m_VDImageData[i, y] != null) { break; } } // did we reach the end of the line? if (i >= m_width) { continue; } // index of the first unused (transparent) pixel in the line AFTER the first colored pixel int j; // now we search the last color of this segment to determine the length for (j = i + 1; j < m_width; j++) { // did we find the last color? if (m_VDImageData[j, y] == null) { break; } } // calculate the size of the segment int length = j - i; // calculate the offset x for the point to start drawing the segment int xOffset = ((j - length - centerX) + (m_originalVD ? 0 : topX)) + 0x200; // calculate the offset y for the point to start drawing the segment int yOffset = ((y - centerY - (m_originalVD ? m_height : frameImage.Height)) + (m_originalVD ? 0 : topY)) + 0x200; // create the data array for this segment byte[] data = new byte[length]; // scan the segment to store the index in the colors list for (int r = 0; r < length; r++) { // is this frame from a VD file? if (m_originalVD) { // get the color index from the array string stringColor = m_VDImageData[r + i, y]; // get the index for the color in the colors list data[r] = (byte)int.Parse(stringColor); } else // for EC animations we have to search for the color index { data[r] = (byte)GetPaletteIndex(frameImage.GetPixel(r + i + topX, y + topY), colors);; } } // create the header (in bytes) int header = length | (yOffset << 12) | (xOffset << 22); // fix the header header ^= _doubleXor; // write the header writer.Write(header); // write each data byte of the frame foreach (byte b in data) { writer.Write(b); } // move to the next segment x = j + 1; i = x; // allow the form to update before we move to the next Application.DoEvents(); } } // write the end of frame flag (header) writer.Write(0x7FFF7FFF); }
// -------------------------------------------------------------- #region PUBLIC FUNCTIONS // -------------------------------------------------------------- /// <summary> /// Load the frame image /// </summary> /// <param name="_ImageData">Byte array to read</param> /// <param name="_ImageDataOffset">Data offset</param> /// <param name="m_Colours">Colors table to use</param> public void LoadFrameImage(byte[] _ImageData, long _ImageDataOffset, List <ColourEntry> m_Colours) { // create the VD image data array (in case we want to save the image in VD later) m_VDImageData = new string[m_width, m_height]; // create the basic image m_image = new DirectBitmap(m_width, m_height); // get the current byte index int currByte = (int)((long)DataOffset - _ImageDataOffset); // starting x and y coordinates for drawing pixels int curx = 0; int cury = 0; // are we still within the image boundries? while (cury < m_height) { // get the next byte byte curr = _ImageData[currByte++]; // are we positioned before the header? if (curr < (byte)128) { // move to the correct starting point for ( ; curr > (byte)0; curr--) { NextCoordinate(ref curx, ref cury, m_width, m_height); } } else // correct position { // get the next byte byte next = _ImageData[currByte++]; // calculate the factors int factor1 = (int)next / 16; int factor2 = (int)next % 16; // is the factor greater than 0? if (factor1 > 0) { // combine the colors Color color = CombineColors(m_Colours[(int)_ImageData[currByte++]].Pixel, m_image.GetPixel(curx, cury), factor1); // color the pixel m_image.SetPixel(curx, cury, color); // flag that this pixel is used (for VD save) m_VDImageData[curx, cury] = "1"; // get the next coordinate in the image NextCoordinate(ref curx, ref cury, m_width, m_height); } // scan the other bytes for (byte i = (byte)((uint)curr - 128U); i > (byte)0; i--) { // get the pixel color Color pixel = m_Colours[(int)_ImageData[currByte++]].Pixel; // set the color to the image m_image.SetPixel(curx, cury, pixel); // flag that this pixel is used (for VD save) m_VDImageData[curx, cury] = "1"; // move to the next coordinate in the image NextCoordinate(ref curx, ref cury, m_width, m_height); } // is the second factor greater than 0? if (factor2 > 0) { // combine the colors Color color = CombineColors(m_Colours[(int)_ImageData[currByte++]].Pixel, m_image.GetPixel(curx, cury), factor2); // color the pixel m_image.SetPixel(curx, cury, color); // flag that this pixel is used (for VD save) m_VDImageData[curx, cury] = "1"; // get the next coordinate in the image NextCoordinate(ref curx, ref cury, m_width, m_height); } } } }
// -------------------------------------------------------------- #region PUBLIC FUNCTIONS // -------------------------------------------------------------- /// <summary> /// Create the image for the multi item /// </summary> /// <param name="gamePath">Game path (used to load the images)</param> /// <param name="itemsCache">Reference to the list with all the items</param> /// <param name="minZ">Minimum Z to show</param> /// <param name="maxZ">Maximum Z to show</param> /// <returns>Multi item image</returns> public Bitmap GetImage(string gamePath, List <ItemData> itemsCache, Hue h, int minZ = -1, int maxZ = -1) { // check if a minZ has been specified if (minZ == -1 || minZ < MinZ) { minZ = MinZ; } // check if a maxZ has been specified if (maxZ == -1 || maxZ > MaxZ) { maxZ = MaxZ; } // default tile size width int tileSize = defaultTileSize.Width; // tile size to use to calculate the width int widthTileSize = Settings.Default.useOldKRItems ? defaultTileSize.Height : tileSize; // calculate the image width int width = Math.Max(MaxX * widthTileSize + MaxY * widthTileSize - MinX * widthTileSize - MinY * widthTileSize + 200, 600); // calculate the image height int height = width * 2; // calculate the middle of the canvas int halfWidth = width / 2; int halfHeight = height / 2; // original maxZ int orgMaxZ = MaxZ; // too big? if (width > 10000 || height > 10000) { return(null); } // initialize the final image Bitmap final = new Bitmap(width, height); // initialize the drawing tool using (Graphics g = Graphics.FromImage(final)) { // set the background transparent g.Clear(Color.Transparent); // get the parts list List <MultiItemPart> PartsList = new List <MultiItemPart>(Parts); // scan all the parts in the list foreach (MultiItemPart p in PartsList) { // get the item data ItemData itm = itemsCache.Where(it => it.ID == p.ItemID).FirstOrDefault(); // item missing? if (itm == null) { continue; } // the floors with the old KR art tiles are not isometric, so we need to recalculate the positon if (Settings.Default.useOldKRItems && Type == MultiType.House && itm.Flags.HasFlag(ItemData.TileFlag.Surface) && !itm.Flags.HasFlag(ItemData.TileFlag.Bridge) && !itm.Flags.HasFlag(ItemData.TileFlag.StairRight)) { p.X -= 1; p.Y -= 1; } // ship mast are drawn incorrectly with the old KR art if (Settings.Default.useOldKRItems && Type == MultiType.Boat && itm.Flags.HasFlag(ItemData.TileFlag.Foliage)) { // set the position (based on the mast ID) switch (itm.ID) { case 16093: case 15962: { p.X += 1; p.Y += 1; break; } case 15980: case 16098: { p.X += 2; p.Y += 2; break; } } } // signs need to be drawn higher if (Type == MultiType.House && itm.Flags.HasFlag(ItemData.TileFlag.Transparent) && !itm.Flags.HasFlag(ItemData.TileFlag.Foliage)) { p.Z += 5; } // fix the SA ship mast location if (Type == MultiType.Boat && IsSAMast(itm.Flags, itm.Name)) { // move the mast higher p.Z = orgMaxZ + (orgMaxZ - p.Z); // make sure the max Z is still correct MaxZ = Math.Max(MaxZ, p.Z); } } // we draw the image from bottom to top for (int z = MinZ; z <= MaxZ; z++) { // and from back to front for (int y = MinY; y <= MaxY; y++) { for (int x = MinX; x <= MaxX; x++) { // get the part at the current location List <MultiItemPart> currParts = Parts.Where(pt => pt.X == x && pt.Y == y && pt.Z == z).Where(pt => pt.OriginalZ >= minZ && pt.OriginalZ <= maxZ).ToList(); // nothing in this position? if (currParts == null || currParts.Count == 0) { continue; } // put the floors first then the rest currParts = currParts.OrderBy(pt => itemsCache.Where(it => it.ID == pt.ItemID).FirstOrDefault() != null && (!(itemsCache.Where(it => it.ID == pt.ItemID).FirstOrDefault()).Flags.HasFlag(ItemData.TileFlag.Surface) || (itemsCache.Where(it => it.ID == pt.ItemID).FirstOrDefault()).Flags.HasFlag(ItemData.TileFlag.Bridge))).ToList(); // draw all the items in this tile foreach (MultiItemPart p in currParts) { // get the item data ItemData itm = itemsCache.Where(it => it.ID == p.ItemID).FirstOrDefault(); // missing item? if (itm == null) { continue; } // calculate where to draw the item Point drawPoint = GetDrawingPosition(p, itm, width, height); // get the cached tile image Bitmap img = itemsImageCache.ContainsKey((int)itm.ID) ? itemsImageCache[(int)itm.ID] : null; // no image in the cache? if (img == null && !itemsImageCache.ContainsKey((int)itm.ID)) { // load the image in the cache itemsImageCache.Add((int)itm.ID, itm.GetItemImage(gamePath, Settings.Default.useOldKRItems)); // get the image img = itemsImageCache[(int)itm.ID]; } // draw the item image if (img != null) { // is there a hue selected? if (h.ID != 0) { // for boats we apply the color only to the dyeable parts if ((Type == MultiType.Boat && itm.Flags.HasFlag(ItemData.TileFlag.PartialHue) && !IsSAMast(itm.Flags, itm.Name)) || Type != MultiType.Boat) { // create a temporary image to apply the hue using (DirectBitmap db = new DirectBitmap(img)) { // apply the hue img = db.ApplyHue(h.HueDiagram, itm.Flags.HasFlag(ItemData.TileFlag.PartialHue)); } } } // are we using the old KR items and this is a floor item? if (Settings.Default.useOldKRItems && itm.Flags.HasFlag(ItemData.TileFlag.Surface) && !itm.Flags.HasFlag(ItemData.TileFlag.Bridge) && !itm.Flags.HasFlag(ItemData.TileFlag.StairRight) && !itm.Flags.HasFlag(ItemData.TileFlag.Container)) { // kr floors need to be rotated 45° g.DrawImage(DirectBitmap.RotateImage(img, 45), drawPoint); } else // draw the item { g.DrawImage(img, drawPoint); } } // update the form Application.DoEvents(); } } } } } return(DirectBitmap.CropImage(final, 50)); }
/// <summary> /// Floodfill gray pixels in an image /// </summary> /// <param name="bmp">Image to parse</param> /// <param name="pt">Starting point (must be gray)</param> /// <param name="levels">Hue colors table</param> /// <param name="handled">List of all the pixels already done</param> private static void FloodFillGrayPixels(DirectBitmap bmp, Point pt, List <Color> levels, ref List <Point> handled) { // create a points queue Queue <Point> q = new Queue <Point>(); // add the current point to the queue q.Enqueue(pt); // keep going until the queue is empty while (q.Count > 0) { // get the first point Point n = q.Dequeue(); // get the color for the current pixel Color currColor = bmp.GetPixel(n.X, n.Y); // if the pixel is not gray or is already been handled, we can get out (we also skil black and transparent colors) if (!(currColor.R == currColor.G && currColor.R == currColor.B) || ColorMatch(currColor, Color.Black) || ColorMatch(currColor, Color.FromArgb(0, 0, 0, 0)) || handled.Contains(pt)) { return; } // get the next point Point w = n, e = new Point(n.X + 1, n.Y); // keep going until the colors are within the threshold or we reach the beginning of the line while ((w.X >= 0) && IsPixelGrayScale(bmp.GetPixel(w.X, w.Y))) { // replace the pixel color bmp.SetPixel(w.X, w.Y, levels[bmp.GetPixel(w.X, w.Y).B]); // add the pixel to the list of the handled ones if (!handled.Contains(w)) { handled.Add(w); } // if the previous pixel is grayscale, we put that in the queue if ((w.Y > 0) && IsPixelGrayScale(bmp.GetPixel(w.X, w.Y - 1))) { q.Enqueue(new Point(w.X, w.Y - 1)); } // if the next pixel is grayscale, we put that in the queue if ((w.Y < bmp.Height - 1) && IsPixelGrayScale(bmp.GetPixel(w.X, w.Y + 1))) { q.Enqueue(new Point(w.X, w.Y + 1)); } // move backwards in the line w.X--; } // keep going until the colors are within the threshold or we reach the end of the line while ((e.X <= bmp.Width - 1) && IsPixelGrayScale(bmp.GetPixel(e.X, e.Y))) { // replace the pixel color bmp.SetPixel(e.X, e.Y, levels[bmp.GetPixel(e.X, e.Y).B]); // add the pixel to the list of the handled ones if (!handled.Contains(e)) { handled.Add(e); } // check the pixel on the previous line, if it matches, we add it to the queue if ((e.Y > 0) && IsPixelGrayScale(bmp.GetPixel(e.X, e.Y - 1))) { q.Enqueue(new Point(e.X, e.Y - 1)); } // check the pixel in the next line, if it matches, we add it to the queue if ((e.Y < bmp.Height - 1) && IsPixelGrayScale(bmp.GetPixel(e.X, e.Y + 1))) { q.Enqueue(new Point(e.X, e.Y + 1)); } // move forward along the line e.X++; } } }
/// <summary> /// Save the animation in the VD file /// </summary> /// <param name="writer">binary writer attached to the file</param> /// <param name="headerPos">position in the file for the current animation headers</param> /// <param name="dir">direction to export</param> /// <param name="animPos">position in the file in which to write the animation data (color, frames, etc...)</param> /// <returns></returns> public bool ExportAnimationToVD(BinaryWriter writer, int dir, ref long headerPos, ref long animPos) { //// make sure the animation images are available //GenerateImages(); // move to the animation header position writer.BaseStream.Seek(headerPos, SeekOrigin.Begin); // write the location of the animation data writer.Write((int)animPos); // update the header position (for the next animation/direction) headerPos = writer.BaseStream.Position; // move to the animation data address writer.BaseStream.Seek(animPos, SeekOrigin.Begin); // initialize the list of colors (EC animations only) List <ColourEntry> cols = new List <ColourEntry>(); // generate the colors for this direction (EC animations only) if (Frames[dir * FramesPerDirection].VDFrameColors.Count <= 0) { // create the image m_Frames[dir * FramesPerDirection].LoadFrameImage(_ImageData, _ImageDataOffset, m_Colours); // load the palette cols = Frames[dir * FramesPerDirection].GeneratePaletteFromImage(); } // scan the colors table of the current frameset (there are always 256 colors) for (int i = 0; i < 0x100; i++) { // write the color (from VD file) if (Frames[dir * FramesPerDirection].VDFrameColors.Count > 0) { writer.Write((ushort)(Frames[dir * FramesPerDirection].VDFrameColors[i].ColorRGB555 ^ 0x8000)); } else // from EC animation { writer.Write((ushort)(cols[i].ColorRGB555 ^ 0x8000)); } } // store the current position long startPos = (int)writer.BaseStream.Position; // write the amount of frames in this direction writer.Write(FramesPerDirection); // store this position long seek = (int)writer.BaseStream.Position; // calculate the length of the frame data long curr = writer.BaseStream.Position + (4 * FramesPerDirection); // scan the frames of this direction of the animation for (int f = dir * FramesPerDirection; f < (dir * FramesPerDirection) + FramesPerDirection; f++) { // move the cursor to the first address available for this frame writer.BaseStream.Seek(seek, SeekOrigin.Begin); // write the frame data length writer.Write((int)(curr - startPos)); // update the position for the next frame seek = writer.BaseStream.Position; // move to write the frame data writer.BaseStream.Seek(curr, SeekOrigin.Begin); // create the frame with the animation (or forced) size using (DirectBitmap realFrame = CreateRealFrameImage(f, null, 0, 0, true)) { // calculate the tile center short centerX = (short)(m_width - EndX); short centerY = (short)(-EndY); // calculate the top-left corner coordinates int topX = Math.Abs((int)m_InitCoordsX - (int)m_Frames[f].InitCoordsX); int topY = Math.Abs((int)m_InitCoordsY - (int)m_Frames[f].InitCoordsY); // write the frame data Frames[f].ExportVDImageData(writer, realFrame, centerX, centerY, topX, topY, cols); } // update the position to write the next frame data curr = writer.BaseStream.Position; } // calculate the length of this frameset data long length = writer.BaseStream.Position - animPos; // update the position for the next frameset animPos = writer.BaseStream.Position; // move back to the headers location writer.BaseStream.Seek(headerPos, SeekOrigin.Begin); // write the frameset size writer.Write((int)length); // write the "extra" flag (is always 0) writer.Write((int)0); // update the headers position for the next frameset headerPos = writer.BaseStream.Position; return(true); }