diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index 9d481fb6..52fb0ac0 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -535,11 +535,14 @@
-
-
-
- WidgetPAZ.xaml
-
+
+
+
+ CreatePAZWidget.xaml
+
+
+ WidgetPAZ.xaml
+
@@ -1134,14 +1137,18 @@
Designer
MSBuild:Compile
-
- Designer
- MSBuild:Compile
-
-
- MSBuild:Compile
- Designer
-
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
Designer
MSBuild:Compile
@@ -1320,4 +1327,4 @@ xcopy "$(ProjectDir)\Resources\Formats.dat" "$(TargetDir)\GameData\" /D /Y >N
-->
-
\ No newline at end of file
+
diff --git a/ArcFormats/Musica/ArcPAZ.cs b/ArcFormats/Musica/ArcPAZ.cs
index 178509c7..e4e40c02 100644
--- a/ArcFormats/Musica/ArcPAZ.cs
+++ b/ArcFormats/Musica/ArcPAZ.cs
@@ -26,6 +26,7 @@ using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
+using System.Text;
using GameRes.Compression;
using GameRes.Cryptography;
using GameRes.Formats.Strings;
@@ -133,7 +134,7 @@ namespace GameRes.Formats.Musica
public override string Description { get { return "Musica engine resource archive"; } }
public override uint Signature { get { return 0; } }
public override bool IsHierarchic { get { return true; } }
- public override bool CanWrite { get { return false; } }
+ public override bool CanWrite { get { return true; } }
public PazOpener ()
{
@@ -319,7 +320,44 @@ namespace GameRes.Formats.Musica
public override ResourceOptions GetDefaultOptions ()
{
- return new PazOptions { Scheme = GetScheme (Properties.Settings.Default.PAZTitle) };
+ return new PazOptions {
+ Scheme = GetScheme (Properties.Settings.Default.PAZTitle),
+ ArcName = Properties.Settings.Default.PAZArchiveKey,
+ CompressContents = Properties.Settings.Default.PAZCompressContents,
+ RetainDirs = Properties.Settings.Default.PAZRetainStructure,
+ Signature = 0,
+ };
+ }
+
+ public override ResourceOptions GetOptions (object widget)
+ {
+ var create = widget as GUI.CreatePAZWidget;
+ if (null == create)
+ return base.GetDefaultOptions ();
+
+ string title = create.SelectedTitle ?? string.Empty;
+ string archive = create.SelectedArchive ?? string.Empty;
+ bool compress = create.CompressContents;
+ bool retain = create.RetainStructure;
+
+ Properties.Settings.Default.PAZTitle = title;
+ Properties.Settings.Default.PAZArchiveKey = archive;
+ Properties.Settings.Default.PAZCompressContents = compress;
+ Properties.Settings.Default.PAZRetainStructure = retain;
+
+ var options = new PazOptions {
+ Scheme = GetScheme (title),
+ ArcName = archive,
+ CompressContents = compress,
+ RetainDirs = retain,
+ Signature = 0,
+ };
+ return options;
+ }
+
+ public override object GetCreationWidget ()
+ {
+ return new GUI.CreatePAZWidget (this);
}
public override object GetAccessWidget ()
@@ -327,6 +365,312 @@ namespace GameRes.Formats.Musica
return new GUI.WidgetPAZ (this);
}
+ public override void Create (Stream output, IEnumerable list, ResourceOptions options,
+ EntryCallback callback)
+ {
+ if (null == output)
+ throw new ArgumentNullException ("output");
+
+ var paz_options = GetOptions (options);
+ var scheme = paz_options.Scheme;
+ string arc_name = paz_options.ArcName ?? string.Empty;
+ PazKey keys = null;
+
+ bool use_crypto = scheme != null;
+ if (use_crypto)
+ {
+ if (string.IsNullOrEmpty (arc_name))
+ throw new InvalidOperationException ("Archive key is not specified.");
+ arc_name = arc_name.ToLowerInvariant ();
+ if (null == scheme.ArcKeys || !scheme.ArcKeys.TryGetValue (arc_name, out keys))
+ throw new InvalidOperationException (string.Format ("Unknown archive key '{0}'.", arc_name));
+ if (VideoPazNames.Contains (arc_name))
+ throw new NotSupportedException ("Creation of MOV PAZ archives is not supported.");
+ }
+ else
+ {
+ arc_name = string.Empty;
+ }
+
+ bool is_audio = use_crypto && AudioPazNames.Contains (arc_name);
+ bool retain_dirs = paz_options.RetainDirs;
+ bool compress_contents = paz_options.CompressContents;
+ byte xor_key = 0;
+
+ var encoding = Encodings.cp932.WithFatalFallback ();
+ var data_key = use_crypto ? keys.DataKey : null;
+ var index_key = use_crypto ? keys.IndexKey : null;
+ int version = scheme != null ? scheme.Version : 0;
+
+ var entries = new List();
+ var used_names = new HashSet (StringComparer.OrdinalIgnoreCase);
+ string temp_path = Path.GetTempFileName ();
+ long data_offset = 0;
+ int callback_index = 0;
+
+ try
+ {
+ using (var data_stream = new FileStream (temp_path, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ {
+ foreach (var entry in list)
+ {
+ string stored_name = retain_dirs ? entry.Name.Replace ("\\", "/") : Path.GetFileName (entry.Name);
+ if (string.IsNullOrWhiteSpace (stored_name))
+ continue;
+ if (!used_names.Add (stored_name))
+ continue;
+
+ if (null != callback)
+ callback (callback_index++, entry, arcStrings.MsgAddingFile);
+
+ var created = CreateEntryData (entry, stored_name, data_stream, ref data_offset,
+ compress_contents, is_audio, scheme, data_key, xor_key, encoding);
+ entries.Add (created);
+ }
+ }
+
+ if (null != callback)
+ callback (callback_index++, null, arcStrings.MsgWritingIndex);
+
+ long plain_index_size = CalculateIndexPlainSize (entries);
+ int aligned_index_size = (int)((plain_index_size + 7) & ~7);
+ uint index_size = (uint)aligned_index_size;
+ uint start_offset = version > 0 ? 0x20u : 0u;
+ long data_start = start_offset + 4 + aligned_index_size;
+
+ foreach (var item in entries)
+ item.Offset = data_start + item.DataOffset;
+
+ var index_buffer = BuildIndexBuffer (entries, aligned_index_size, index_key, xor_key);
+ uint stored_index_size = EncodeIndexSize (index_size, xor_key);
+
+ uint signature = version > 0 ? ResolveSignature (scheme, paz_options.Signature) : 0u;
+
+ output.Position = 0;
+ using (var writer = new BinaryWriter (output, Encoding.ASCII, true))
+ {
+ if (start_offset != 0)
+ {
+ writer.Write (signature);
+ if (start_offset > 4)
+ writer.Write (new byte[start_offset - 4]);
+ }
+ writer.Write (stored_index_size);
+ writer.Write (index_buffer);
+ }
+
+ output.Position = data_start;
+ using (var data_stream = new FileStream (temp_path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ data_stream.CopyTo (output);
+ }
+ }
+ finally
+ {
+ try { File.Delete (temp_path); } catch { }
+ }
+ }
+
+ static long CalculateIndexPlainSize (IList entries)
+ {
+ long size = 4;
+ foreach (var entry in entries)
+ size += entry.NameBytes.Length + 1 + 8 + 4 + 4 + 4 + 4;
+ return size;
+ }
+
+ byte[] BuildIndexBuffer (IList entries, int aligned_size, byte[] index_key, byte xor_key)
+ {
+ var index_stream = new MemoryStream (Math.Max (aligned_size, 8));
+ using (var writer = new BinaryWriter (index_stream, Encoding.ASCII, true))
+ {
+ writer.Write (entries.Count);
+ foreach (var entry in entries)
+ {
+ writer.Write (entry.NameBytes);
+ writer.Write ((byte)0);
+ writer.Write (entry.Offset);
+ writer.Write (entry.UnpackedSize);
+ writer.Write (entry.Size);
+ writer.Write (entry.AlignedSize);
+ writer.Write (entry.IsPacked ? 1 : 0);
+ }
+ writer.Flush ();
+ }
+ if (index_stream.Length < aligned_size)
+ index_stream.SetLength (aligned_size);
+ var buffer = index_stream.ToArray ();
+ if (buffer.Length > 0 && null != index_key)
+ {
+ var crypto = new Blowfish (index_key);
+ crypto.Encipher (buffer, buffer.Length);
+ }
+ if (xor_key != 0)
+ {
+ for (int i = 0; i < buffer.Length; ++i)
+ buffer[i] ^= xor_key;
+ }
+ return buffer;
+ }
+
+ static uint EncodeIndexSize (uint index_size, byte xor_key)
+ {
+ if (0 == xor_key)
+ return index_size;
+ uint mask = (uint)(xor_key << 24 | xor_key << 16 | xor_key << 8 | xor_key);
+ return index_size ^ mask;
+ }
+
+ uint ResolveSignature (PazScheme scheme, uint explicit_value)
+ {
+ if (explicit_value != 0)
+ return explicit_value;
+ foreach (var pair in KnownSchemes)
+ {
+ if (object.ReferenceEquals (pair.Value, scheme))
+ return pair.Key;
+ }
+ return 0u;
+ }
+
+ PazCreateEntry CreateEntryData (Entry entry, string stored_name, FileStream data_stream, ref long data_offset,
+ bool compress_contents, bool is_audio, PazScheme scheme, byte[] data_key,
+ byte xor_key, Encoding encoding)
+ {
+ string file_path = entry.Name;
+ using (var input = File.Open (file_path, FileMode.Open, FileAccess.Read))
+ {
+ long file_length = input.Length;
+ if (file_length < 0 || file_length > uint.MaxValue)
+ throw new FileSizeException (entry.Name);
+ if (file_length > int.MaxValue)
+ throw new FileSizeException (entry.Name);
+
+ var raw = new byte[file_length];
+ int read = 0;
+ while (read < raw.Length)
+ {
+ int chunk = input.Read (raw, read, raw.Length - read);
+ if (0 == chunk)
+ break;
+ read += chunk;
+ }
+ if (read != raw.Length)
+ throw new EndOfStreamException (entry.Name);
+
+ bool should_compress = compress_contents && ShouldCompressFile (entry) && raw.Length > 0;
+ byte[] payload = raw;
+ bool is_packed = false;
+ if (should_compress)
+ {
+ using (var packed_stream = new MemoryStream (raw.Length))
+ {
+ using (var zstream = new ZLibStream (packed_stream, CompressionMode.Compress, CompressionLevel.Level9, true))
+ {
+ zstream.Write (raw, 0, raw.Length);
+ }
+ var packed = packed_stream.ToArray ();
+ if (packed.Length < raw.Length)
+ {
+ payload = packed;
+ is_packed = true;
+ }
+ }
+ }
+
+ var info = new PazCreateEntry ();
+ info.Name = stored_name;
+ try
+ {
+ info.NameBytes = encoding.GetBytes (stored_name);
+ }
+ catch (EncoderFallbackException X)
+ {
+ throw new InvalidFileName (stored_name, arcStrings.MsgIllegalCharacters, X);
+ }
+
+ info.UnpackedSize = (uint)raw.Length;
+ info.Size = (uint)payload.Length;
+ info.IsPacked = is_packed;
+
+ byte[] aligned;
+ if (0 == (payload.Length & 7))
+ {
+ aligned = (byte[])payload.Clone ();
+ }
+ else
+ {
+ int aligned_size = (payload.Length + 7) & ~7;
+ aligned = new byte[aligned_size];
+ Buffer.BlockCopy (payload, 0, aligned, 0, payload.Length);
+ }
+
+ int version = scheme != null ? scheme.Version : 0;
+ if (version > 0)
+ {
+ string password = string.Empty;
+ if (!is_packed && scheme != null && scheme.TypeKeys != null)
+ password = scheme.GetTypePassword (stored_name, is_audio);
+ if (!string.IsNullOrEmpty (password))
+ {
+ string key_string = string.Format ("{0} {1:X08} {2}", stored_name.ToLowerInvariant(), info.UnpackedSize, password);
+ var key_bytes = Encodings.cp932.GetBytes (key_string);
+ var rc4 = new Rc4Transform (key_bytes);
+ if (version >= 2)
+ {
+ uint crc = Crc32.Compute (key_bytes, 0, key_bytes.Length);
+ int skip = (int)(crc >> 12) & 0xFF;
+ for (int i = 0; i < skip; ++i)
+ rc4.NextByte ();
+ }
+ rc4.TransformBlock (aligned, 0, payload.Length, aligned, 0);
+ }
+ }
+
+ if (aligned.Length > 0 && null != data_key)
+ {
+ var crypto = new Blowfish (data_key);
+ crypto.Encipher (aligned, aligned.Length);
+ }
+
+ if (xor_key != 0)
+ {
+ for (int i = 0; i < aligned.Length; ++i)
+ aligned[i] ^= xor_key;
+ }
+
+ info.AlignedSize = (uint)aligned.Length;
+ info.DataOffset = data_offset;
+
+ data_stream.Position = data_offset;
+ data_stream.Write (aligned, 0, aligned.Length);
+ data_offset += aligned.Length;
+
+ return info;
+ }
+ }
+
+ bool ShouldCompressFile (Entry entry)
+ {
+ if ("image" == entry.Type || "archive" == entry.Type)
+ return false;
+ if (entry.Name.HasExtension (".ogg"))
+ return false;
+ return true;
+ }
+
+ class PazCreateEntry
+ {
+ public string Name;
+ public byte[] NameBytes;
+ public long DataOffset;
+ public long Offset;
+ public uint UnpackedSize;
+ public uint Size;
+ public uint AlignedSize;
+ public bool IsPacked;
+ }
+
PazScheme GetScheme (string title)
{
PazScheme scheme;
@@ -395,5 +739,9 @@ namespace GameRes.Formats.Musica
public class PazOptions : ResourceOptions
{
public PazScheme Scheme;
+ public string ArcName;
+ public bool CompressContents;
+ public bool RetainDirs;
+ public uint Signature;
}
}
diff --git a/ArcFormats/Musica/CreatePAZWidget.xaml b/ArcFormats/Musica/CreatePAZWidget.xaml
new file mode 100644
index 00000000..59be7b2a
--- /dev/null
+++ b/ArcFormats/Musica/CreatePAZWidget.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ArcFormats/Musica/CreatePAZWidget.xaml.cs b/ArcFormats/Musica/CreatePAZWidget.xaml.cs
new file mode 100644
index 00000000..a9d8f5c8
--- /dev/null
+++ b/ArcFormats/Musica/CreatePAZWidget.xaml.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Controls;
+using GameRes.Formats.Musica;
+using GameRes.Formats.Properties;
+using GameRes.Formats.Strings;
+
+namespace GameRes.Formats.GUI
+{
+ ///
+ /// Interaction logic for CreatePAZWidget.xaml
+ ///
+ public partial class CreatePAZWidget : Grid
+ {
+ readonly PazOpener m_paz;
+
+ public CreatePAZWidget (PazOpener paz)
+ {
+ m_paz = paz;
+ InitializeComponent ();
+ Title.SelectionChanged += TitleSelectionChanged;
+ PopulateTitles ();
+ UpdateArchiveList ();
+ }
+
+ void PopulateTitles ()
+ {
+ var titles = new List { arcStrings.ArcNoEncryption };
+ titles.AddRange (m_paz.KnownTitles.Keys.OrderBy (x => x));
+ Title.ItemsSource = titles;
+ string saved_title = Settings.Default.PAZTitle;
+ if (!string.IsNullOrEmpty (saved_title) && titles.Contains (saved_title))
+ Title.SelectedValue = saved_title;
+ else if (titles.Count > 0)
+ Title.SelectedIndex = 0;
+ }
+
+ void TitleSelectionChanged (object sender, SelectionChangedEventArgs e)
+ {
+ UpdateArchiveList ();
+ }
+
+ void UpdateArchiveList ()
+ {
+ string title = Title.SelectedValue as string;
+ PazScheme scheme = null;
+ if (!string.IsNullOrEmpty (title))
+ m_paz.KnownTitles.TryGetValue (title, out scheme);
+
+ List arc_keys = (scheme != null && scheme.ArcKeys != null)
+ ? scheme.ArcKeys.Keys.OrderBy (x => x).ToList ()
+ : new List ();
+
+ Archive.ItemsSource = arc_keys;
+ string saved_key = Settings.Default.PAZArchiveKey;
+ if (!string.IsNullOrEmpty (saved_key) && arc_keys.Contains (saved_key))
+ Archive.SelectedValue = saved_key;
+ else if (arc_keys.Count > 0)
+ Archive.SelectedIndex = 0;
+ else
+ Archive.Text = scheme != null ? (saved_key ?? string.Empty) : string.Empty;
+
+ Archive.IsEnabled = scheme != null;
+ }
+
+ public string SelectedTitle
+ {
+ get { return Title.SelectedValue as string ?? Title.Text; }
+ }
+
+ public string SelectedArchive
+ {
+ get
+ {
+ if (!Archive.IsEnabled)
+ return string.Empty;
+ var selected = Archive.SelectedValue as string;
+ if (!string.IsNullOrEmpty (selected))
+ return selected;
+ return Archive.Text ?? string.Empty;
+ }
+ }
+
+ public bool CompressContents
+ {
+ get { return Compress.IsChecked ?? false; }
+ }
+
+ public bool RetainStructure
+ {
+ get { return Retain.IsChecked ?? false; }
+ }
+ }
+}
diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs
index 7cae9633..11c77d6d 100644
--- a/ArcFormats/Properties/Settings.Designer.cs
+++ b/ArcFormats/Properties/Settings.Designer.cs
@@ -637,14 +637,50 @@ namespace GameRes.Formats.Properties {
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
- public string PAZTitle {
- get {
- return ((string)(this["PAZTitle"]));
- }
- set {
- this["PAZTitle"] = value;
- }
- }
+ public string PAZTitle {
+ get {
+ return ((string)(this["PAZTitle"]));
+ }
+ set {
+ this["PAZTitle"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("")]
+ public string PAZArchiveKey {
+ get {
+ return ((string)(this["PAZArchiveKey"]));
+ }
+ set {
+ this["PAZArchiveKey"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("False")]
+ public bool PAZCompressContents {
+ get {
+ return ((bool)(this["PAZCompressContents"]));
+ }
+ set {
+ this["PAZCompressContents"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("True")]
+ public bool PAZRetainStructure {
+ get {
+ return ((bool)(this["PAZRetainStructure"]));
+ }
+ set {
+ this["PAZRetainStructure"] = value;
+ }
+ }
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings
index 4b4c6753..65249a40 100644
--- a/ArcFormats/Properties/Settings.settings
+++ b/ArcFormats/Properties/Settings.settings
@@ -155,9 +155,18 @@
-
-
-
+
+
+
+
+
+
+
+ False
+
+
+ True
+
@@ -201,4 +210,4 @@
932
-
\ No newline at end of file
+
diff --git a/ArcFormats/app.config b/ArcFormats/app.config
index 9d9ed542..4b66151b 100644
--- a/ArcFormats/app.config
+++ b/ArcFormats/app.config
@@ -157,9 +157,18 @@
-
-
-
+
+
+
+
+
+
+
+ False
+
+
+ True
+