From 3e2bab9dac094f46106709280c4ebff87553e5b2 Mon Sep 17 00:00:00 2001 From: morkt Date: Tue, 29 Jul 2014 07:02:49 +0400 Subject: [PATCH] implemented Steins;Gate archive creation. --- ArcFormats/ArcFormats.csproj | 7 + ArcFormats/ArcSteinsGate.cs | 243 ++++++++++++++++++++- ArcFormats/CreateSGWidget.xaml | 16 ++ ArcFormats/CreateSGWidget.xaml.cs | 16 ++ ArcFormats/Properties/Settings.Designer.cs | 12 + ArcFormats/Properties/Settings.settings | 3 + ArcFormats/Strings/arcStrings.Designer.cs | 9 + ArcFormats/Strings/arcStrings.resx | 3 + ArcFormats/Strings/arcStrings.ru-RU.resx | 3 + ArcFormats/app.config | 3 + 10 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 ArcFormats/CreateSGWidget.xaml create mode 100644 ArcFormats/CreateSGWidget.xaml.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index a6ef9dd4..e123709c 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -63,6 +63,9 @@ + + CreateSGWidget.xaml + CreateXP3Widget.xaml @@ -119,6 +122,10 @@ + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/ArcFormats/ArcSteinsGate.cs b/ArcFormats/ArcSteinsGate.cs index 4dfd988f..ffb6b4e2 100644 --- a/ArcFormats/ArcSteinsGate.cs +++ b/ArcFormats/ArcSteinsGate.cs @@ -2,16 +2,43 @@ //! \date Thu Jul 24 23:36:01 2014 //! \brief Nitro+ Steins;Gate archive implementation. // +// Copyright (C) 2014 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.IO; using System.Text; +using System.Linq; using System.Collections.Generic; using System.ComponentModel.Composition; using GameRes.Formats.Strings; using GameRes.Utility; +using GameRes.Formats.Properties; namespace GameRes.Formats { + public class SteinsGateOptions : ResourceOptions + { + public Encoding FileNameEncoding { get; set; } + } + [Export(typeof(ArchiveFormat))] public class NpaSteinsGateOpener : ArchiveFormat { @@ -19,6 +46,7 @@ namespace GameRes.Formats public override string Description { get { return arcStrings.NPASteinsGateDescription; } } public override uint Signature { get { return 0; } } public override bool IsHierarchic { get { return true; } } + public override bool CanCreate { get { return true; } } public NpaSteinsGateOpener () { @@ -37,22 +65,25 @@ namespace GameRes.Formats return null; var stream = new SteinsGateEncryptedStream (file, 4, (uint)index_size); - using (var header = new BinaryReader (stream, Encoding.Unicode)) + using (var header = new BinaryReader (stream)) { int entry_count = header.ReadInt32(); if (entry_count <= 0) return null; + index_size -= 4; int average_entry_size = index_size / entry_count; if (average_entry_size < 0x11) return null; - var dir = new List ((int)entry_count); - for (uint i = 0; i < entry_count; ++i) + var dir = new List (entry_count); + for (int i = 0; i < entry_count; ++i) { int name_length = header.ReadInt32(); if (name_length+0x10 > index_size) return null; - string filename = new string (header.ReadChars (name_length/2)); + byte[] name_raw = header.ReadBytes (name_length); + Encoding enc = GuessEncoding (name_raw); + string filename = enc.GetString (name_raw); var entry = FormatCatalog.Instance.CreateEntry (filename); entry.Size = header.ReadUInt32(); @@ -79,19 +110,179 @@ namespace GameRes.Formats buffer[offset+i] ^= KeyString[i & 7]; } } + + Encoding GuessEncoding (byte[] text) + { + bool has_zero = false; + bool has_non_ascii = false; + foreach (var symbol in text) + { + if (0 == symbol) + { + has_zero = true; + break; + } + else if (symbol > 0x7f) + { + has_non_ascii = true; + } + } + if (has_zero) + return Encoding.Unicode; + else if (has_non_ascii) + return Encodings.cp932; + else + return Encoding.ASCII; + } + + public override ResourceOptions GetDefaultOptions () + { + return new SteinsGateOptions { + FileNameEncoding = GetEncoding (Settings.Default.SGFileNameEncoding), + }; + } + + public override object GetCreationWidget () + { + return new GUI.CreateSGWidget(); + } + + Encoding GetEncoding (string name) + { + if ("shift-jis" == name) + return Encodings.cp932; + if ("utf-16" == name) + return Encoding.Unicode; + return Encoding.Default; + } + + internal class RawEntry : Entry + { + public byte[] IndexName; + } + + public override void Create (Stream output, IEnumerable list, ResourceOptions options, + EntryCallback callback) + { + var sg_options = GetOptions (options); + Encoding encoding = Encodings.WithFatalFallback (sg_options.FileNameEncoding); + long start_pos = output.Position; + int callback_count = 0; + + uint index_size = 4; + int entry_count = list.Count(); + var real_entry_list = new List (entry_count); + var used_names = new HashSet(); + foreach (var entry in list) + { + string name = entry.Name.Replace (@"\", "/"); + if (!used_names.Add (name)) // duplicate name + continue; + var header_entry = new RawEntry { Name = entry.Name }; + try + { + header_entry.IndexName = encoding.GetBytes (name); + } + catch (EncoderFallbackException X) + { + throw new InvalidFileName (entry.Name, arcStrings.MsgIllegalCharacters, X); + } + index_size += (uint)header_entry.IndexName.Length + 16; + real_entry_list.Add (header_entry); + } + output.Seek (4+index_size, SeekOrigin.Current); + foreach (var entry in real_entry_list) + { + using (var input = File.Open (entry.Name, FileMode.Open, FileAccess.Read)) + { + var file_size = input.Length; + if (file_size > uint.MaxValue) + throw new FileSizeException(); + entry.Offset = output.Position; + entry.Size = (uint)file_size; + if (null != callback) + callback (callback_count++, entry, arcStrings.MsgAddingFile); + using (var stream = new SteinsGateEncryptedStream (output)) + input.CopyTo (stream); + } + } + if (null != callback) + callback (callback_count++, null, arcStrings.MsgWritingIndex); + output.Position = start_pos; + output.WriteByte ((byte)(index_size & 0xff)); + output.WriteByte ((byte)((index_size >> 8) & 0xff)); + output.WriteByte ((byte)((index_size >> 16) & 0xff)); + output.WriteByte ((byte)((index_size >> 24) & 0xff)); + var encrypted_stream = new SteinsGateEncryptedStream (output); + using (var header = new BinaryWriter (encrypted_stream)) + { + header.Write (entry_count); + foreach (var entry in real_entry_list) + { + header.Write (entry.IndexName.Length); + header.Write (entry.IndexName); + header.Write ((uint)entry.Size); + header.Write ((long)entry.Offset); + } + } + } } - public class SteinsGateEncryptedStream : ArcView.ArcStream + public class SteinsGateEncryptedStream : Stream { - public SteinsGateEncryptedStream (ArcView file, long offset, uint size) - : base (file, offset, size) + private Stream m_stream; + private long m_base_pos; + private bool m_should_dispose; + + public Stream BaseStream { get { return m_stream; } } + + public override bool CanRead { get { return m_stream.CanRead; } } + public override bool CanSeek { get { return m_stream.CanSeek; } } + public override bool CanWrite { get { return m_stream.CanWrite; } } + public override long Length { get { return m_stream.Length - m_base_pos; } } + public override long Position { + get { return m_stream.Position - m_base_pos; } + set { m_stream.Position = m_base_pos + value; } + } + + public SteinsGateEncryptedStream (ArcView file, long offset, uint size) + { + m_stream = file.CreateStream (offset, size); + m_should_dispose = true; + m_base_pos = 0; + } + + public SteinsGateEncryptedStream (Stream output) + { + m_stream = output; + m_should_dispose = false; + m_base_pos = m_stream.Position; + } + + #region System.IO.Stream methods + public override void Flush() + { + m_stream.Flush(); + } + + public override long Seek (long offset, SeekOrigin origin) + { + if (SeekOrigin.Begin == origin) + offset += m_base_pos; + offset = m_stream.Seek (offset, origin); + return offset - m_base_pos; + } + + public override void SetLength (long length) + { + throw new System.NotSupportedException ("SteinsGateEncryptedStream.SetLength method is not supported"); } public override int Read (byte[] buffer, int offset, int count) { int position = (int)Position & 7; - int read = base.Read (buffer, offset, count); + int read = m_stream.Read (buffer, offset, count); if (read > 0) { for (int i = 0; i < read; ++i) @@ -105,12 +296,46 @@ namespace GameRes.Formats public override int ReadByte () { int position = (int)Position & 7; - int b = base.ReadByte(); + int b = m_stream.ReadByte(); if (-1 != b) { b ^= NpaSteinsGateOpener.KeyString[position]; } return b; } + + public override void Write (byte[] buffer, int offset, int count) + { + int position = (int)Position & 7; + byte[] write_buf = new byte[count]; + for (int i = 0; i < count; ++i) + { + write_buf[i] = (byte)(buffer[offset+i] ^ NpaSteinsGateOpener.KeyString[(position+i)&7]); + } + m_stream.Write (write_buf, 0, count); + } + + public override void WriteByte (byte value) + { + int position = (int)Position & 7; + m_stream.WriteByte ((byte)(value ^ NpaSteinsGateOpener.KeyString[position])); + } + #endregion + + #region IDisposable Members + bool disposed = false; + protected override void Dispose (bool disposing) + { + if (!disposed) + { + if (disposing && m_should_dispose) + { + m_stream.Dispose(); + } + disposed = true; + base.Dispose (disposing); + } + } + #endregion } } diff --git a/ArcFormats/CreateSGWidget.xaml b/ArcFormats/CreateSGWidget.xaml new file mode 100644 index 00000000..6ceaaa75 --- /dev/null +++ b/ArcFormats/CreateSGWidget.xaml @@ -0,0 +1,16 @@ + + + + diff --git a/ArcFormats/CreateSGWidget.xaml.cs b/ArcFormats/CreateSGWidget.xaml.cs new file mode 100644 index 00000000..e78df245 --- /dev/null +++ b/ArcFormats/CreateSGWidget.xaml.cs @@ -0,0 +1,16 @@ +using System.Windows; +using System.Windows.Controls; + +namespace GameRes.Formats.GUI +{ + /// + /// Interaction logic for CreateSGWidget.xaml + /// + public partial class CreateSGWidget : Grid + { + public CreateSGWidget () + { + InitializeComponent (); + } + } +} diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs index 914db947..a9445ed2 100644 --- a/ArcFormats/Properties/Settings.Designer.cs +++ b/ArcFormats/Properties/Settings.Designer.cs @@ -117,5 +117,17 @@ namespace GameRes.Formats.Properties { this["XP3RetainStructure"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("shift-jis")] + public string SGFileNameEncoding { + get { + return ((string)(this["SGFileNameEncoding"])); + } + set { + this["SGFileNameEncoding"] = value; + } + } } } diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings index 1df61621..ed32b483 100644 --- a/ArcFormats/Properties/Settings.settings +++ b/ArcFormats/Properties/Settings.settings @@ -26,5 +26,8 @@ False + + shift-jis + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.Designer.cs b/ArcFormats/Strings/arcStrings.Designer.cs index e3693097..03932d18 100644 --- a/ArcFormats/Strings/arcStrings.Designer.cs +++ b/ArcFormats/Strings/arcStrings.Designer.cs @@ -261,6 +261,15 @@ namespace GameRes.Formats.Strings { } } + /// + /// Looks up a localized string similar to Filename encoding. + /// + public static string SGLabelEncoding { + get { + return ResourceManager.GetString("SGLabelEncoding", resourceCulture); + } + } + /// /// Looks up a localized string similar to Liar-soft game resource archive. /// diff --git a/ArcFormats/Strings/arcStrings.resx b/ArcFormats/Strings/arcStrings.resx index 65feab3e..2c752c56 100644 --- a/ArcFormats/Strings/arcStrings.resx +++ b/ArcFormats/Strings/arcStrings.resx @@ -186,6 +186,9 @@ predefined encryption scheme. Amaterasu Translations Muv-Luv script file + + Filename encoding + Liar-soft game resource archive diff --git a/ArcFormats/Strings/arcStrings.ru-RU.resx b/ArcFormats/Strings/arcStrings.ru-RU.resx index ec5f4ac0..9b1a589e 100644 --- a/ArcFormats/Strings/arcStrings.ru-RU.resx +++ b/ArcFormats/Strings/arcStrings.ru-RU.resx @@ -159,6 +159,9 @@ Записывается оглавление... + + Кодировка имён файлов + Сжать содержимое diff --git a/ArcFormats/app.config b/ArcFormats/app.config index a447bb45..7f2bf1c5 100644 --- a/ArcFormats/app.config +++ b/ArcFormats/app.config @@ -28,6 +28,9 @@ False + + shift-jis + \ No newline at end of file