Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e7f411e
[Leaf] ArcPAK.cs: Move Leaf LZSS compression logic to a separate clas…
andreiagmu Jul 26, 2026
7d0008e
[Leaf] ArcLAC.cs: LAC repack implementation
andreiagmu Jul 26, 2026
23d5f3a
[Leaf] ArcLAC.cs: Fix "CanWrite" properties
andreiagmu Jul 26, 2026
798a3f2
[Leaf] Add information disclaimer regarding this specific LAC repack …
andreiagmu Aug 16, 2026
68e8854
[Leaf] ArcLAC.cs: Order entries using the same order as the original …
andreiagmu Aug 16, 2026
da8b7cd
[Leaf] ArcPAK.cs: Move Leaf LZSS compression logic to a separate clas…
andreiagmu Jul 26, 2026
f8f69e9
[Leaf] ArcLAC.cs: LAC repack implementation
andreiagmu Jul 26, 2026
4309b88
[Leaf] ArcLAC.cs: Fix "CanWrite" properties
andreiagmu Jul 26, 2026
153456b
[Leaf] Add information disclaimer regarding this specific LAC repack …
andreiagmu Aug 16, 2026
d802367
[Leaf] ArcLAC.cs: Order entries using the same order as the original …
andreiagmu Aug 16, 2026
33426a3
Merge remote-tracking branch 'origin/add-PAK-LAC-repacker' into add-P…
andreiagmu Aug 17, 2026
e639c54
[Leaf] ArcLAC.cs: Adjust compression logic to skip small uncompressed…
andreiagmu Aug 17, 2026
c546e79
[Leaf] ArcLAC.cs: Clarify condition for skipping small uncompressed data
andreiagmu Aug 17, 2026
0d8b1f1
[Leaf] ArcLAC.cs: Use variable naming convention consistent with ArcP…
andreiagmu Aug 17, 2026
3942d19
[Leaf] LeafCompression.cs: Add `frameFill` parameter to LZSS compression
andreiagmu Aug 17, 2026
529b94d
[Leaf] ArcLAC.cs: Simplify entry ordering logic; Remove custom LacEnt…
andreiagmu Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ArcFormats/ArcFormats.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@
<Compile Include="ArcARCX.cs" />
<Compile Include="ArcASAR.cs" />
<Compile Include="Artemis\ArcMJA.cs" />
<Compile Include="Leaf\LeafCompression.cs" />
<Compile Include="Leaf\LeafVideo.cs" />
<Compile Include="LightVN\ArcMCDAT.cs" />
<Compile Include="Macintosh\ImagePICT.cs" />
Expand Down
102 changes: 101 additions & 1 deletion ArcFormats/Leaf/ArcLAC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
// IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Text;
using GameRes.Compression;

namespace GameRes.Formats.Leaf
Expand Down Expand Up @@ -86,7 +89,7 @@ public class PakOpener : ArchiveFormat
public override string Description { get { return "Leaf resource archive"; } }
public override uint Signature { get { return 0x43414C; } } // 'LAC'
public override bool IsHierarchic { get { return false; } }
public override bool CanWrite { get { return false; } }
public override bool CanWrite { get { return true; } }

public PakOpener ()
{
Expand Down Expand Up @@ -148,5 +151,102 @@ public override Stream OpenEntry (ArcFile arc, Entry entry)
Stream input = arc.File.CreateStream (entry.Offset, entry.Size);
return new LzssStream (input);
}

// --- REPACK IMPLEMENTATION ---
// Note: This implementation was made for the Leaf game "Kimi ga Yobu, Megiddo no Oka de".
// Other games may or may not be supported.

public override void Create (Stream output, IEnumerable<Entry> entries, ResourceOptions options, EntryCallback callback)
{
// Order entries using the same order as the original PAK/LAC archive.
entries = entries.OrderBy (x => x.Name.ToLowerAscii(), StringComparer.Ordinal).ToList();

int count = entries.Count();
using (var writer = new BinaryWriter (output, Encoding.ASCII, true))
{
// Write Header (LAC)
writer.Write (Signature);
writer.Write (count);

long tableOffset = 8;
long dataOffset = tableOffset + (count * 0x28);

writer.BaseStream.Position = dataOffset;

var entryInfos = new List<EntryInfo>();
int i = 0;

foreach (var entry in entries)
{
if (callback != null)
callback (i + 1, entry, null);

long currentOffset = writer.BaseStream.Position;

using (var input = File.OpenRead(entry.Name))
{
byte[] rawData = new byte[input.Length];
input.Read (rawData, 0, (int)input.Length);

// Compress
byte[] compressedData = LeafLzss.Compress (rawData, 0);

uint totalSize;
bool isPacked;

// Write Data
if (rawData.Length > 32 // Skip small uncompressed data that should not be compressed.
&& compressedData.Length < rawData.Length)
{
isPacked = true;
totalSize = (uint)(compressedData.Length + 4);
writer.Write ((uint)rawData.Length);
writer.Write (compressedData);
}
else
{
isPacked = false;
totalSize = (uint)rawData.Length;
writer.Write (rawData);
}

entryInfos.Add (new EntryInfo
{
Name = Path.GetFileName (entry.Name),
Offset = (uint)currentOffset,
Size = totalSize,
IsPacked = isPacked
});
}
i++;
}

// Write File Table
writer.BaseStream.Position = tableOffset;
foreach (var info in entryInfos)
{
byte[] nameBuf = new byte[0x20];

byte[] nameBytes = Encodings.cp932.GetBytes (info.Name);
int len = Math.Min (nameBytes.Length, 0x1F);
for (int j = 0; j < len; ++j)
nameBuf[j] = (byte)(nameBytes[j] ^ 0xFF);
if (info.IsPacked)
nameBuf[0x1F] = 1;

writer.Write (nameBuf);
writer.Write (info.Size);
writer.Write (info.Offset);
}
}
}

struct EntryInfo
{
public string Name;
public uint Offset;
public uint Size;
public bool IsPacked;
}
}
}
166 changes: 1 addition & 165 deletions ArcFormats/Leaf/ArcPAK.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public override void Create (Stream output, IEnumerable<Entry> entries, Resource
input.Read (rawData, 0, (int)input.Length);

// Compress
byte[] compressedData = LeafLzss.Compress (rawData);
byte[] compressedData = LeafLzss.Compress (rawData, 0x20);

// Write Data (Prefix + LZSS)
writer.Write ((uint)compressedData.Length);
Expand Down Expand Up @@ -313,168 +313,4 @@ internal struct EntryDefV2 : IEntryDefinition
public bool IsPacked { get { return _is_packed != 0; } }
}
#pragma warning restore 649,169

// --- LEAF LZSS COMPRESSION ---
internal static class LeafLzss
{
const int N = 4096;
const int F = 18;
const int THR = 2;
const int NIL = N;

public static byte[] Compress (byte[] input)
{
if (input.Length == 0) return new byte[0];

using (var outStream = new MemoryStream (input.Length))
{
// Arrays size = N + 257 to handle Root Nodes (N+1..N+256) safely
int[] lson = new int[N + 257];
int[] rson = new int[N + 257];
int[] dad = new int[N + 257];
byte[] text_buf = new byte[N + F - 1];

for (int j = N + 1; j <= N + 256; j++) rson[j] = NIL;
for (int j = 0; j < N; j++) dad[j] = NIL;

int match_position = 0, match_length = 0;

void InsertNode (int r_node)
{
int i, p, cmp;
int key_pos = r_node;

// Root for this character
p = N + 1 + text_buf[key_pos];

rson[r_node] = lson[r_node] = NIL;
match_length = 0;

cmp = 1; // Initial state

for (; ; )
{
if (cmp >= 0)
{
if (rson[p] != NIL) p = rson[p];
else { rson[p] = r_node; dad[r_node] = p; return; }
}
else
{
if (lson[p] != NIL) p = lson[p];
else { lson[p] = r_node; dad[r_node] = p; return; }
}

// Compare bytes only after finding a valid child node 'p' (buffer index)
for (i = 1; i < F; i++)
if ((cmp = text_buf[key_pos + i] - text_buf[p + i]) != 0) break;

if (i > match_length)
{
match_position = p;
match_length = i;
if (match_length >= F) break;
}
}

// Replace node logic
dad[r_node] = dad[p]; lson[r_node] = lson[p]; rson[r_node] = rson[p];
dad[lson[p]] = r_node; dad[rson[p]] = r_node;
if (rson[dad[p]] == p) rson[dad[p]] = r_node;
else lson[dad[p]] = r_node;
dad[p] = NIL;
}

void DeleteNode (int p)
{
int q;
if (dad[p] == NIL) return;
if (rson[p] == NIL) q = lson[p];
else if (lson[p] == NIL) q = rson[p];
else
{
q = lson[p];
if (rson[q] != NIL)
{
do { q = rson[q]; } while (rson[q] != NIL);
rson[dad[q]] = lson[q]; dad[lson[q]] = dad[q];
lson[q] = lson[p]; dad[lson[p]] = q;
}
rson[q] = rson[p]; dad[rson[p]] = q;
}
dad[q] = dad[p];
if (rson[dad[p]] == p) rson[dad[p]] = q;
else lson[dad[p]] = q;
dad[p] = NIL;
}

int code_buf_ptr = 1;
byte[] code_buf = new byte[17];
byte mask = 1;
int s = 0, r = N - F;
int len = 0;

for (int j = 0; j < r; j++) text_buf[j] = 0x20;

int bytes_read = 0;
for (len = 0; len < F && bytes_read < input.Length; len++)
text_buf[r + len] = input[bytes_read++];

if (len == 0) return new byte[0];

for (int j = 1; j <= F; j++) InsertNode (r - j);
InsertNode (r);

do
{
if (match_length > len) match_length = len;
if (match_length <= THR)
{
match_length = 1;
code_buf[0] |= mask;
code_buf[code_buf_ptr++] = text_buf[r];
}
else
{
code_buf[code_buf_ptr++] = (byte)(match_position & 0xFF);
code_buf[code_buf_ptr++] = (byte)(((match_position >> 4) & 0xF0) | (match_length - (THR + 1)));
}

if ((mask <<= 1) == 0)
{
outStream.Write (code_buf, 0, code_buf_ptr);
code_buf[0] = 0; code_buf_ptr = 1; mask = 1;
}

int last_match_length = match_length;

int i;
for (i = 0; i < last_match_length && bytes_read < input.Length; i++)
{
DeleteNode (s);
byte c = input[bytes_read++];
text_buf[s] = c;
if (s < F - 1) text_buf[s + N] = c;
s = (s + 1) & (N - 1);
r = (r + 1) & (N - 1);
InsertNode (r);
}

while (bytes_read == input.Length && i++ < last_match_length)
{
DeleteNode (s);
s = (s + 1) & (N - 1);
r = (r + 1) & (N - 1);
if (--len != 0) InsertNode (r);
}

} while (len > 0);

if (code_buf_ptr > 1)
outStream.Write (code_buf, 0, code_buf_ptr);

return outStream.ToArray();
}
}
}
}
Loading
Loading