private static Bitmap ConvertIndicesToRgbaBitmap(int width, int height, JJ2Block block, bool removeShadow) { byte[] data = block.AsByteArray(); Bitmap result = new Bitmap(width, height, PixelFormat.Format32bppArgb); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = data[y * width + x]; // Use menu palette here ColorRgba color; if (removeShadow && (index == 63 || index == 143)) { // Remove original shadow pixels color = new ColorRgba(0); } else { color = JJ2DefaultPalette.Menu[index]; } result.SetPixel(x, y, Color.FromArgb(color.A, color.R, color.G, color.B)); } } return(result); }
private void LoadImageData(JJ2Block imageBlock, JJ2Block alphaBlock) { const int BlockSize = 32; for (int i = 0; i < tiles.Length; i++) { ref TilesetTileSection tile = ref tiles[i]; tile.Image = new Bitmap(BlockSize, BlockSize); byte[] imageData = imageBlock.ReadRawBytes(BlockSize * BlockSize, tile.ImageDataOffset); byte[] alphaMaskData = alphaBlock.ReadRawBytes(128, tile.AlphaDataOffset); for (int j = 0; j < (BlockSize * BlockSize); j++) { byte idx = imageData[j]; Color color; if (alphaMaskData.Length > 0 && ((alphaMaskData[j / 8] >> (j % 8)) & 0x01) == 0x00) { //color = Color.Transparent; color = JJ2DefaultPalette.ByIndex[0]; } else { //color = palette[idx]; color = JJ2DefaultPalette.ByIndex[idx]; } tile.Image.SetPixel(j % BlockSize, j / BlockSize, color); } }
public static JJ2DataFile Open(string path, bool strictParser) { using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) using (BinaryReader r = new BinaryReader(s)) { JJ2DataFile j2d = new JJ2DataFile(); uint magic = r.ReadUInt32(); if (magic != 0x42494C50 /*PLIB*/) { throw new InvalidOperationException("Invalid magic string"); } uint signature = r.ReadUInt32(); if (signature != 0xBEBAADDE) { throw new InvalidOperationException("Invalid signature"); } uint version = r.ReadUInt32(); uint recordedSize = r.ReadUInt32(); if (strictParser && s.Length != recordedSize) { throw new InvalidOperationException("Unexpected file size"); } uint recordedCRC = r.ReadUInt32(); int headerBlockPackedSize = r.ReadInt32(); int headerBlockUnpackedSize = r.ReadInt32(); JJ2Block headerBlock = new JJ2Block(s, headerBlockPackedSize, headerBlockUnpackedSize); try { while (true) { string name = headerBlock.ReadString(32, true); uint type = headerBlock.ReadUInt32(); uint offset = headerBlock.ReadUInt32(); uint fileCRC = headerBlock.ReadUInt32(); int filePackedSize = headerBlock.ReadInt32(); int fileUnpackedSize = headerBlock.ReadInt32(); //Console.WriteLine(name + " | " + type.ToString("X") + " | " + fileUnpackedSize + " | " + offset); s.Position = offset; JJ2Block fileBlock = new JJ2Block(s, filePackedSize, fileUnpackedSize); byte[] data = fileBlock.AsByteArray(); } } catch (EndOfStreamException) { // End of file list } // ToDo: Extract files, but it's not needed for now... return(j2d); } }
private void LoadMetadata(JJ2Block block) { palette = new Color[256]; for (int i = 0; i < 256; i++) { byte red = block.ReadByte(); byte green = block.ReadByte(); byte blue = block.ReadByte(); byte alpha = block.ReadByte(); palette[i] = Color.FromArgb(255 - alpha, red, green, blue); } tileCount = block.ReadInt32(); int maxTiles = MaxSupportedTiles; tiles = new TilesetTileSection[maxTiles]; for (int i = 0; i < maxTiles; ++i) { tiles[i].Opaque = block.ReadBool(); } // Block of unknown values, skip block.DiscardBytes(maxTiles); for (int i = 0; i < maxTiles; ++i) { tiles[i].ImageDataOffset = block.ReadUInt32(); } // Block of unknown values, skip block.DiscardBytes(4 * maxTiles); for (int i = 0; i < maxTiles; ++i) { tiles[i].AlphaDataOffset = block.ReadUInt32(); } // Block of unknown values, skip block.DiscardBytes(4 * maxTiles); for (int i = 0; i < maxTiles; ++i) { tiles[i].MaskDataOffset = block.ReadUInt32(); } // We don't care about the flipped masks, those are generated on runtime block.DiscardBytes(4 * maxTiles); }
public static void Convert(string path, string targetPath, bool isPlus) { JJ2Version version; RawList <AnimSection> anims = new RawList <AnimSection>(); RawList <SampleSection> samples = new RawList <SampleSection>(); using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) using (BinaryReader r = new BinaryReader(s)) { Log.Write(LogType.Info, "Reading compressed stream..."); Log.PushIndent(); bool seemsLikeCC = false; uint magic = r.ReadUInt32(); if (magic != 0x42494C41 /*ALIB*/) { throw new InvalidOperationException("Invalid magic string"); } uint signature = r.ReadUInt32(); if (signature != 0x00BEBA00) { throw new InvalidOperationException("Invalid signature"); } uint headerLen = r.ReadUInt32(); uint magicUnknown = r.ReadUInt32(); if (magicUnknown != 0x18080200) { throw new InvalidOperationException("Invalid magic number"); } uint fileLen = r.ReadUInt32(); uint crc = r.ReadUInt32(); int setCnt = r.ReadInt32(); uint[] setAddresses = new uint[setCnt]; for (uint i = 0; i < setCnt; i++) { setAddresses[i] = r.ReadUInt32(); } if (headerLen != s.Position) { throw new InvalidOperationException("Header size mismatch"); } // Read content bool isStreamComplete = true; { int i = 0; try { for (; i < setCnt; i++) { uint magicANIM = r.ReadUInt32(); byte animCount = r.ReadByte(); byte sndCount = r.ReadByte(); ushort frameCount = r.ReadUInt16(); uint cumulativeSndIndex = r.ReadUInt32(); int infoBlockLenC = r.ReadInt32(); int infoBlockLenU = r.ReadInt32(); int frameDataBlockLenC = r.ReadInt32(); int frameDataBlockLenU = r.ReadInt32(); int imageDataBlockLenC = r.ReadInt32(); int imageDataBlockLenU = r.ReadInt32(); int sampleDataBlockLenC = r.ReadInt32(); int sampleDataBlockLenU = r.ReadInt32(); JJ2Block infoBlock = new JJ2Block(s, infoBlockLenC, infoBlockLenU); JJ2Block frameDataBlock = new JJ2Block(s, frameDataBlockLenC, frameDataBlockLenU); JJ2Block imageDataBlock = new JJ2Block(s, imageDataBlockLenC, imageDataBlockLenU); JJ2Block sampleDataBlock = new JJ2Block(s, sampleDataBlockLenC, sampleDataBlockLenU); if (magicANIM != 0x4D494E41) { Log.Write(LogType.Warning, "Header for set " + i + " is incorrect (bad magic value)! Skipping the subfile."); continue; } List <AnimSection> setAnims = new List <AnimSection>(); for (ushort j = 0; j < animCount; j++) { AnimSection anim = new AnimSection(); anim.Set = i; anim.Anim = j; anim.FrameCount = infoBlock.ReadUInt16(); anim.FrameRate = infoBlock.ReadUInt16(); anim.Frames = new AnimFrameSection[anim.FrameCount]; // Skip the rest, seems to be 0x00000000 for all headers infoBlock.DiscardBytes(4); anims.Add(anim); setAnims.Add(anim); } if (i == 65 && setAnims.Count > 5) { seemsLikeCC = true; } if (frameCount > 0) { if (setAnims.Count == 0) { throw new InvalidOperationException("Set has frames but no anims"); } short lastColdspotX = 0, lastColdspotY = 0; short lastHotspotX = 0, lastHotspotY = 0; short lastGunspotX = 0, lastGunspotY = 0; AnimSection currentAnim = setAnims[0]; ushort currentAnimIdx = 0, currentFrame = 0; for (ushort j = 0; j < frameCount; j++) { if (currentFrame >= currentAnim.FrameCount) { currentAnim = setAnims[++currentAnimIdx]; currentFrame = 0; while (currentAnim.FrameCount == 0 && currentAnimIdx < setAnims.Count) { currentAnim = setAnims[++currentAnimIdx]; } } ref AnimFrameSection frame = ref currentAnim.Frames[currentFrame]; frame.SizeX = frameDataBlock.ReadInt16(); frame.SizeY = frameDataBlock.ReadInt16(); frame.ColdspotX = frameDataBlock.ReadInt16(); frame.ColdspotY = frameDataBlock.ReadInt16(); frame.HotspotX = frameDataBlock.ReadInt16(); frame.HotspotY = frameDataBlock.ReadInt16(); frame.GunspotX = frameDataBlock.ReadInt16(); frame.GunspotY = frameDataBlock.ReadInt16(); frame.ImageAddr = frameDataBlock.ReadInt32(); frame.MaskAddr = frameDataBlock.ReadInt32(); // Adjust normalized position // In the output images, we want to make the hotspot and image size constant. currentAnim.NormalizedHotspotX = (short)Math.Max(-frame.HotspotX, currentAnim.NormalizedHotspotX); currentAnim.NormalizedHotspotY = (short)Math.Max(-frame.HotspotY, currentAnim.NormalizedHotspotY); currentAnim.LargestOffsetX = (short)Math.Max(frame.SizeX + frame.HotspotX, currentAnim.LargestOffsetX); currentAnim.LargestOffsetY = (short)Math.Max(frame.SizeY + frame.HotspotY, currentAnim.LargestOffsetY); currentAnim.AdjustedSizeX = (short)Math.Max( currentAnim.NormalizedHotspotX + currentAnim.LargestOffsetX, currentAnim.AdjustedSizeX ); currentAnim.AdjustedSizeY = (short)Math.Max( currentAnim.NormalizedHotspotY + currentAnim.LargestOffsetY, currentAnim.AdjustedSizeY ); #if DEBUG if (currentFrame > 0) { int diffPrevX, diffPrevY, diffNextX, diffNextY; if (frame.ColdspotX != 0 && frame.ColdspotY != 0) { diffPrevX = (lastColdspotX - lastHotspotX); diffPrevY = (lastColdspotY - lastHotspotY); diffNextX = (frame.ColdspotX - frame.HotspotX); diffNextY = (frame.ColdspotY - frame.HotspotY); if (diffPrevX != diffNextX || diffPrevY != diffNextY) { Log.Write(LogType.Warning, "Animation " + currentAnim.Anim + " coldspots in set " + currentAnim.Set + " are different!"); Log.PushIndent(); Log.Write(LogType.Warning, "Frame #" + (currentFrame - 1) + ": " + diffPrevX + "," + diffPrevY + " | " + "Frame #" + currentFrame + ": " + diffNextX + "," + diffNextY); Log.PopIndent(); } } if (frame.GunspotX != 0 && frame.GunspotY != 0) { diffPrevX = (lastGunspotX - lastHotspotX); diffPrevY = (lastGunspotY - lastHotspotY); diffNextX = (frame.GunspotX - frame.HotspotX); diffNextY = (frame.GunspotY - frame.HotspotY); if (diffPrevX != diffNextX || diffPrevY != diffNextY) { Log.Write(LogType.Warning, "Animation " + currentAnim.Anim + " gunspots in set " + currentAnim.Set + " are different!"); Log.PushIndent(); Log.Write(LogType.Warning, "Frame #" + (currentFrame - 1) + ": " + diffPrevX + "," + diffPrevY + " | " + "Frame #" + currentFrame + ": " + diffNextX + "," + diffNextY); Log.PopIndent(); } } } #endif lastColdspotX = frame.ColdspotX; lastColdspotY = frame.ColdspotY; lastHotspotX = frame.HotspotX; lastHotspotY = frame.HotspotY; lastGunspotX = frame.GunspotX; lastGunspotY = frame.GunspotY; currentFrame++; } // Read the image data for each animation frame for (ushort j = 0; j < setAnims.Count; j++) { AnimSection anim = setAnims[j]; if (anim.FrameCount < anim.Frames.Length) { Log.Write(LogType.Error, "Animation " + j + " frame count in set " + i + " doesn't match! Expected " + anim.FrameCount + " frames, but read " + anim.Frames.Length + " instead."); throw new InvalidOperationException(); } for (ushort frame = 0; frame < anim.FrameCount; ++frame) { int dpos = (anim.Frames[frame].ImageAddr + 4); imageDataBlock.SeekTo(dpos - 4); ushort width2 = imageDataBlock.ReadUInt16(); imageDataBlock.SeekTo(dpos - 2); ushort height2 = imageDataBlock.ReadUInt16(); ref AnimFrameSection frameData = ref anim.Frames[frame]; frameData.DrawTransparent = (width2 & 0x8000) > 0; int pxRead = 0; int pxTotal = (frameData.SizeX * frameData.SizeY); bool lastOpEmpty = true; List <byte> imageData = new List <byte>(pxTotal); while (pxRead < pxTotal) { if (dpos > 0x10000000) { Log.Write(LogType.Error, "Loading of animation " + j + " in set " + i + " failed! Aborting."); break; } imageDataBlock.SeekTo(dpos); byte op = imageDataBlock.ReadByte(); //if (op == 0) { // Console.WriteLine("[" + i + ":" + j + "] Next image operation should probably not be 0x00."); //} if (op < 0x80) { // Skip the given number of pixels, writing them with the transparent color 0 pxRead += op; while (op-- > 0) { imageData.Add((byte)0x00); } dpos++; } else if (op == 0x80) { // Skip until the end of the line ushort linePxLeft = (ushort)(frameData.SizeX - pxRead % frameData.SizeX); if (pxRead % anim.Frames[frame].SizeX == 0 && !lastOpEmpty) { linePxLeft = 0; } pxRead += linePxLeft; while (linePxLeft-- > 0) { imageData.Add((byte)0x00); } dpos++; } else { // Copy specified amount of pixels (ignoring the high bit) ushort bytesToRead = (ushort)(op & 0x7F); imageDataBlock.SeekTo(dpos + 1); byte[] nextData = imageDataBlock.ReadRawBytes(bytesToRead); imageData.AddRange(nextData); pxRead += bytesToRead; dpos += bytesToRead + 1; } lastOpEmpty = (op == 0x80); } frameData.ImageData = imageData.ToArray(); frameData.MaskData = new BitArray(pxTotal, false); dpos = frameData.MaskAddr; pxRead = 0; // No mask if (dpos == unchecked ((int)0xFFFFFFFF)) { continue; } while (pxRead < pxTotal) { imageDataBlock.SeekTo(dpos); byte b = imageDataBlock.ReadByte(); for (byte bit = 0; bit < 8 && (pxRead + bit) < pxTotal; ++bit) { frameData.MaskData[pxRead + bit] = ((b & (1 << (7 - bit))) != 0); } pxRead += 8; } } } } for (ushort j = 0; j < sndCount; ++j) { SampleSection sample; //sample.Id = (ushort)(cumulativeSndIndex + j); sample.IdInSet = j; sample.Set = i; int totalSize = sampleDataBlock.ReadInt32(); uint magicRIFF = sampleDataBlock.ReadUInt32(); int chunkSize = sampleDataBlock.ReadInt32(); // "ASFF" for 1.20, "AS " for 1.24 uint format = sampleDataBlock.ReadUInt32(); bool isASFF = (format == 0x46465341); uint magicSAMP = sampleDataBlock.ReadUInt32(); uint sampSize = sampleDataBlock.ReadUInt32(); // Padding/unknown data #1 // For set 0 sample 0: // 1.20 1.24 // +00 00 00 00 00 00 00 00 00 +00 40 00 00 00 00 00 00 00 // +08 00 00 00 00 00 00 00 00 +08 00 00 00 00 00 00 00 00 // +10 00 00 00 00 00 00 00 00 +10 00 00 00 00 00 00 00 00 // +18 00 00 00 00 +18 00 00 00 00 00 00 00 00 // +20 00 00 00 00 00 40 FF 7F sampleDataBlock.DiscardBytes(40 - (isASFF ? 12 : 0)); if (isASFF) { // All 1.20 samples seem to be 8-bit. Some of them are among those // for which 1.24 reads as 24-bit but that might just be a mistake. sampleDataBlock.DiscardBytes(2); sample.Multiplier = 0; } else { // for 1.24. 1.20 has "20 40" instead in s0s0 which makes no sense sample.Multiplier = sampleDataBlock.ReadUInt16(); } // Unknown. s0s0 1.20: 00 80, 1.24: 80 00 sampleDataBlock.DiscardBytes(2); uint payloadSize = sampleDataBlock.ReadUInt32(); // Padding #2, all zeroes in both sampleDataBlock.DiscardBytes(8); sample.SampleRate = sampleDataBlock.ReadUInt32(); int actualDataSize = chunkSize - 76 + (isASFF ? 12 : 0); sample.Data = sampleDataBlock.ReadRawBytes(actualDataSize); // Padding #3 sampleDataBlock.DiscardBytes(4); if (magicRIFF != 0x46464952 || magicSAMP != 0x504D4153) { throw new InvalidOperationException("Sample has invalid header"); } if (sample.Data.Length < actualDataSize) { Log.Write(LogType.Warning, "Sample " + j + " in set " + i + " was shorter than expected! Expected " + actualDataSize + " bytes, but read " + sample.Data.Length + " instead."); } if (totalSize > chunkSize + 12) { // Sample data is probably aligned to X bytes since the next sample doesn't always appear right after the first ends. Log.Write(LogType.Warning, "Adjusting read offset of sample " + j + " in set " + i + " by " + (totalSize - chunkSize - 12) + " bytes."); sampleDataBlock.DiscardBytes(totalSize - chunkSize - 12); } samples.Add(sample); } } } catch (EndOfStreamException) {
public static JJ2Episode Open(string path) { using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) using (BinaryReader r = new BinaryReader(s)) { JJ2Episode episode = new JJ2Episode(); episode.episodeToken = Path.GetFileNameWithoutExtension(path).ToLower(CultureInfo.InvariantCulture); // ToDo: Implement JJ2+ extended data, but I haven't seen it anywhere yet // the condition of unlocking (currently only defined for 0 meaning "always unlocked" // and 1 meaning "requires the previous episode to be finished", stored as a 4-byte-long // integer starting at byte 0x4), binary flags of various purpose (currently supported // flags are 1 and 2 used to reset respectively player ammo and lives when the episode // begins; stored as a 4-byte-long integer starting at byte 0x8), file name of the preceding // episode (used mostly to determine whether the episode should be locked, stored // as a 32-byte-long chain of characters starting at byte 0x4C), file name of the following // episode (that is cycled to after the episode ends, stored as a 32-byte-long // chain of characters starting at byte 0x6C) // Header (208 bytes) int headerSize = r.ReadInt32(); episode.position = r.ReadInt32(); episode.isRegistered = (r.ReadInt32() != 0); int unknown1 = r.ReadInt32(); // Episode name { byte[] episodeNameRaw = r.ReadBytes(128); episode.episodeName = Encoding.ASCII.GetString(episodeNameRaw); int i = episode.episodeName.IndexOf('\0'); if (i != -1) { episode.episodeName = episode.episodeName.Substring(0, i); } } // First level { byte[] firstLevelRaw = r.ReadBytes(32); episode.firstLevel = Encoding.ASCII.GetString(firstLevelRaw); int i = episode.firstLevel.IndexOf('\0'); if (i != -1) { episode.firstLevel = episode.firstLevel.Substring(0, i); } } // ToDo: Episode images are not supported yet int width = r.ReadInt32(); int height = r.ReadInt32(); int unknown2 = r.ReadInt32(); int unknown3 = r.ReadInt32(); int titleWidth = r.ReadInt32(); int titleHeight = r.ReadInt32(); int unknown4 = r.ReadInt32(); int unknown5 = r.ReadInt32(); { int imagePackedSize = r.ReadInt32(); int imageUnpackedSize = width * height; JJ2Block imageBlock = new JJ2Block(s, imagePackedSize, imageUnpackedSize); //episode.image = ConvertIndicesToRgbaBitmap(width, height, imageBlock, false); } { int titleLightPackedSize = r.ReadInt32(); int titleLightUnpackedSize = titleWidth * titleHeight; JJ2Block titleLightBlock = new JJ2Block(s, titleLightPackedSize, titleLightUnpackedSize); episode.titleLight = ConvertIndicesToRgbaBitmap(titleWidth, titleHeight, titleLightBlock, true); } //{ // int titleDarkPackedSize = r.ReadInt32(); // int titleDarkUnpackedSize = titleWidth * titleHeight; // JJ2Block titleDarkBlock = new JJ2Block(s, titleDarkPackedSize, titleDarkUnpackedSize); // episode.titleDark = ConvertIndicesToRgbaBitmap(titleWidth, titleHeight, titleDarkBlock, true); //} return(episode); } }
public static JJ2Tileset Open(string path, bool strictParser) { using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Skip copyright notice s.Seek(180, SeekOrigin.Current); JJ2Tileset tileset = new JJ2Tileset(); JJ2Block headerBlock = new JJ2Block(s, 262 - 180); uint magic = headerBlock.ReadUInt32(); if (magic != 0x454C4954 /*TILE*/) { throw new InvalidOperationException("Invalid magic string"); } uint signature = headerBlock.ReadUInt32(); if (signature != 0xAFBEADDE) { throw new InvalidOperationException("Invalid signature"); } tileset.name = headerBlock.ReadString(32, true); ushort versionNum = headerBlock.ReadUInt16(); tileset.version = (versionNum <= 512 ? JJ2Version.BaseGame : JJ2Version.TSF); int recordedSize = headerBlock.ReadInt32(); if (strictParser && s.Length != recordedSize) { throw new InvalidOperationException("Unexpected file size"); } // Get the CRC; would check here if it matches if we knew what variant it is AND what it applies to // Test file across all CRC32 variants + Adler had no matches to the value obtained from the file // so either the variant is something else or the CRC is not applied to the whole file but on a part int recordedCRC = headerBlock.ReadInt32(); // Read the lengths, uncompress the blocks and bail if any block could not be uncompressed // This could look better without all the copy-paste, but meh. int infoBlockPackedSize = headerBlock.ReadInt32(); int infoBlockUnpackedSize = headerBlock.ReadInt32(); int imageBlockPackedSize = headerBlock.ReadInt32(); int imageBlockUnpackedSize = headerBlock.ReadInt32(); int alphaBlockPackedSize = headerBlock.ReadInt32(); int alphaBlockUnpackedSize = headerBlock.ReadInt32(); int maskBlockPackedSize = headerBlock.ReadInt32(); int maskBlockUnpackedSize = headerBlock.ReadInt32(); JJ2Block infoBlock = new JJ2Block(s, infoBlockPackedSize, infoBlockUnpackedSize); JJ2Block imageBlock = new JJ2Block(s, imageBlockPackedSize, imageBlockUnpackedSize); JJ2Block alphaBlock = new JJ2Block(s, alphaBlockPackedSize, alphaBlockUnpackedSize); JJ2Block maskBlock = new JJ2Block(s, maskBlockPackedSize, maskBlockUnpackedSize); tileset.LoadMetadata(infoBlock); tileset.LoadImageData(imageBlock, alphaBlock); tileset.LoadMaskData(maskBlock); return(tileset); } }