diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index 561fabfc..e50c5517 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -74,6 +74,9 @@
CreateINTWidget.xaml
+
+ CreateNPAWidget.xaml
+
CreateONSWidget.xaml
@@ -153,6 +156,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/ArcFormats/ArcNPA.cs b/ArcFormats/ArcNPA.cs
index 6ad4aa4c..2d87c016 100644
--- a/ArcFormats/ArcNPA.cs
+++ b/ArcFormats/ArcNPA.cs
@@ -25,6 +25,8 @@
using System;
using System.IO;
+using System.Linq;
+using System.Text;
using System.ComponentModel.Composition;
using System.Collections.Generic;
using System.Runtime.InteropServices;
@@ -32,11 +34,12 @@ using ZLibNet;
using GameRes.Formats.Strings;
using GameRes.Formats.Properties;
-namespace GameRes.Formats
+namespace GameRes.Formats.NitroPlus
{
internal class NpaEntry : PackedEntry
{
public byte[] RawName;
+ public int FolderId;
}
internal class NpaArchive : ArcFile
@@ -67,18 +70,25 @@ namespace GameRes.Formats
public class NpaOptions : ResourceOptions
{
- public NpaTitleId TitleId { get; set; }
+ public NpaTitleId TitleId { get; set; }
+ public bool CompressContents { get; set; }
+ public int Key1 { get; set; }
+ public int Key2 { get; set; }
}
[Export(typeof(ArchiveFormat))]
public class NpaOpener : ArchiveFormat
{
- public override string Tag { get { return "NPA"; } }
+ public override string Tag { get { return "NPA"; } }
public override string Description { get { return arcStrings.NPADescription; } }
- public override uint Signature { get { return 0x0141504e; } } // NPA\x01
- public override bool IsHierarchic { get { return true; } }
+ public override uint Signature { get { return 0x0141504e; } } // NPA\x01
+ public override bool IsHierarchic { get { return true; } }
+ public override bool CanCreate { get { return true; } }
- /// Known encryption schemes.
+ ///
+ /// Known encryption schemes.
+ /// Order should match NpaTitleId enumeration.
+ ///
public static readonly string[] KnownSchemes = new string[] {
arcStrings.ArcNoEncryption,
"Chaos;Head", "Chaos;Head Trial 1", "Chaos;Head Trial 2", "Muramasa Trial", "Muramasa",
@@ -89,6 +99,9 @@ namespace GameRes.Formats
"Kimi to Kanojo to Kanojo no Koi", "Phenomeno", "Nekoda -Nyanda-",
};
+ public const int DefaultKey1 = 0x4147414e;
+ public const int DefaultKey2 = 0x21214f54;
+
public override ArcFile TryOpen (ArcView file)
{
int key1 = file.View.ReadInt32 (7);
@@ -130,7 +143,7 @@ namespace GameRes.Formats
raw_name[x] += DecryptName (x, i, key);
var info_offset = cur_offset + 5 + name_size;
- uint id = file.View.ReadUInt32 (info_offset);
+ int id = file.View.ReadInt32 (info_offset);
uint offset = file.View.ReadUInt32 (info_offset+4);
uint size = file.View.ReadUInt32 (info_offset+8);
uint unpacked_size = file.View.ReadUInt32 (info_offset+12);
@@ -140,12 +153,13 @@ namespace GameRes.Formats
Offset = dir_size+offset+41,
Size = size,
UnpackedSize = unpacked_size,
- IsPacked = compressed,
RawName = raw_name,
+ FolderId = id,
};
if (!entry.CheckPlacement (file.MaxOffset))
return null;
entry.Type = FormatCatalog.Instance.GetTypeFromName (entry.Name);
+ entry.IsPacked = compressed && entry.Type != "image";
dir.Add (entry);
}
cur_offset += 4 + name_size + 17;
@@ -156,23 +170,94 @@ namespace GameRes.Formats
return new ArcFile (file, this, dir);
}
+ public override void Create (Stream output, IEnumerable list, ResourceOptions options,
+ EntryCallback callback)
+ {
+ var npa_options = GetOptions (options);
+ int callback_count = 0;
+
+ // build file index
+ var index = new Indexer (list, npa_options);
+
+ output.Position = 41 + index.Size;
+ long data_offset = 0;
+
+ // write files
+ foreach (var entry in index.Entries.Where (e => e.Type != "directory"))
+ {
+ if (data_offset > uint.MaxValue)
+ throw new FileSizeException();
+ if (null != callback)
+ callback (callback_count++, entry, arcStrings.MsgAddingFile);
+ using (var file = File.OpenRead (entry.Name))
+ {
+ var size = file.Length;
+ if (size > uint.MaxValue)
+ throw new FileSizeException();
+ entry.Offset = data_offset;
+ entry.UnpackedSize = (uint)size;
+ if (entry.IsPacked)
+ {
+ using (var zstream = new ZLibStream (output, CompressionMode.Compress,
+ CompressionLevel.Level9, true))
+ {
+ file.CopyTo (zstream);
+ zstream.Flush();
+ entry.Size = (uint)zstream.TotalOut;
+ }
+ }
+ else
+ {
+ file.CopyTo (output);
+ entry.Size = entry.UnpackedSize;
+ }
+ data_offset += entry.Size;
+ }
+ }
+ if (null != callback)
+ callback (callback_count++, null, arcStrings.MsgWritingIndex);
+
+ output.Position = 0;
+ using (var header = new BinaryWriter (output, Encoding.ASCII, true))
+ {
+ header.Write (Signature);
+ header.Write ((short)0);
+ header.Write ((byte)0);
+ header.Write (npa_options.Key1);
+ header.Write (npa_options.Key2);
+ header.Write (npa_options.CompressContents);
+ header.Write (npa_options.TitleId != NpaTitleId.NotEncrypted);
+ header.Write (index.TotalCount);
+ header.Write (index.FolderCount);
+ header.Write (index.FileCount);
+ header.Write ((long)0);
+ header.Write (index.Size);
+ foreach (var entry in index.Entries)
+ {
+ header.Write (entry.RawName.Length);
+ header.Write (entry.RawName);
+ header.Write ((byte)("directory" == entry.Type ? 1 : 2));
+ header.Write (entry.FolderId);
+ header.Write ((uint)entry.Offset);
+ header.Write (entry.Size);
+ header.Write (entry.UnpackedSize);
+ }
+ }
+ }
+
public override Stream OpenEntry (ArcFile arc, Entry entry)
{
if (arc is NpaArchive && entry is NpaEntry)
return OpenEncryptedEntry (arc as NpaArchive, entry as NpaEntry);
var input = arc.File.CreateStream (entry.Offset, entry.Size);
- return UnpackEntry (input, entry);
+ return UnpackEntry (input, entry as PackedEntry);
}
- private Stream UnpackEntry (Stream input, Entry entry)
+ private Stream UnpackEntry (Stream input, PackedEntry entry)
{
- if (entry.Type != "image")
- {
- var npa_entry = entry as PackedEntry;
- if (null != npa_entry && npa_entry.IsPacked)
- return new ZLibStream (input, CompressionMode.Decompress);
- }
+ if (null != entry && entry.IsPacked)
+ return new ZLibStream (input, CompressionMode.Decompress);
return input;
}
@@ -225,7 +310,7 @@ namespace GameRes.Formats
}
}
- byte DecryptName (int index, int curfile, int arc_key)
+ public static byte DecryptName (int index, int curfile, int arc_key)
{
int key = 0xFC*index;
@@ -242,7 +327,7 @@ namespace GameRes.Formats
return (byte)(key & 0xff);
}
- byte GetKeyFromEntry (NpaEntry entry, NpaTitleId game_id, int key2)
+ internal static byte GetKeyFromEntry (NpaEntry entry, NpaTitleId game_id, int key2)
{
int key1;
switch (game_id)
@@ -311,19 +396,12 @@ namespace GameRes.Formats
public override ResourceOptions GetDefaultOptions ()
{
- return new NpaOptions { TitleId = Settings.Default.NPAScheme };
- }
-
- public override ResourceOptions GetOptions (object w)
- {
- var widget = w as GUI.WidgetNPA;
- if (null != widget)
- {
- NpaTitleId scheme = GetTitleId (widget.GetScheme());
- Settings.Default.NPAScheme = scheme;
- return new NpaOptions { TitleId = scheme };
- }
- return this.GetDefaultOptions();
+ return new NpaOptions {
+ TitleId = GetTitleId (Settings.Default.NPAScheme),
+ CompressContents = Settings.Default.NPACompressContents,
+ Key1 = (int)Settings.Default.NPAKey1,
+ Key2 = (int)Settings.Default.NPAKey2,
+ };
}
public override object GetAccessWidget ()
@@ -331,6 +409,11 @@ namespace GameRes.Formats
return new GUI.WidgetNPA();
}
+ public override object GetCreationWidget ()
+ {
+ return new GUI.CreateNPAWidget();
+ }
+
NpaTitleId QueryGameEncryption ()
{
var options = Query (arcStrings.ArcEncryptedNotice);
@@ -419,4 +502,110 @@ namespace GameRes.Formats
new byte[] { 0xdc,0xdc,0xec,0xcd,0xdb,0xdc,0xdc,0xdc,0xdc,0xdc,0xdc,0xdc,0xdc,0xdc,0xdc,0xdc,0x1e,0x4e,0x66,0xb6 },
};
}
+
+ ///
+ /// Archive creation helper.
+ ///
+ internal class Indexer
+ {
+ List m_entries;
+ Encoding m_encoding = Encodings.cp932.WithFatalFallback();
+ int m_key;
+ int m_size = 0;
+ int m_directory_count = 0;
+ int m_file_count = 0;
+
+ public IEnumerable Entries { get { return m_entries; } }
+
+ public int Key { get { return m_key; } }
+ public int Size { get { return m_size; } }
+ public int TotalCount { get { return m_entries.Count; } }
+ public int FolderCount { get { return m_directory_count; } }
+ public int FileCount { get { return m_file_count; } }
+
+ public Indexer (IEnumerable source_list, NpaOptions options)
+ {
+ m_entries = new List (source_list.Count());
+ var game_id = options.TitleId;
+ if (game_id == NpaTitleId.LAMENTO || game_id == NpaTitleId.LAMENTOTR)
+ m_key = options.Key1 + options.Key2;
+ else
+ m_key = options.Key1 * options.Key2;
+
+ foreach (var entry in source_list)
+ {
+ string name = entry.Name;
+ var dir = Path.GetDirectoryName (name);
+ int folder_id = 0;
+ if (!string.IsNullOrEmpty (dir))
+ folder_id = AddDirectory (dir);
+
+ bool compress = options.CompressContents;
+ if (compress) // don't compress images
+ compress = !FormatCatalog.Instance.LookupFileName (name).OfType().Any();
+ var npa_entry = new NpaEntry
+ {
+ Name = name,
+ IsPacked = compress,
+ RawName = EncodeName (name, m_entries.Count),
+ FolderId = folder_id,
+ };
+ ++m_file_count;
+ AddEntry (npa_entry);
+ }
+ }
+
+ byte[] EncodeName (string name, int entry_number)
+ {
+ try
+ {
+ byte[] raw_name = m_encoding.GetBytes (name);
+ for (int i = 0; i < name.Length; ++i)
+ raw_name[i] -= NpaOpener.DecryptName (i, entry_number, m_key);
+ return raw_name;
+ }
+ catch (EncoderFallbackException X)
+ {
+ throw new InvalidFileName (name, arcStrings.MsgIllegalCharacters, X);
+ }
+ }
+
+ void AddEntry (NpaEntry entry)
+ {
+ m_entries.Add (entry);
+ m_size += 4 + entry.RawName.Length + 17;
+ }
+
+ Dictionary m_directory_map = new Dictionary();
+
+ int AddDirectory (string dir)
+ {
+ int folder_id = 0;
+ if (m_directory_map.TryGetValue (dir, out folder_id))
+ return folder_id;
+ string path = "";
+ foreach (var component in dir.Split (Path.DirectorySeparatorChar))
+ {
+ path = Path.Combine (path, component);
+ if (m_directory_map.TryGetValue (path, out folder_id))
+ continue;
+ folder_id = ++m_directory_count;
+ m_directory_map[path] = folder_id;
+
+ var npa_entry = new NpaEntry
+ {
+ Name = path,
+ Type = "directory",
+ Offset = 0,
+ Size = 0,
+ UnpackedSize = 0,
+ IsPacked = false,
+ RawName = EncodeName (path, m_entries.Count),
+ FolderId = folder_id,
+ };
+ m_entries.Add (npa_entry);
+ }
+ return folder_id;
+ }
+ }
}
diff --git a/ArcFormats/CreateNPAWidget.xaml b/ArcFormats/CreateNPAWidget.xaml
new file mode 100644
index 00000000..ee0ef456
--- /dev/null
+++ b/ArcFormats/CreateNPAWidget.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ArcFormats/CreateNPAWidget.xaml.cs b/ArcFormats/CreateNPAWidget.xaml.cs
new file mode 100644
index 00000000..efeddc53
--- /dev/null
+++ b/ArcFormats/CreateNPAWidget.xaml.cs
@@ -0,0 +1,24 @@
+using System.Windows.Controls;
+using GameRes.Formats.NitroPlus;
+
+namespace GameRes.Formats.GUI
+{
+ ///
+ /// Interaction logic for CreateNPAWidget.xaml
+ ///
+ public partial class CreateNPAWidget : Grid
+ {
+ public CreateNPAWidget ()
+ {
+ InitializeComponent ();
+ }
+
+ private void Reset_Click (object sender, System.Windows.RoutedEventArgs e)
+ {
+ this.EncryptionWidget.Scheme.SelectedIndex = 0;
+ this.Key1Box.Text = NpaOpener.DefaultKey1.ToString ("X8");
+ this.Key2Box.Text = NpaOpener.DefaultKey2.ToString ("X8");
+ this.CompressContents.IsChecked = false;
+ }
+ }
+}
diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs
index 64b0d991..8b41edd0 100644
--- a/ArcFormats/Properties/Settings.Designer.cs
+++ b/ArcFormats/Properties/Settings.Designer.cs
@@ -25,10 +25,10 @@ namespace GameRes.Formats.Properties {
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Configuration.DefaultSettingValueAttribute("NotEncrypted")]
- public global::GameRes.Formats.NpaTitleId NPAScheme {
+ [global::System.Configuration.DefaultSettingValueAttribute("")]
+ public string NPAScheme {
get {
- return ((global::GameRes.Formats.NpaTitleId)(this["NPAScheme"]));
+ return ((string)(this["NPAScheme"]));
}
set {
this["NPAScheme"] = value;
@@ -201,5 +201,41 @@ namespace GameRes.Formats.Properties {
this["RPAKey"] = value;
}
}
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("False")]
+ public bool NPACompressContents {
+ get {
+ return ((bool)(this["NPACompressContents"]));
+ }
+ set {
+ this["NPACompressContents"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("1095188814")]
+ public uint NPAKey1 {
+ get {
+ return ((uint)(this["NPAKey1"]));
+ }
+ set {
+ this["NPAKey1"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("555831124")]
+ public uint NPAKey2 {
+ get {
+ return ((uint)(this["NPAKey2"]));
+ }
+ set {
+ this["NPAKey2"] = value;
+ }
+ }
}
}
diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings
index 638a30e5..da1a068d 100644
--- a/ArcFormats/Properties/Settings.settings
+++ b/ArcFormats/Properties/Settings.settings
@@ -2,8 +2,8 @@
-
- NotEncrypted
+
+
@@ -47,5 +47,14 @@
1111638594
+
+ False
+
+
+ 1095188814
+
+
+ 555831124
+
\ No newline at end of file
diff --git a/ArcFormats/Strings/arcStrings.Designer.cs b/ArcFormats/Strings/arcStrings.Designer.cs
index b7d8bafc..63d703b9 100644
--- a/ArcFormats/Strings/arcStrings.Designer.cs
+++ b/ArcFormats/Strings/arcStrings.Designer.cs
@@ -124,6 +124,15 @@ namespace GameRes.Formats.Strings {
}
}
+ ///
+ /// Looks up a localized string similar to Reset.
+ ///
+ public static string ArcReset {
+ get {
+ return ResourceManager.GetString("ArcReset", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to âge proprietary image format.
///
@@ -315,6 +324,15 @@ namespace GameRes.Formats.Strings {
}
}
+ ///
+ /// Looks up a localized string similar to Compress contents.
+ ///
+ public static string NPACompressContents {
+ get {
+ return ResourceManager.GetString("NPACompressContents", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Nitro+ resource archive.
///
@@ -324,6 +342,25 @@ namespace GameRes.Formats.Strings {
}
}
+ ///
+ /// Looks up a localized string similar to Encryption keys
+ ///(required even if contents is not encrypted).
+ ///
+ public static string NPAKeys {
+ get {
+ return ResourceManager.GetString("NPAKeys", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Encryption scheme.
+ ///
+ public static string NPALabelScheme {
+ get {
+ return ResourceManager.GetString("NPALabelScheme", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Nitro+ Steins;Gate resource archive.
///
diff --git a/ArcFormats/Strings/arcStrings.resx b/ArcFormats/Strings/arcStrings.resx
index 22e4bf0f..e8fc1998 100644
--- a/ArcFormats/Strings/arcStrings.resx
+++ b/ArcFormats/Strings/arcStrings.resx
@@ -139,6 +139,9 @@ Choose appropriate encryption scheme.
no encryption
+
+ Reset
+
âge proprietary image format
@@ -204,9 +207,19 @@ predefined encryption scheme.
Writing index...
+
+ Compress contents
+
Nitro+ resource archive
+
+ Encryption keys
+(required even if contents is not encrypted)
+
+
+ Encryption scheme
+
Nitro+ Steins;Gate resource archive
diff --git a/ArcFormats/Strings/arcStrings.ru-RU.resx b/ArcFormats/Strings/arcStrings.ru-RU.resx
index e9dadca1..efe97aa4 100644
--- a/ArcFormats/Strings/arcStrings.ru-RU.resx
+++ b/ArcFormats/Strings/arcStrings.ru-RU.resx
@@ -136,6 +136,9 @@
без шифрования
+
+ Сбросить
+
Создание зашифрованных архивов не реализовано.
@@ -189,6 +192,16 @@
Записывается оглавление...
+
+ Сжать содержимое
+
+
+ Ключи шифрования
+(требуются даже если содержимое не шифруется)
+
+
+ Способ шифрования
+
Тип архива
diff --git a/ArcFormats/WidgetNPA.xaml b/ArcFormats/WidgetNPA.xaml
index 5c4d61f3..d95a4d8f 100644
--- a/ArcFormats/WidgetNPA.xaml
+++ b/ArcFormats/WidgetNPA.xaml
@@ -1,9 +1,10 @@
-
\ No newline at end of file
+ ItemsSource="{Binding Source={x:Static fmt:NpaOpener.KnownSchemes}, Mode=OneWay}"
+ SelectedValue="{Binding Source={x:Static p:Settings.Default}, Path=NPAScheme, Mode=TwoWay}"/>
+
diff --git a/ArcFormats/WidgetNPA.xaml.cs b/ArcFormats/WidgetNPA.xaml.cs
index c2288830..8923e076 100644
--- a/ArcFormats/WidgetNPA.xaml.cs
+++ b/ArcFormats/WidgetNPA.xaml.cs
@@ -2,6 +2,7 @@
using System.Windows.Controls;
using System.Linq;
using GameRes.Formats.Properties;
+using GameRes.Formats.NitroPlus;
namespace GameRes.Formats.GUI
{
@@ -12,15 +13,14 @@ namespace GameRes.Formats.GUI
{
public WidgetNPA ()
{
+ var selected = Settings.Default.NPAScheme;
InitializeComponent();
var sorted = NpaOpener.KnownSchemes.Skip (1).OrderBy (x => x);
Scheme.ItemsSource = NpaOpener.KnownSchemes.Take(1).Concat (sorted);
- Scheme.SelectedItem = NpaOpener.KnownSchemes[(int)Settings.Default.NPAScheme];
- }
-
- public string GetScheme()
- {
- return Scheme.SelectedItem as string;
+ if (NpaTitleId.NotEncrypted == NpaOpener.GetTitleId (selected))
+ Scheme.SelectedIndex = 0;
+ else
+ Scheme.SelectedValue = selected;
}
}
}
diff --git a/ArcFormats/app.config b/ArcFormats/app.config
index 760fad6a..cdb202d3 100644
--- a/ArcFormats/app.config
+++ b/ArcFormats/app.config
@@ -8,7 +8,7 @@
- NotEncrypted
+
@@ -49,6 +49,15 @@
1111638594
+
+ False
+
+
+ 1095188814
+
+
+ 555831124
+