diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index 6e5bd09e..e4a716ef 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -192,6 +192,7 @@
+
diff --git a/ArcFormats/Musica/ArcPAK.cs b/ArcFormats/Musica/ArcPAK.cs
new file mode 100644
index 00000000..d090af84
--- /dev/null
+++ b/ArcFormats/Musica/ArcPAK.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Text;
+
+namespace GameRes.Formats.Musica
+{
+ [Export(typeof(ArchiveFormat))]
+ public class PakOpener : ArchiveFormat
+ {
+ public override string Tag { get; } = "PAK";
+ public override string Description { get; } = "Musica engine legacy resource archive";
+ public override uint Signature { get; } = 0;
+ public override bool IsHierarchic { get; } = true;
+ public override bool CanWrite { get; } = false;
+
+ public PakOpener()
+ {
+ Extensions = new[] { "pak" };
+ ContainedFormats = new string[] { "PNG", "OGG" };
+ }
+
+ static readonly HashSet PakImageNames = new HashSet()
+ {
+ "bg", "st",
+ };
+
+ static readonly HashSet PakAudioNames = new HashSet()
+ {
+ "bgm", "se", "voice",
+ };
+
+ private string GetType(string pakName, string entryName)
+ {
+ if (PakImageNames.Contains(pakName))
+ {
+ return "image";
+ }
+
+ if (PakAudioNames.Contains(pakName))
+ {
+ return "audio";
+ }
+
+ return FormatCatalog.Instance.GetTypeFromName(entryName, ContainedFormats);
+ }
+
+ public override ArcFile TryOpen(ArcView view)
+ {
+ Stream input = view.CreateStream();
+ using(input = new NegStream(input))
+ {
+ using(ArcView.Reader reader = new ArcView.Reader(input))
+ {
+ int count = reader.ReadInt32();
+ if (count <= 0)
+ {
+ return null;
+ }
+
+ List entries = new List(count);
+ for(int i = 0; i < count; ++i)
+ {
+ uint indexLen = reader.ReadUInt32();
+ long indexStart = input.Position;
+
+ string name = input.ReadCString();
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return null;
+ }
+ uint size = reader.ReadUInt32();
+ uint offset = reader.ReadUInt32();
+
+ Entry entry = new Entry() { Name = name, Offset = offset, Size = size };
+ if (!entry.CheckPlacement(view.MaxOffset))
+ {
+ return null;
+ }
+ entry.Type = this.GetType(Path.GetFileNameWithoutExtension(view.Name), entry.Name);
+ entries.Add(entry);
+
+ input.Position = indexStart + indexLen;
+ }
+ return new PakArchive(view, this, entries, input.Position);
+ }
+ }
+ }
+
+ public override Stream OpenEntry(ArcFile arc, Entry entry)
+ {
+ if (!(arc is PakArchive pakArc))
+ {
+ return base.OpenEntry(arc, entry);
+ }
+
+ return new NegStream(base.OpenEntry(arc, pakArc.GetEntry(entry)));
+ }
+ }
+
+ internal class PakArchive : ArcFile
+ {
+ private long m_IndexSize;
+ public PakArchive(ArcView arc, ArchiveFormat impl, ICollection dir, long indexSize) : base(arc, impl, dir)
+ {
+ m_IndexSize = indexSize;
+ }
+
+ public Entry GetEntry(Entry e)
+ {
+ return new Entry
+ {
+ Name = e.Name,
+ Offset = e.Offset + m_IndexSize,
+ Size = e.Size,
+ Type = e.Type,
+ };
+ }
+ }
+
+ //neg reg8
+ public class NegStream : ProxyStream
+ {
+ public NegStream(Stream stream, bool leave_open = false) : base(stream, leave_open)
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ int read = BaseStream.Read(buffer, offset, count);
+ for (int i = 0; i < read; ++i)
+ {
+ buffer[offset + i] = (byte)-buffer[offset + i];
+ }
+ return read;
+ }
+
+ public override int ReadByte()
+ {
+ int b = BaseStream.ReadByte();
+ if (-1 != b)
+ {
+ b = (byte)-b;
+ }
+ return b;
+ }
+
+ byte[] write_buf;
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (null == write_buf)
+ write_buf = new byte[81920];
+ while (count > 0)
+ {
+ int chunk = Math.Min(write_buf.Length, count);
+ for (int i = 0; i < chunk; ++i)
+ {
+ write_buf[i] = (byte)-buffer[offset + i];
+ }
+ BaseStream.Write(write_buf, 0, chunk);
+ offset += chunk;
+ count -= chunk;
+ }
+ }
+
+ public override void WriteByte(byte value)
+ {
+ BaseStream.WriteByte((byte)-value);
+ }
+ }
+}