diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 8d23fd3f..22e9b33f 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -91,6 +91,11 @@ + + + + WidgetMSD.xaml + @@ -734,6 +739,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/ArcFormats/NSystem/ArcFJSYS.cs b/ArcFormats/NSystem/ArcFJSYS.cs new file mode 100644 index 00000000..a2e16a1c --- /dev/null +++ b/ArcFormats/NSystem/ArcFJSYS.cs @@ -0,0 +1,215 @@ +//! \file ArcFJSYS.cs +//! \date Thu Nov 24 14:00:07 2016 +//! \brief NSystem engine resource archive. +// +// Copyright (C) 2016 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using GameRes.Formats.Properties; +using GameRes.Formats.Strings; +using GameRes.Utility; + +namespace GameRes.Formats.NSystem +{ + internal class MsdArchive : ArcFile + { + public readonly string Key; + + public MsdArchive (ArcView arc, ArchiveFormat impl, ICollection dir, string key) + : base (arc, impl, dir) + { + Key = key; + } + } + + [Export(typeof(ArchiveFormat))] + public class FjsysOpener : ArchiveFormat + { + public override string Tag { get { return "FJSYS"; } } + public override string Description { get { return "NSystem engine resource archive"; } } + public override uint Signature { get { return 0x59534A46; } } // 'FJSY' + public override bool IsHierarchic { get { return false; } } + public override bool CanWrite { get { return false; } } + + public FjsysOpener () + { + Extensions = new string[] { "" }; + } + + public override ArcFile TryOpen (ArcView file) + { + if (file.View.ReadByte (4) != 'S') + return null; + uint names_size = file.View.ReadUInt32 (0xC); + int count = file.View.ReadInt32 (0x10); + if (!IsSaneCount (count)) + return null; + uint index_offset = 0x54; + uint index_size = (uint)count * 0x10; + var names = file.View.ReadBytes (index_offset + index_size, names_size); + + var dir = new List (count); + bool has_scripts = false; + for (int i = 0; i < count; ++i) + { + var name_offset = file.View.ReadInt32 (index_offset); + var name = Binary.GetCString (names, name_offset); + var entry = FormatCatalog.Instance.Create (name); + entry.Size = file.View.ReadUInt32 (index_offset+4); + entry.Offset = file.View.ReadInt64 (index_offset+8); + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + has_scripts = has_scripts || name.EndsWith (".msd", StringComparison.InvariantCultureIgnoreCase); + dir.Add (entry); + index_offset += 0x10; + } + if (has_scripts) + { + var password = QueryPassword (file.Name); + if (!string.IsNullOrEmpty (password)) + return new MsdArchive (file, this, dir, password); + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var msarc = arc as MsdArchive; + if (null == msarc || string.IsNullOrEmpty (msarc.Key) + || !entry.Name.EndsWith (".msd", StringComparison.InvariantCultureIgnoreCase) + || arc.File.View.AsciiEqual (entry.Offset, "MSCENARIO FILE ")) + return base.OpenEntry (arc, entry); + var input = arc.File.CreateStream (entry.Offset, entry.Size); + return new CryptoStream (input, new MsdTransform (msarc.Key), CryptoStreamMode.Read); + } + + string QueryPassword (string arc_name) + { + var title = FormatCatalog.Instance.LookupGame (arc_name); + if (!string.IsNullOrEmpty (title) && KnownPasswords.ContainsKey (title)) + return KnownPasswords[title]; + var options = Query (arcStrings.FJSYSNotice); + return options.MsdPassword; + } + + public override ResourceOptions GetDefaultOptions () + { + return new FjsysOptions { MsdPassword = Settings.Default.FJSYSPassword }; + } + + public override object GetAccessWidget () + { + return new GUI.WidgetMSD(); + } + + public static Dictionary KnownPasswords = new Dictionary(); + + public override ResourceScheme Scheme + { + get { return new FjsysScheme { MsdPasswords = KnownPasswords }; } + set { KnownPasswords = ((FjsysScheme)value).MsdPasswords; } + } + } + + internal sealed class MsdTransform : ICryptoTransform + { + const int BlockSize = 0x20; + string m_key; + MD5 m_md5; + StringBuilder m_hash_str; + int m_block_num; + + public bool CanReuseTransform { get { return false; } } + public bool CanTransformMultipleBlocks { get { return true; } } + public int InputBlockSize { get { return BlockSize; } } + public int OutputBlockSize { get { return BlockSize; } } + + public MsdTransform (string key) + { + m_key = key; + m_md5 = MD5.Create(); + m_hash_str = new StringBuilder (BlockSize); + m_block_num = 0; + } + + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, + byte[] outputBuffer, int outputOffset) + { + int block_count = inputCount / BlockSize; + for (int i = 0; i < block_count; ++i) + { + DoTransform (inputBuffer, inputOffset, BlockSize, outputBuffer, outputOffset); + inputOffset += BlockSize; + outputOffset += BlockSize; + } + return inputCount; + } + + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] outputBuffer = new byte[inputCount]; + DoTransform (inputBuffer, inputOffset, inputCount, outputBuffer, 0); + return outputBuffer; + } + + void DoTransform (byte[] input, int src, int count, byte[] output, int dst) + { + string chunk_key_str = m_key + m_block_num.ToString(); + ++m_block_num; + var chunk_key = Encodings.cp932.GetBytes (chunk_key_str); + var hash = m_md5.ComputeHash (chunk_key); + m_hash_str.Clear(); + for (int j = 0; j < hash.Length; ++j) + m_hash_str.AppendFormat ("{0:x2}", hash[j]); + count = Math.Min (m_hash_str.Length, count); + for (int k = 0; k < count; ++k) + output[dst++] = (byte)(input[src++] ^ m_hash_str[k]); + } + + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_md5.Dispose(); + _disposed = true; + } + GC.SuppressFinalize (this); + } + } + + [Serializable] + public class FjsysScheme : ResourceScheme + { + public Dictionary MsdPasswords; + } + + public class FjsysOptions : ResourceOptions + { + public string MsdPassword; + } +} diff --git a/ArcFormats/NSystem/ImageMGD.cs b/ArcFormats/NSystem/ImageMGD.cs new file mode 100644 index 00000000..c250dd1e --- /dev/null +++ b/ArcFormats/NSystem/ImageMGD.cs @@ -0,0 +1,240 @@ +//! \file ImageMGD.cs +//! \date Thu Nov 24 14:37:18 2016 +//! \brief NSystem image format. +// +// Copyright (C) 2016 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace GameRes.Formats.NSystem +{ + internal class MgdMetaData : ImageMetaData + { + public int DataOffset; + public int UnpackedSize; + public int Mode; + } + + [Export(typeof(ImageFormat))] + public class MgdFormat : ImageFormat + { + public override string Tag { get { return "MGD/NSystem"; } } + public override string Description { get { return "NSystem image format"; } } + public override uint Signature { get { return 0x2044474D; } } // 'MGD ' + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + var header = file.ReadHeader (0x1A); + int header_size = header.ToUInt16 (4); + uint width = header.ToUInt16 (0xC); + uint height = header.ToUInt16 (0xE); + int unpacked_size = header.ToInt32 (0x10); + int mode = header.ToInt32 (0x18); + if (mode < 0 || mode > 2) + return null; + return new MgdMetaData + { + Width = width, + Height = height, + BPP = 32, + DataOffset = header_size, + UnpackedSize = unpacked_size, + Mode = mode, + }; + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + var meta = (MgdMetaData)info; + file.Position = meta.DataOffset; + int data_size = file.ReadInt32(); + switch (meta.Mode) + { + case 0: + var pixels = file.ReadBytes (data_size); + return ImageData.Create (info, PixelFormats.Bgra32, null, pixels); + + case 1: + { + var decoder = new MgdDecoder (file, meta, data_size); + decoder.Unpack(); + return ImageData.Create (info, PixelFormats.Bgra32, null, decoder.Data); + } + + case 2: + using (var png = new StreamRegion (file.AsStream, file.Position, data_size, true)) + { + var decoder = new PngBitmapDecoder (png, + BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + var frame = decoder.Frames[0]; + frame.Freeze(); + return new ImageData (frame, info); + } + + default: + throw new InvalidFormatException(); + } + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("MgdFormat.Write not implemented"); + } + } + + internal class MgdDecoder + { + IBinaryStream m_input; + MgdMetaData m_info; + byte[] m_output; + + public byte[] Data { get { return m_output; } } + + public MgdDecoder (IBinaryStream input, MgdMetaData info, int packed_size) + { + m_input = input; + m_info = info; + m_output = new byte[m_info.UnpackedSize]; + } + + public void Unpack () + { + int alpha_size = m_input.ReadInt32(); + UnpackAlpha (alpha_size); + int rgb_size = m_input.ReadInt32(); + UnpackColor (rgb_size); + } + + void UnpackAlpha (int length) + { + int dst = 3; + while (length > 0) + { + int count = m_input.ReadInt16(); + length -= 2; + if (count < 0) + { + count = (count & 0x7FFF) + 1; + byte a = (byte)~m_input.ReadUInt8(); + length--; + for (int i = 0; i < count; ++i) + { + m_output[dst] = a; + dst += 4; + } + } + else + { + for (int i = 0; i < count; ++i) + { + m_output[dst] = (byte)~m_input.ReadUInt8(); + dst += 4; + } + length -= count; + } + + } + } + + void UnpackColor (int length) + { + int dst = 0; + while (length > 0) + { + int count = m_input.ReadUInt8(); + length--; + switch (count & 0xC0) + { + case 0x80: + count &= 0x3F; + int b = m_output[dst-4]; + int g = m_output[dst-3]; + int r = m_output[dst-2]; + for (int i = 0; i < count; ++i) + { + ushort delta = m_input.ReadUInt16(); + length -= 2; + if (0 != (delta & 0x8000)) + { + r += (delta >> 10) & 0x1F; + g += (delta >> 5) & 0x1F; + b += delta & 0x1F; + } + else + { + if (0 != (delta & 0x4000)) + r -= (delta >> 10) & 0xF; + else + r += (delta >> 10) & 0xF; + + if (0 != (delta & 0x0200)) + g -= (delta >> 5) & 0xF; + else + g += (delta >> 5) & 0xF; + + if (0 != (delta & 0x0010)) + b -= delta & 0xF; + else + b += delta & 0xF; + } + m_output[dst ] = (byte)b; + m_output[dst+1] = (byte)g; + m_output[dst+2] = (byte)r; + dst += 4; + } + break; + + case 0x40: + count &= 0x3F; + m_input.Read (m_output, dst, 3); + length -= 3; + int src = dst; + dst += 4; + for (int i = 0; i < count; ++i) + { + m_output[dst ] = m_output[src ]; + m_output[dst+1] = m_output[src+1]; + m_output[dst+2] = m_output[src+2]; + dst += 4; + } + break; + + case 0: + for (int i = 0; i < count; ++i) + { + m_input.Read (m_output, dst, 3); + length -= 3; + dst += 4; + } + break; + + default: + throw new InvalidFormatException(); + } + } + } + } +} diff --git a/ArcFormats/NSystem/WidgetMSD.xaml b/ArcFormats/NSystem/WidgetMSD.xaml new file mode 100644 index 00000000..64a106f4 --- /dev/null +++ b/ArcFormats/NSystem/WidgetMSD.xaml @@ -0,0 +1,10 @@ + + + + diff --git a/ArcFormats/NSystem/WidgetMSD.xaml.cs b/ArcFormats/NSystem/WidgetMSD.xaml.cs new file mode 100644 index 00000000..fa7e0a51 --- /dev/null +++ b/ArcFormats/NSystem/WidgetMSD.xaml.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows.Controls; +using GameRes.Formats.NSystem; +using GameRes.Formats.Properties; +using GameRes.Formats.Strings; + +namespace GameRes.Formats.GUI +{ + /// + /// Interaction logic for WidgetMSD.xaml + /// + public partial class WidgetMSD : StackPanel + { + public WidgetMSD () + { + InitializeComponent (); + var first = new Dictionary { { arcStrings.ArcNoEncryption, "" } }; + Title.ItemsSource = first.Concat (FjsysOpener.KnownPasswords.OrderBy (x => x.Key)); + Password.Text = Settings.Default.FJSYSPassword; + } + + private void Title_SelectionChanged (object sender, SelectionChangedEventArgs e) + { + if (null != this.Title.SelectedItem && null != this.Password) + { + var selected = (KeyValuePair)this.Title.SelectedItem; + this.Password.Text = selected.Value; + } + } + } +} diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs index b8c7815e..8b229ab8 100644 --- a/ArcFormats/Properties/Settings.Designer.cs +++ b/ArcFormats/Properties/Settings.Designer.cs @@ -657,5 +657,17 @@ namespace GameRes.Formats.Properties { this["EAGLSEncryption"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string FJSYSPassword { + get { + return ((string)(this["FJSYSPassword"])); + } + set { + this["FJSYSPassword"] = value; + } + } } } diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings index 73836f72..5c698f28 100644 --- a/ArcFormats/Properties/Settings.settings +++ b/ArcFormats/Properties/Settings.settings @@ -161,5 +161,8 @@ + + + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.Designer.cs b/ArcFormats/Strings/arcStrings.Designer.cs index ae4e7a8c..9472da7c 100644 --- a/ArcFormats/Strings/arcStrings.Designer.cs +++ b/ArcFormats/Strings/arcStrings.Designer.cs @@ -188,6 +188,16 @@ namespace GameRes.Formats.Strings { } } + /// + /// Looks up a localized string similar to Archive contains encrypted scripts. + ///Choose encryption scheme or enter a passphrase.. + /// + public static string FJSYSNotice { + get { + return ResourceManager.GetString("FJSYSNotice", resourceCulture); + } + } + /// /// Looks up a localized string similar to Choose title or enter a key. /// diff --git a/ArcFormats/Strings/arcStrings.ko-KR.resx b/ArcFormats/Strings/arcStrings.ko-KR.resx index efb3b744..9794af23 100644 --- a/ArcFormats/Strings/arcStrings.ko-KR.resx +++ b/ArcFormats/Strings/arcStrings.ko-KR.resx @@ -379,4 +379,8 @@ 이미지가 암호화됨. 적절한 암호체계를 선택하세요. + + 아카이브가 암호화된 스크립트를 포함함. +암호체계를 고르거나 암호를 입력하세요. + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.resx b/ArcFormats/Strings/arcStrings.resx index 1d5db66b..8ca8d83d 100644 --- a/ArcFormats/Strings/arcStrings.resx +++ b/ArcFormats/Strings/arcStrings.resx @@ -382,4 +382,8 @@ Choose appropriate encryption scheme. Image is encrypted. Choose appropriate encryption scheme. + + Archive contains encrypted scripts. +Choose encryption scheme or enter a passphrase. + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.ru-RU.resx b/ArcFormats/Strings/arcStrings.ru-RU.resx index 0991dec0..e6cd3457 100644 --- a/ArcFormats/Strings/arcStrings.ru-RU.resx +++ b/ArcFormats/Strings/arcStrings.ru-RU.resx @@ -158,6 +158,10 @@ Ключи шифрования + + Архив содержит зашифрованные скрипты. +Выберите способ шифрования или введите текстовый пароль. + Выберите наименование или введите ключ diff --git a/ArcFormats/Strings/arcStrings.zh-Hans.resx b/ArcFormats/Strings/arcStrings.zh-Hans.resx index 9decc4d1..b3174e4b 100644 --- a/ArcFormats/Strings/arcStrings.zh-Hans.resx +++ b/ArcFormats/Strings/arcStrings.zh-Hans.resx @@ -380,4 +380,8 @@ 图像已加密。 请选择正确的加密方式。 + + 压缩文件包含已加密的脚本。 +请选择加密方式或输入密码。 + \ No newline at end of file diff --git a/ArcFormats/app.config b/ArcFormats/app.config index 5a8f517e..18fb27af 100644 --- a/ArcFormats/app.config +++ b/ArcFormats/app.config @@ -163,6 +163,9 @@ + + + diff --git a/supported.html b/supported.html index a72ee4f6..95d33fda 100644 --- a/supported.html +++ b/supported.html @@ -1266,6 +1266,10 @@ Soukan Yuugi
Motto! Debutopia
Ura Seitokai yo, Koumon o Seiatsu Seyo!
+*FJSYSNoNSystem +Idol★Harem
+ +*.mgdMGDNo

1 Non-encrypted only