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;
+ }
+}