Add Musica PAZ Writer (Not works)

This commit is contained in:
2025-11-01 22:48:42 +08:00
parent ecc3b4473f
commit 44c2f7600f
7 changed files with 560 additions and 31 deletions

View File

@@ -537,6 +537,9 @@
<Compile Include="Morning\ArcTTD.cs" />
<Compile Include="Musica\ArcPAZ.cs" />
<Compile Include="Musica\ArcSQZ.cs" />
<Compile Include="Musica\CreatePAZWidget.xaml.cs">
<DependentUpon>CreatePAZWidget.xaml</DependentUpon>
</Compile>
<Compile Include="Musica\WidgetPAZ.xaml.cs">
<DependentUpon>WidgetPAZ.xaml</DependentUpon>
</Compile>
@@ -1138,6 +1141,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Musica\CreatePAZWidget.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Musica\WidgetPAZ.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>

View File

@@ -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<Entry> list, ResourceOptions options,
EntryCallback callback)
{
if (null == output)
throw new ArgumentNullException ("output");
var paz_options = GetOptions<PazOptions> (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<PazCreateEntry>();
var used_names = new HashSet<string> (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<PazCreateEntry> entries)
{
long size = 4;
foreach (var entry in entries)
size += entry.NameBytes.Length + 1 + 8 + 4 + 4 + 4 + 4;
return size;
}
byte[] BuildIndexBuffer (IList<PazCreateEntry> 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;
}
}

View File

@@ -0,0 +1,26 @@
<Grid x:Class="GameRes.Formats.GUI.CreatePAZWidget"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:p="clr-namespace:GameRes.Formats.Properties">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,5">
<StackPanel Orientation="Vertical" Margin="0,0,10,0">
<Label Content="Title" Target="{Binding ElementName=Title}" Padding="4,0,0,5"/>
<ComboBox Name="Title" Width="200"
SelectedValue="{Binding Source={x:Static p:Settings.Default}, Path=PAZTitle, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Orientation="Vertical">
<Label Content="Archive key" Target="{Binding ElementName=Archive}" Padding="4,0,0,5"/>
<ComboBox Name="Archive" Width="180"
SelectedValue="{Binding Source={x:Static p:Settings.Default}, Path=PAZArchiveKey, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
<CheckBox Grid.Row="1" Name="Compress" Content="Compress contents" Margin="0,0,0,5"
IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=PAZCompressContents, Mode=TwoWay}"/>
<CheckBox Grid.Row="2" Name="Retain" Content="Retain folder structure"
IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=PAZRetainStructure, Mode=TwoWay}"/>
</Grid>

View File

@@ -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
{
/// <summary>
/// Interaction logic for CreatePAZWidget.xaml
/// </summary>
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<string> { 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<string> arc_keys = (scheme != null && scheme.ArcKeys != null)
? scheme.ArcKeys.Keys.OrderBy (x => x).ToList ()
: new List<string> ();
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; }
}
}
}

View File

@@ -646,6 +646,42 @@ namespace GameRes.Formats.Properties {
}
}
[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()]
[global::System.Configuration.DefaultSettingValueAttribute("")]

View File

@@ -158,6 +158,15 @@
<Setting Name="PAZTitle" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="PAZArchiveKey" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="PAZCompressContents" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="PAZRetainStructure" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="EAGLSEncryption" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>

View File

@@ -160,6 +160,15 @@
<setting name="PAZTitle" serializeAs="String">
<value />
</setting>
<setting name="PAZArchiveKey" serializeAs="String">
<value />
</setting>
<setting name="PAZCompressContents" serializeAs="String">
<value>False</value>
</setting>
<setting name="PAZRetainStructure" serializeAs="String">
<value>True</value>
</setting>
<setting name="EAGLSEncryption" serializeAs="String">
<value />
</setting>