From 446b9dff3e8c62d7a44d129b31578bfe311c6554 Mon Sep 17 00:00:00 2001 From: YeLike <93629620+YeLikesss@users.noreply.github.com> Date: Fri, 22 May 2026 00:27:09 +0800 Subject: [PATCH] =?UTF-8?q?[Entergram]=20=E5=B0=81=E5=8C=85=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ArcFormats/ArcFormats.csproj | 2 + ArcFormats/ArcFormats.csproj.user | 6 ++ ArcFormats/Entergram/ArcPacV1.cs | 153 +++++++++++++++++++++++++++ ArcFormats/Entergram/QuickLZ.cs | 170 ++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 ArcFormats/ArcFormats.csproj.user create mode 100644 ArcFormats/Entergram/ArcPacV1.cs create mode 100644 ArcFormats/Entergram/QuickLZ.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 1c3664b1..d814fa64 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -178,6 +178,8 @@ + + diff --git a/ArcFormats/ArcFormats.csproj.user b/ArcFormats/ArcFormats.csproj.user new file mode 100644 index 00000000..c10e84ba --- /dev/null +++ b/ArcFormats/ArcFormats.csproj.user @@ -0,0 +1,6 @@ + + + + ProjectFiles + + \ No newline at end of file diff --git a/ArcFormats/Entergram/ArcPacV1.cs b/ArcFormats/Entergram/ArcPacV1.cs new file mode 100644 index 00000000..b828d70d --- /dev/null +++ b/ArcFormats/Entergram/ArcPacV1.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Text; + +namespace GameRes.Formats.Entergram +{ + [Export(typeof(ArchiveFormat))] + public class PacOpenerV1 : ArchiveFormat + { + public override string Tag => "PacV1/Entergram"; + public override string Description => "Entergram Unity resource archive"; + public override uint Signature => 0x20434150; // PAC/x20 + public override bool IsHierarchic => true; + public override bool CanWrite => false; + + private static readonly byte[] smHeader = new byte[] + { + 0x50, 0x41, 0x43, 0x20, 0x56, 0x45, 0x52, 0x2D, 0x31, 0x2E, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00 + }; + + public override ArcFile TryOpen(ArcView file) + { + if (!file.View.BytesEqual(0L, smHeader)) + { + return null; + } + + List entries = this.ParseEntry(file); + if(entries == null) + { + return null; + } + + return new ArcFile(file, this, entries); + } + + public override Stream OpenEntry(ArcFile arc, Entry entry) + { + if(!(entry is PackedEntry e)) + { + return base.OpenEntry(arc, entry); + } + + if (e.IsPacked) + { + using(ArcViewStream s = arc.File.CreateStream(e.Offset, e.Size)) + { + byte[] data = s.ReadBytes(int.MaxValue); + data = QLZCompressor.Decompress(data); + return new MemoryStream(data, false); + } + } + else + { + return base.OpenEntry(arc, entry); + } + } + + private List ParseEntry(ArcView file) + { + bool compressed = Path.GetFileNameWithoutExtension(file.Name).EndsWith("_c"); + using(ArcViewStream stream = file.CreateStream(smHeader.LongLength)) + { + List entries = new List(); + + // 8 Bytes Entry Mode (Default) + { + stream.Position = 0L; + while (stream.Position < stream.Length) + { + string name = ReadString(stream); + + long offset = stream.ReadInt64() + 0x10L; + long length = stream.ReadInt64(); + if (!CheckEntry(offset, length, file.MaxOffset)) + { + entries.Clear(); + break; + } + + PackedEntry entry = Create(name); + entry.Offset = offset; + entry.Size = (uint)length; + entry.IsPacked = compressed; + entries.Add(entry); + + stream.Seek(length, SeekOrigin.Current); + } + } + + // 10 Bytes Entry Mode + if (!entries.Any()) + { + stream.Position = 0L; + while (stream.Position < stream.Length) + { + string name = ReadString(stream); + + long offset = stream.ReadInt64() + 0x14L; + stream.Position += 2L; + long length = stream.ReadInt64(); + stream.Position += 2L; + if (!CheckEntry(offset, length, file.MaxOffset)) + { + entries.Clear(); + break; + } + + PackedEntry entry = Create(name); + entry.Offset = offset; + entry.Size = (uint)length; + entry.IsPacked = compressed; + entries.Add(entry); + + stream.Seek(length, SeekOrigin.Current); + } + } + + return entries.Cast().ToList(); + } + } + + private static string ReadString(ArcViewStream stream) + { + byte[] buf = new byte[0x20]; + if (stream.Read(buf, 0, buf.Length) != buf.Length) + { + return string.Empty; + } + + int len = Array.IndexOf(buf, 0); + if (len <= 0) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(buf, 0, len); + } + + private static bool CheckEntry(long offset, long length, long max) + { + return offset >= 0L && + length >= 0L && + offset < max && + length <= max && + length <= uint.MaxValue && + offset <= max - length; + } + } +} diff --git a/ArcFormats/Entergram/QuickLZ.cs b/ArcFormats/Entergram/QuickLZ.cs new file mode 100644 index 00000000..525a8d62 --- /dev/null +++ b/ArcFormats/Entergram/QuickLZ.cs @@ -0,0 +1,170 @@ +using System; + +namespace GameRes.Formats.Entergram +{ + internal static class QLZCompressor + { + public struct QLZHeader + { + public QLZHeader(byte[] src) + { + byte b = src[0]; + this.Compressible = (b & CONTAINER_Compressible) == CONTAINER_Compressible; + if (this.Compressible) + { + b -= CONTAINER_Compressible; + } + if (b != STATIC_HEADER_FIRSTBYTE) + { + throw new Exception("Invalid QLZ Header: " + b.ToString()); + } + this.CompressedSize = BitConverter.ToInt32(src, 1); + this.RawSize = BitConverter.ToInt32(src, 5); + } + + public const int HEADER_LENGTH = 9; + public const byte STATIC_HEADER_FIRSTBYTE = 0x5E; + public const byte CONTAINER_Compressible = 1; + public const int Level = 3; + + public bool Compressible; + public int CompressedSize; + public int RawSize; + } + + public static byte[] Decompress(byte[] compressed) + { + QLZCompressor.QLZHeader qlzheader = new QLZCompressor.QLZHeader(compressed); + if (qlzheader.RawSize == 0) + { + return new byte[0]; + } + byte[] array = new byte[qlzheader.RawSize]; + if (!qlzheader.Compressible) + { + Array.Copy(compressed, QLZHeader.HEADER_LENGTH, array, 0, qlzheader.RawSize); + } + else + { + QLZCompressor.Decompress_Unsafe(compressed, array, qlzheader.RawSize); + } + return array; + } + + private unsafe static void Decompress_Unsafe(byte[] compressed, byte[] decompressed, int rawSize) + { + if (rawSize < decompressed.Length) + { + throw new Exception("Decompressed Array is not enough size"); + } + + fixed (byte* ptr = compressed) + { + fixed (byte* ptr2 = decompressed) + { + int src = QLZHeader.HEADER_LENGTH; + int dst = 0; + uint cword_val = 1U; + int last_matchstart = rawSize - UNCONDITIONAL_MATCHLEN - UNCOMPRESSED_END - 1; + uint fetch = 0U; + for (; ; ) + { + if (cword_val == 1U) + { + cword_val = ReadUInt32(ptr + src); + src += 4; + if (dst <= last_matchstart) + { + fetch = ReadUInt32(ptr + src); + } + } + if ((cword_val & 1U) == 1U) + { + cword_val >>= 1; + uint offset; + uint matchlen; + if ((fetch & 3U) == 0U) + { + offset = (fetch & 0xFFU) >> 2; + matchlen = 3U; + src++; + } + else if ((fetch & 2U) == 0U) + { + offset = (fetch & 0xFFFFU) >> 2; + matchlen = 3U; + src += 2; + } + else if ((fetch & 1U) == 0U) + { + offset = (fetch & 0xFFFFU) >> 6; + matchlen = ((fetch >> 2) & 0xFU) + 3U; + src += 2; + } + else if ((fetch & 0x7FU) != 3U) + { + offset = (fetch >> 7) & 0x1FFFFU; + matchlen = ((fetch >> 2) & 0x1FU) + 2U; + src += 3; + } + else + { + offset = fetch >> 0xF; + matchlen = ((fetch >> 7) & 0xFFU) + 3U; + src += 4; + } + uint num7 = (uint)((long)dst - (long)((ulong)offset)); + ptr2[dst] = ptr2[num7]; + (ptr2 + dst)[1] = (ptr2 + num7)[1]; + (ptr2 + dst)[2] = (ptr2 + num7)[2]; + int num8 = 3; + while ((long)num8 < (long)((ulong)matchlen)) + { + (ptr2 + dst)[num8] = (ptr2 + num7)[num8]; + num8++; + } + dst += (int)matchlen; + fetch = ReadUInt32(ptr + src); + } + else + { + if (dst > last_matchstart) + { + break; + } + ptr2[dst] = ptr[src]; + dst++; + src++; + cword_val >>= 1; + fetch = (uint)((((int)fetch >> 8) & 0xFFFF) | ((int)(ptr + src)[2] << 0x10) | ((int)(ptr + src)[3] << 0x18)); + } + } + while (dst <= rawSize - 1) + { + if (cword_val == 1U) + { + src += 4; + cword_val = 0x80000000U; + } + ptr2[dst] = ptr[src]; + dst++; + src++; + cword_val >>= 1; + } + } + } + } + + private unsafe static uint ReadUInt32(byte* p) + { + return *(uint*)p; + } + + private const int HASH_VALUES = 0x1000; + private const int MINOFFSET = 2; + private const int UNCONDITIONAL_MATCHLEN = 6; + private const int UNCOMPRESSED_END = 4; + private const int CWORD_LEN = 4; + private const int QLZ_POINTERS_3 = 0x10; + } +}