diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index a28694c8..95207d12 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -68,6 +68,9 @@ + + WidgetMCG.xaml + @@ -394,6 +397,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/ArcFormats/FC01/ImageMCG.cs b/ArcFormats/FC01/ImageMCG.cs index b459c41c..1493a6bb 100644 --- a/ArcFormats/FC01/ImageMCG.cs +++ b/ArcFormats/FC01/ImageMCG.cs @@ -29,6 +29,7 @@ using System.ComponentModel.Composition; using System.IO; using System.Windows.Media; using GameRes.Formats.Properties; +using GameRes.Formats.Strings; using GameRes.Utility; namespace GameRes.Formats.FC01 @@ -37,6 +38,18 @@ namespace GameRes.Formats.FC01 { public int DataOffset; public int PackedSize; + public int Version; + } + + internal class McgOptions : ResourceOptions + { + public byte Key; + } + + [Serializable] + public class McgScheme : ResourceScheme + { + public Dictionary KnownKeys; } [Export(typeof(ImageFormat))] @@ -46,12 +59,23 @@ namespace GameRes.Formats.FC01 public override string Description { get { return "F&C Co. image format"; } } public override uint Signature { get { return 0x2047434D; } } // 'MCG' + public static Dictionary KnownKeys = new Dictionary(); + + public override ResourceScheme Scheme + { + get { return new McgScheme { KnownKeys = KnownKeys }; } + set { KnownKeys = ((McgScheme)value).KnownKeys; } + } + public override ImageMetaData ReadMetaData (Stream stream) { byte[] header = new byte[0x40]; if (header.Length != stream.Read (header, 0, header.Length)) return null; - if (!Binary.AsciiEqual (header, 4, "2.00")) + if (header[5] != '.') + return null; + int version = header[4] * 100 + header[6] * 10 + header[7] - 0x14D0; + if (version != 200 && version != 101) throw new NotSupportedException ("Not supported MCG format version"); int header_size = LittleEndian.ToInt32 (header, 0x10); if (header_size < 0x40) @@ -68,18 +92,32 @@ namespace GameRes.Formats.FC01 BPP = bpp, DataOffset = header_size, PackedSize = LittleEndian.ToInt32 (header, 0x38) - header_size, + Version = version, }; } + // cache key value so that dialog does not pop up on every file accessed. + byte? LastKey = null; + public override ImageData Read (Stream stream, ImageMetaData info) { - var meta = info as McgMetaData; - if (null == meta) - throw new ArgumentException ("McgFormat.Read should be supplied with McgMetaData", "info"); - - var reader = new McgDecoder (stream, meta); + var meta = (McgMetaData)info; + byte key = Settings.Default.MCGLastKey; + if (101 == meta.Version) + { + if (null == LastKey) + { + var options = Query (arcStrings.ArcImageEncrypted); + key = options.Key; + } + else + key = LastKey.Value; + } + var reader = new McgDecoder (stream, meta, key); reader.Unpack(); - return ImageData.Create (info, PixelFormats.Bgr24, null, reader.Data); + if (reader.Key != 0) + LastKey = reader.Key; + return ImageData.Create (info, PixelFormats.Bgr24, null, reader.Data, reader.Stride); } public override void Write (Stream file, ImageData image) @@ -87,10 +125,23 @@ namespace GameRes.Formats.FC01 throw new System.NotImplementedException ("McgFormat.Write not implemented"); } - public static readonly IReadOnlyDictionary KnownKeys = new Dictionary() + public override ResourceOptions GetDefaultOptions () { - { "Konata yori Kanata made", 0xD5 }, - }; + return new McgOptions { Key = Settings.Default.MCGLastKey }; + } + + public override ResourceOptions GetOptions (object widget) + { + var w = widget as GUI.WidgetMCG; + if (null != w) + Settings.Default.MCGLastKey = w.GetKey(); + return GetDefaultOptions(); + } + + public override object GetAccessWidget () + { + return new GUI.WidgetMCG(); + } } // mcg decompression // graphic.unt @ 100047B0 @@ -99,31 +150,65 @@ namespace GameRes.Formats.FC01 { byte[] m_input; byte[] m_output; - uint m_width; - uint m_height; - uint m_pixels; + int m_width; + int m_height; + int m_pixels; byte m_key; + int m_version; public byte[] Data { get { return m_output; } } + public int Stride { get; private set; } + public byte Key { get { return m_key; } } - public McgDecoder (Stream input, McgMetaData info) + public McgDecoder (Stream input, McgMetaData info, byte key) { input.Position = info.DataOffset; m_input = new byte[info.PackedSize]; if (m_input.Length != input.Read (m_input, 0, m_input.Length)) throw new InvalidFormatException ("Unexpected end of file"); - m_width = info.Width; - m_height = info.Height; + m_width = (int)info.Width; + m_height = (int)info.Height; m_pixels = m_width*m_height; - m_output = new byte[m_pixels*3]; - m_key = Settings.Default.MCGLastKey; + m_key = key; + m_version = info.Version; + Stride = 3 * m_width; + if (101 == m_version) + Stride = (Stride + 3) & -4; } static readonly byte[] ChannelOrder = { 1, 0, 2 }; public void Unpack () { - var reader = new MrgDecoder (m_input, 0, m_pixels); + if (200 == m_version) + UnpackV200(); + else + UnpackV101(); + } + + void UnpackV101 () + { + if (m_key != 0) + { + MrgOpener.Decrypt (m_input, 0, m_input.Length-1, m_key); + } + using (var input = new MemoryStream (m_input)) + using (var lzss = new MrgLzssReader (input, m_input.Length, Stride * m_height)) + { + lzss.Unpack(); + // data remaining within input stream indicates invalid encryption key + if (input.Length - input.Position > 1) + { + m_key = 0; + } + m_output = lzss.Data; + } + } + + void UnpackV200 () + { + m_output = new byte[m_pixels*3]; + var reader = new MrgDecoder (m_input, 0, (uint)m_pixels); do { reader.ResetKey (m_key); @@ -156,27 +241,26 @@ namespace GameRes.Formats.FC01 void Transform () { - uint dst = 0; - uint stride = (m_width - 1) * 3; - for (uint y = m_height-1; y > 0; --y) // @@1a + int dst = 0; + for (int y = m_height-1; y > 0; --y) // @@1a { - for (uint x = stride; x > 0; --x) // @@1b + for (int x = Stride-3; x > 0; --x) // @@1b { int p0 = m_output[dst]; - int py = m_output[dst+stride+3] - p0; + int py = m_output[dst+Stride] - p0; int px = m_output[dst+3] - p0; p0 = Math.Abs (px + py); py = Math.Abs (py); px = Math.Abs (px); byte pv; if (p0 >= px && py >= px) - pv = m_output[dst+stride+3]; + pv = m_output[dst+Stride]; else if (p0 < py) pv = m_output[dst]; else pv = m_output[dst+3]; - m_output[dst+stride+6] += (byte)(pv + 0x80); + m_output[dst+Stride+3] += (byte)(pv + 0x80); ++dst; } dst += 3; diff --git a/ArcFormats/FC01/WidgetMCG.xaml b/ArcFormats/FC01/WidgetMCG.xaml new file mode 100644 index 00000000..c53afa75 --- /dev/null +++ b/ArcFormats/FC01/WidgetMCG.xaml @@ -0,0 +1,26 @@ + + + + + + diff --git a/ArcFormats/FC01/WidgetMCG.xaml.cs b/ArcFormats/FC01/WidgetMCG.xaml.cs new file mode 100644 index 00000000..70fc07e6 --- /dev/null +++ b/ArcFormats/FC01/WidgetMCG.xaml.cs @@ -0,0 +1,77 @@ +using GameRes.Formats.FC01; +using GameRes.Formats.Strings; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Windows.Controls; +using System.Windows.Data; + +namespace GameRes.Formats.GUI +{ + /// + /// Interaction logic for WidgetMCG.xaml + /// + public partial class WidgetMCG : StackPanel + { + public WidgetMCG () + { + InitializeComponent(); + var none = new KeyValuePair[] { new KeyValuePair (arcStrings.ArcIgnoreEncryption, 0) }; + Title.ItemsSource = none.Concat (McgFormat.KnownKeys.OrderBy (x => x.Key)); + } + + public byte GetKey () + { + return ByteKeyConverter.StringToByte (this.Passkey.Text); + } + } + + internal static class ByteKeyConverter + { + public static string ByteToString (object value) + { + byte key = (byte)value; + return key.ToString ("X2", CultureInfo.InvariantCulture); + } + + public static byte StringToByte (object value) + { + string s = value as string; + if (string.IsNullOrEmpty (s)) + return 0; + byte key; + if (!byte.TryParse (s, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out key)) + return 0; + return key; + } + } + + [ValueConversion (typeof (byte), typeof (string))] + class ByteToStringConverter : IValueConverter + { + public object Convert (object value, Type targetType, object parameter, CultureInfo culture) + { + return ByteKeyConverter.ByteToString (value); + } + + public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) + { + return ByteKeyConverter.StringToByte (value); + } + } + + [ValueConversion (typeof (string), typeof (byte))] + class StringToByteConverter : IValueConverter + { + public object Convert (object value, Type targetType, object parameter, CultureInfo culture) + { + return ByteKeyConverter.StringToByte (value); + } + + public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) + { + return ByteKeyConverter.ByteToString (value); + } + } +} diff --git a/supported.html b/supported.html index 73090279..d4f874d0 100644 --- a/supported.html +++ b/supported.html @@ -376,7 +376,10 @@ Yuukyou Gangu 2
*.wbmWPX\x1ABMPNo *.wpnWBD\x1AWAVNo *.wwaWPX\x1AWAVNo -*.mrgMRGNoF&CKonata yori Kanata made +*.mrgMRGNoF&C +Guren ni Somaru Gin no Rozario
+Konata yori Kanata made
+ *.mcgMCG 2.00No *.acdACD 1.00No *.pk
*.gpk
*.tpk
*.wpk
-NoU-Me Soft @@ -417,6 +420,7 @@ Nega0
*.wag
*.4agWAG@
GAF4No *.ykcYKC001YesYuka +Aozora no Mieru Oka
PriministAr
SuGirly Wish
_summer