From b4d3ca64f3c148ea0dcaccbc419010e2f0b64ed6 Mon Sep 17 00:00:00 2001
From: YeLike <93629620+YeLikesss@users.noreply.github.com>
Date: Wed, 22 Apr 2026 18:50:51 +0800
Subject: [PATCH] =?UTF-8?q?[NekoNovel]=20=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ArcFormats/ArcFormats.csproj | 1 +
ArcFormats/NekoNovel/ArcNKPACK.cs | 94 +++++++++++++++++++++++++++++++
2 files changed, 95 insertions(+)
create mode 100644 ArcFormats/NekoNovel/ArcNKPACK.cs
diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index 2aa2aa8c..8b715459 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -265,6 +265,7 @@
+
diff --git a/ArcFormats/NekoNovel/ArcNKPACK.cs b/ArcFormats/NekoNovel/ArcNKPACK.cs
new file mode 100644
index 00000000..0519d21d
--- /dev/null
+++ b/ArcFormats/NekoNovel/ArcNKPACK.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Text;
+using GameRes.Compression;
+using System.ComponentModel.Composition;
+using CommunityToolkit.HighPerformance;
+
+namespace GameRes.Formats.NekoNovel
+{
+ [Export(typeof(ArchiveFormat))]
+ public class NkpackOpener : ArchiveFormat
+ {
+ public override string Tag => "NKPACK/NekoNovel";
+ public override string Description => "NekoNovel resource archive";
+ public override uint Signature => 0u;
+ public override bool IsHierarchic => true;
+ public override bool CanWrite => false;
+
+ public NkpackOpener()
+ {
+ Extensions = new string[] { "nkpack" };
+ }
+
+ private static readonly byte[] s_mHeader = new byte[]
+ {
+ 0x0F, 0x00, 0x00, 0x00, 0x4E, 0x4B, 0x4E, 0x4F, 0x45, 0x56, 0x4C, 0x20, 0x50, 0x41, 0x43, 0x4B,
+ 0x41, 0x47, 0x45
+ };
+
+ public override ArcFile TryOpen(ArcView file)
+ {
+ if (!file.View.BytesEqual(0L, s_mHeader))
+ {
+ return null;
+ }
+ using (ArcViewStream stream = file.CreateStream())
+ {
+ stream.Seek(-4L, SeekOrigin.End);
+ uint entryOffset = ~stream.ReadUInt32();
+
+ stream.Seek(entryOffset, SeekOrigin.Begin);
+ string signature = ReadString(stream);
+
+ int count = stream.ReadInt32();
+ List entries = new List(count);
+ for (int i = 0; i < count; ++i)
+ {
+ string name = ReadString(stream);
+
+ PackedEntry entry = Create(name);
+ entry.Offset = ~stream.ReadUInt32();
+ entry.UnpackedSize = stream.ReadUInt32();
+ entry.Size = ~stream.ReadUInt32();
+ entry.IsPacked = true;
+
+ if (!entry.CheckPlacement(file.MaxOffset))
+ {
+ return null;
+ }
+
+ entries.Add(entry);
+ }
+
+ return new ArcFile(file, this, entries);
+ }
+ }
+
+ public override Stream OpenEntry(ArcFile arc, Entry entry)
+ {
+ if (!(entry is PackedEntry e))
+ {
+ return arc.File.CreateStream(entry.Offset, entry.Size);
+ }
+ return new ZLibStream(arc.File.CreateStream(e.Offset, e.Size), CompressionMode.Decompress);
+ }
+
+ private unsafe static string ReadString(Stream stream)
+ {
+ uint length = 0u;
+ stream.Read(new Span(&length, 4));
+
+ if (length == 0u)
+ {
+ return string.Empty;
+ }
+
+ byte[] data = new byte[length];
+ stream.Read(data, 0, data.Length);
+
+ return Encoding.UTF8.GetString(data);
+ }
+ }
+}