├── App.config
├── CollectionExtensions.cs
├── Bcd.cs
├── LICENSE
├── CSystemArc.sln
├── Properties
└── AssemblyInfo.cs
├── BcdCompression.cs
├── README.md
├── ArchiveEntry.cs
├── CSystemArc.csproj
├── ArchiveReader.cs
├── ArchiveWriter.cs
├── CSystemConfig.cs
├── Program.cs
├── CSystemImage.cs
└── LzssStream.cs
/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace CSystemArc
5 | {
6 | internal static class CollectionExtensions
7 | {
8 | public static TValue FetchValue(this IDictionary dict, TKey key, Func getValue)
9 | {
10 | if (!dict.TryGetValue(key, out TValue value))
11 | {
12 | value = getValue();
13 | dict.Add(key, value);
14 | }
15 | return value;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Bcd.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace CSystemArc
5 | {
6 | internal static class Bcd
7 | {
8 | public static int Read(Stream stream)
9 | {
10 | int value = 0;
11 | for (int i = 0; i < 8; i++)
12 | {
13 | value *= 10;
14 | byte b = (byte)stream.ReadByte();
15 | if (b != 0xFF)
16 | value += b ^ 0x7F;
17 | }
18 | return value;
19 | }
20 |
21 | public static void Write(Stream stream, int value)
22 | {
23 | byte[] bcd = new byte[8];
24 | for (int i = 7; i >= 0; i--)
25 | {
26 | value = Math.DivRem(value, 10, out int remainder);
27 | bcd[i] = remainder != 0 ? (byte)(remainder ^ 0x7F) : (byte)0xFF;
28 | }
29 | stream.Write(bcd, 0, 8);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 arcusmaximus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CSystemArc.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31410.357
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSystemArc", "CSystemArc.csproj", "{FBE058D8-AC2D-4A5E-A140-7BB0173755B8}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {FBE058D8-AC2D-4A5E-A140-7BB0173755B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {FBE058D8-AC2D-4A5E-A140-7BB0173755B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {FBE058D8-AC2D-4A5E-A140-7BB0173755B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {FBE058D8-AC2D-4A5E-A140-7BB0173755B8}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {0793DFAD-3EC4-4E14-9FC9-E140B3D55DB0}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // アセンブリに関する一般情報は以下の属性セットをとおして制御されます。
6 | // 制御されます。アセンブリに関連付けられている情報を変更するには、
7 | // これらの属性値を変更します。
8 | [assembly: AssemblyTitle("CSystemArc")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("CSystemArc")]
13 | [assembly: AssemblyCopyright("Copyright © 2021")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // ComVisible を false に設定すると、このアセンブリ内の型は COM コンポーネントから
18 | // 参照できなくなります。COM からこのアセンブリ内の型にアクセスする必要がある場合は、
19 | // その型の ComVisible 属性を true に設定します。
20 | [assembly: ComVisible(false)]
21 |
22 | // このプロジェクトが COM に公開される場合、次の GUID が typelib の ID になります
23 | [assembly: Guid("fbe058d8-ac2d-4a5e-a140-7bb0173755b8")]
24 |
25 | // アセンブリのバージョン情報は次の 4 つの値で構成されています:
26 | //
27 | // メジャー バージョン
28 | // マイナー バージョン
29 | // ビルド番号
30 | // リビジョン
31 | //
32 | // すべての値を指定するか、次を使用してビルド番号とリビジョン番号を既定に設定できます
33 | // 既定値にすることができます:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.1.0.0")]
36 | [assembly: AssemblyFileVersion("1.1.0.0")]
37 |
--------------------------------------------------------------------------------
/BcdCompression.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Compression;
4 |
5 | namespace CSystemArc
6 | {
7 | internal static class BcdCompression
8 | {
9 | public static byte[] Decompress(Stream stream)
10 | {
11 | int uncompressedSize = Bcd.Read(stream);
12 | int compressedSize = Bcd.Read(stream);
13 |
14 | long offset = stream.Position;
15 |
16 | byte[] decompressed = new byte[uncompressedSize];
17 | using LzssStream lzss = new LzssStream(stream, CompressionMode.Decompress, true);
18 | lzss.Read(decompressed, 0, uncompressedSize);
19 |
20 | stream.Position = offset + compressedSize;
21 | return decompressed;
22 | }
23 |
24 | public static void Compress(byte[] uncompressedData, Stream stream)
25 | {
26 | Compress(new ArraySegment(uncompressedData), stream);
27 | }
28 |
29 | public static void Compress(ArraySegment uncompressedData, Stream stream)
30 | {
31 | using MemoryStream compressedStream = new MemoryStream();
32 | using LzssStream lzss = new LzssStream(compressedStream, CompressionMode.Compress);
33 | lzss.Write(uncompressedData.Array, uncompressedData.Offset, uncompressedData.Count);
34 |
35 | Bcd.Write(stream, uncompressedData.Count);
36 | Bcd.Write(stream, (int)compressedStream.Length);
37 | compressedStream.TryGetBuffer(out ArraySegment compressedData);
38 | stream.Write(compressedData.Array, compressedData.Offset, compressedData.Count);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSystemTools
2 | Script and image unpacking and repacking tool for the Cyberworks "C,system" visual novel engine.
3 |
4 | Note: the tool has had only limited testing and doesn't work for all games.
5 |
6 | ## Overview
7 |
8 | Games based on this engine can have the following files:
9 |
10 | * Arc00.dat: general configuration (e.g. window title, font size and so on)
11 | * Arc01.dat: scenario file index
12 | * Arc04.dat: scenario file content
13 | * Arc02.dat: image file index
14 | * Arc05.dat, Arc05a.dat, Arc05b.dat: image file content
15 | * Arc03.dat: audio file index
16 | * Arc06.dat: audio file content
17 | * Arc07.dat: unknown index
18 | * Arc08.dat: unknown content
19 | * Arc09.dat: unknown index
20 |
21 | .a0 scenario files extracted from Arc01.dat/Arc04.dat can be translated using [VNTranslationTools](https://github.com/arcusmaximus/VNTranslationTools).
22 |
23 | Images will be automatically converted to and from PNG.
24 |
25 | Audio files are currently not supported (the obfuscated OGGs will be extracted as-is).
26 |
27 | ## Command line
28 |
29 | ```
30 | CSystemArc unpack index.dat content1.dat content2.dat ... folder
31 | ```
32 | Extract the specified archives to a folder. Example: `unpack Arc01.dat Arc04.dat scenarios`
33 |
34 | This command will also print the version number of the archive format at the very beginning (e.g. "Archive version: 23"). This is needed for the "pack" command later on.
35 |
36 | ```
37 | CSystemArc pack version folder index.dat content1.dat content2.dat ...
38 | ```
39 | Pack a folder into one or more archive files. The archive files will be completely overwritten (files that are in the original archives but not in the folder will be lost). Example: `pack 23 scenarios Arc01.dat Arc04.dat`
40 |
41 | ```
42 | CSystemArc readconfig Arc00.dat config.xml
43 | ```
44 | Convert the binary configuration file to a more or less human-readable (and editable) XML file.
45 | Some known settings:
46 | * AT/AL/AB/AR: message window top/left/bottom/right
47 | * M: maximum number of lines
48 | * F: font size
49 | * X: character spacing
50 | * Y: line spacing
51 |
52 | ```
53 | CSystemArc writeconfig config.xml Arc00.dat
54 | ```
55 | Convert the above XML file back into a binary file that works with the game.
56 |
--------------------------------------------------------------------------------
/ArchiveEntry.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace CSystemArc
4 | {
5 | internal class ArchiveEntry
6 | {
7 | public int Version
8 | {
9 | get;
10 | set;
11 | }
12 |
13 | public int Index
14 | {
15 | get;
16 | set;
17 | }
18 |
19 | public int ContentArchiveIndex
20 | {
21 | get;
22 | set;
23 | }
24 |
25 | public int Id
26 | {
27 | get;
28 | set;
29 | }
30 |
31 | public int Offset
32 | {
33 | get;
34 | set;
35 | }
36 |
37 | public int UncompressedSize
38 | {
39 | get;
40 | set;
41 | }
42 |
43 | public int CompressedSize
44 | {
45 | get;
46 | set;
47 | }
48 |
49 | public char Type
50 | {
51 | get;
52 | set;
53 | }
54 |
55 | public char SubType
56 | {
57 | get;
58 | set;
59 | }
60 |
61 | public void Read(BinaryReader reader)
62 | {
63 | Version = reader.ReadInt32();
64 | Id = reader.ReadInt32();
65 | UncompressedSize = reader.ReadInt32();
66 | CompressedSize = reader.ReadInt32();
67 | Offset = reader.ReadInt32();
68 | Type = (char)reader.ReadByte();
69 | SubType = (char)reader.ReadByte();
70 | reader.ReadInt32();
71 | if (Version > 0x16)
72 | ContentArchiveIndex = reader.ReadByte();
73 | }
74 |
75 | public void Write(BinaryWriter writer)
76 | {
77 | writer.Write(Version);
78 | writer.Write(Id);
79 | writer.Write(UncompressedSize);
80 | writer.Write(CompressedSize);
81 | writer.Write(Offset);
82 | writer.Write((byte)Type);
83 | writer.Write((byte)SubType);
84 | writer.Write(-1);
85 | if (Version > 0x16)
86 | writer.Write((byte)ContentArchiveIndex);
87 | }
88 |
89 | public override string ToString()
90 | {
91 | return $"{Id}:{ContentArchiveIndex} (type {Type}{SubType})";
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/CSystemArc.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {FBE058D8-AC2D-4A5E-A140-7BB0173755B8}
8 | Exe
9 | CSystemArc
10 | CSystemArc
11 | v4.8
12 | 512
13 | true
14 | true
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 | true
26 | 8
27 |
28 |
29 | AnyCPU
30 | pdbonly
31 | true
32 | bin\Release\
33 | TRACE
34 | prompt
35 | 4
36 | 8
37 | true
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ArchiveReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.IO.Compression;
5 | using System.Linq;
6 |
7 | namespace CSystemArc
8 | {
9 | internal class ArchiveReader : IDisposable
10 | {
11 | private readonly IList _contentStreams;
12 | private readonly bool _leaveOpen;
13 |
14 | private readonly Dictionary> _entries = new Dictionary>();
15 |
16 | public ArchiveReader(string indexFilePath, IList contentFilePaths)
17 | {
18 | _contentStreams = new List();
19 | foreach (string contentFilePath in contentFilePaths)
20 | {
21 | _contentStreams.Add(File.OpenRead(contentFilePath));
22 | }
23 |
24 | _leaveOpen = false;
25 |
26 | using Stream indexStream = File.OpenRead(indexFilePath);
27 | ReadEntries(indexStream);
28 | }
29 |
30 | public ArchiveReader(Stream indexStream, IList contentStreams, bool leaveOpen = false)
31 | {
32 | _contentStreams = contentStreams;
33 | _leaveOpen = leaveOpen;
34 |
35 | ReadEntries(indexStream);
36 | if (!leaveOpen)
37 | indexStream.Dispose();
38 | }
39 |
40 | private void ReadEntries(Stream compressedIndexStream)
41 | {
42 | byte[] index = BcdCompression.Decompress(compressedIndexStream);
43 |
44 | using Stream indexStream = new MemoryStream(index);
45 | BinaryReader indexReader = new BinaryReader(indexStream);
46 |
47 | int prevId = -1;
48 |
49 | while (indexStream.Position < indexStream.Length)
50 | {
51 | ArchiveEntry entry = new ArchiveEntry();
52 | entry.Read(indexReader);
53 |
54 | List entriesOfType = _entries.FetchValue(entry.Type, () => new List());
55 | entry.Index = entriesOfType.Count;
56 | entriesOfType.Add(entry);
57 |
58 | //if (entry.Id <= prevId)
59 | // throw new InvalidDataException("Entries not sorted by ID");
60 |
61 | prevId = entry.Id;
62 | }
63 | }
64 |
65 | public IEnumerable Entries => _entries.SelectMany(p => p.Value);
66 |
67 | public ArchiveEntry GetEntry(char type, int index)
68 | {
69 | return _entries[type][index];
70 | }
71 |
72 | public byte[] GetEntryContent(ArchiveEntry entry)
73 | {
74 | Stream contentStream = _contentStreams[entry.ContentArchiveIndex];
75 | contentStream.Position = entry.Offset;
76 |
77 | byte[] data = new byte[entry.UncompressedSize];
78 | if (entry.CompressedSize == entry.UncompressedSize)
79 | {
80 | contentStream.Read(data, 0, data.Length);
81 | }
82 | else
83 | {
84 | using LzssStream lzss = new LzssStream(contentStream, CompressionMode.Decompress, true);
85 | lzss.Read(data, 0, data.Length);
86 | }
87 | return data;
88 | }
89 |
90 | public void Dispose()
91 | {
92 | if (!_leaveOpen)
93 | {
94 | foreach (Stream contentStream in _contentStreams)
95 | {
96 | contentStream.Dispose();
97 | }
98 | }
99 | _contentStreams.Clear();
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/ArchiveWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.IO.Compression;
5 | using System.Linq;
6 |
7 | namespace CSystemArc
8 | {
9 | internal class ArchiveWriter : IDisposable
10 | {
11 | private const int MaxContentArchiveSize = 0x7FFFFFFF;
12 |
13 | private readonly int _version;
14 | private readonly Stream _indexStream;
15 | private readonly IList _contentStreams;
16 | private readonly bool _leaveOpen;
17 | private bool _disposed;
18 |
19 | private readonly List _entries = new List();
20 |
21 | public ArchiveWriter(int version, string indexFilePath, IList contentFilePaths)
22 | {
23 | _version = version;
24 | _indexStream = File.Open(indexFilePath, FileMode.Create, FileAccess.Write);
25 | _contentStreams = new List();
26 | foreach (string contentFilePath in contentFilePaths)
27 | {
28 | _contentStreams.Add(File.Open(contentFilePath, FileMode.Create, FileAccess.Write));
29 | }
30 |
31 | _leaveOpen = false;
32 | }
33 |
34 | public ArchiveWriter(int version, Stream indexStream, IList contentStreams, bool leaveOpen = false)
35 | {
36 | _version = version;
37 | _indexStream = indexStream;
38 | _contentStreams = contentStreams;
39 | _leaveOpen = leaveOpen;
40 | }
41 |
42 | public void Write(int id, char type, char subType, byte[] content)
43 | {
44 | Write(id, type, subType, new ArraySegment(content));
45 | }
46 |
47 | public void Write(int id, char type, char subType, ArraySegment content)
48 | {
49 | int contentArchiveIndex = 0;
50 | while (_contentStreams[contentArchiveIndex].Length + content.Count > MaxContentArchiveSize)
51 | {
52 | contentArchiveIndex++;
53 | }
54 |
55 | Stream contentStream = _contentStreams[contentArchiveIndex];
56 | ArchiveEntry entry =
57 | new ArchiveEntry
58 | {
59 | Version = _version,
60 | ContentArchiveIndex = contentArchiveIndex,
61 | Id = id,
62 | Type = type,
63 | SubType = subType,
64 | Offset = (int)contentStream.Position,
65 | UncompressedSize = content.Count
66 | };
67 |
68 | bool compressed = (type == 'b' ||
69 | type == 'c' ||
70 | type == 'e' ||
71 | type == 'n' ||
72 | type == 'o' ||
73 | type == 'p');
74 | if (compressed)
75 | {
76 | using LzssStream lzss = new LzssStream(contentStream, CompressionMode.Compress, true);
77 | lzss.Write(content.Array, content.Offset, content.Count);
78 | }
79 | else
80 | {
81 | contentStream.Write(content.Array, content.Offset, content.Count);
82 | }
83 |
84 | entry.CompressedSize = (int)contentStream.Position - entry.Offset;
85 | _entries.Add(entry);
86 | }
87 |
88 | private void WriteIndex()
89 | {
90 | using MemoryStream uncompressedIndexStream = new MemoryStream();
91 | BinaryWriter writer = new BinaryWriter(uncompressedIndexStream);
92 | foreach (ArchiveEntry entry in _entries.OrderBy(e => e.Id))
93 | {
94 | entry.Write(writer);
95 | }
96 |
97 | uncompressedIndexStream.TryGetBuffer(out ArraySegment uncompressedIndexData);
98 | BcdCompression.Compress(uncompressedIndexData, _indexStream);
99 | }
100 |
101 | public void Dispose()
102 | {
103 | if (_disposed)
104 | return;
105 |
106 | WriteIndex();
107 |
108 | if (!_leaveOpen)
109 | {
110 | foreach (Stream contentStream in _contentStreams)
111 | {
112 | contentStream.Dispose();
113 | }
114 |
115 | _indexStream?.Dispose();
116 | }
117 | _contentStreams.Clear();
118 | _disposed = true;
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/CSystemConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.IO;
5 | using System.Text;
6 | using System.Xml.Linq;
7 |
8 | namespace CSystemArc
9 | {
10 | internal class CSystemConfig
11 | {
12 | private static readonly Encoding SjisEncoding = Encoding.GetEncoding(932, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
13 |
14 | private List _items;
15 | private byte[] _data1;
16 | private byte[] _data2;
17 |
18 | public void Read(Stream stream)
19 | {
20 | byte[] stringData = BcdCompression.Decompress(stream);
21 | _items = UnpackItems(stringData);
22 |
23 | int data1Size = Bcd.Read(stream);
24 | _data1 = new byte[data1Size];
25 | stream.Read(_data1, 0, _data1.Length);
26 |
27 | int data2Size = Bcd.Read(stream);
28 | _data2 = new byte[data2Size];
29 | stream.Read(_data2, 0, _data2.Length);
30 |
31 | if (stream.Position != stream.Length)
32 | throw new InvalidDataException();
33 | }
34 |
35 | public void Write(Stream stream)
36 | {
37 | ArraySegment stringData = PackItems(_items);
38 | BcdCompression.Compress(stringData, stream);
39 |
40 | Bcd.Write(stream, _data1.Length);
41 | stream.Write(_data1, 0, _data1.Length);
42 |
43 | Bcd.Write(stream, _data2.Length);
44 | stream.Write(_data2, 0, _data2.Length);
45 | }
46 |
47 | public XDocument ToXml()
48 | {
49 | if (_items.Count > 8)
50 | throw new InvalidDataException("Unexpected number of items");
51 |
52 | XElement configElem =
53 | new XElement(
54 | "config",
55 | TextItemToXml(0),
56 | DictionaryItemToXml(1),
57 | TextItemToXml(2),
58 | TextItemToXml(3),
59 | TextItemToXml(4),
60 | TextItemToXml(5),
61 | BinaryItemToXml(6),
62 | BinaryItemToXml(7)
63 | );
64 |
65 | XElement data1Elem =
66 | new XElement(
67 | "data1",
68 | BytesToHex(_data1)
69 | );
70 |
71 | XElement data2Elem =
72 | new XElement(
73 | "data2",
74 | BytesToHex(_data2)
75 | );
76 |
77 | return new XDocument(
78 | new XElement(
79 | "csystem",
80 | configElem,
81 | data1Elem,
82 | data2Elem
83 | )
84 | );
85 | }
86 |
87 | public void FromXml(XDocument doc)
88 | {
89 | XElement root = doc.Root;
90 | if (root.Name != "csystem")
91 | throw new InvalidDataException("Invalid root element name");
92 |
93 | XElement configElem = root.Element("config");
94 | if (configElem == null)
95 | throw new InvalidDataException(" element missing");
96 |
97 | _items = new List();
98 | foreach (XElement itemElem in configElem.Elements("item"))
99 | {
100 | _items.Add(ItemFromXml(itemElem));
101 | }
102 |
103 | XElement data1Elem = root.Element("data1");
104 | if (data1Elem == null)
105 | throw new InvalidDataException(" element is missing");
106 |
107 | _data1 = HexToBytes(data1Elem.Value);
108 |
109 | XElement data2Elem = root.Element("data2");
110 | if (data2Elem == null)
111 | throw new InvalidDataException(" element is missing");
112 |
113 | _data2 = HexToBytes(data2Elem.Value);
114 | }
115 |
116 | private static List UnpackItems(byte[] data)
117 | {
118 | MemoryStream stream = new MemoryStream(data);
119 | BinaryReader reader = new BinaryReader(stream);
120 | List items = new List();
121 | while (stream.Position < stream.Length)
122 | {
123 | int length = reader.ReadInt32();
124 | byte[] item = reader.ReadBytes(length);
125 |
126 | if (item.Length > 0 && item[0] == (byte)'S')
127 | {
128 | byte[] xorlessItem = new byte[item.Length - 4];
129 | xorlessItem[0] = item[0];
130 | Array.Copy(item, 5, xorlessItem, 1, item.Length - 5);
131 | item = xorlessItem;
132 | }
133 |
134 | if (item.Length >= 2 && item[item.Length - 2] == 0xFE && item[item.Length - 1] == 0xA)
135 | {
136 | byte[] trimmedItem = new byte[item.Length - 2];
137 | Array.Copy(item, trimmedItem, trimmedItem.Length);
138 | item = trimmedItem;
139 | }
140 |
141 | items.Add(item);
142 | }
143 | return items;
144 | }
145 |
146 | private static ArraySegment PackItems(List items)
147 | {
148 | MemoryStream stream = new MemoryStream();
149 | BinaryWriter writer = new BinaryWriter(stream);
150 | foreach (byte[] item in items)
151 | {
152 | byte[] itemToWrite = item;
153 | if (item[0] == (byte)'S')
154 | {
155 | byte[] xoredItem = new byte[item.Length + 4];
156 | xoredItem[0] = item[0];
157 | Array.Copy(item, 1, xoredItem, 5, item.Length - 1);
158 | itemToWrite = xoredItem;
159 | }
160 |
161 | writer.Write(itemToWrite.Length);
162 | writer.Write(itemToWrite);
163 | }
164 |
165 | stream.TryGetBuffer(out ArraySegment data);
166 | return data;
167 | }
168 |
169 | private XElement BinaryItemToXml(int index)
170 | {
171 | if (index >= _items.Count)
172 | return null;
173 |
174 | byte[] item = _items[index];
175 | return new XElement(
176 | "item",
177 | new XAttribute("type", "binary"),
178 | BytesToHex(item)
179 | );
180 | }
181 |
182 | private XElement TextItemToXml(int index)
183 | {
184 | if (index >= _items.Count)
185 | return null;
186 |
187 | try
188 | {
189 | string text = SjisEncoding.GetString(_items[index]);
190 | return new XElement(
191 | "item",
192 | new XAttribute("type", "text"),
193 | text
194 | );
195 | }
196 | catch (DecoderFallbackException)
197 | {
198 | return BinaryItemToXml(index);
199 | }
200 | }
201 |
202 | private XElement DictionaryItemToXml(int index)
203 | {
204 | if (index >= _items.Count)
205 | return null;
206 |
207 | byte[] item = _items[index];
208 |
209 | XElement dictElem =
210 | new XElement(
211 | "item",
212 | new XAttribute("type", "dict")
213 | );
214 |
215 | if ((char)item[0] != '#')
216 | throw new InvalidDataException();
217 |
218 | int offset = 1;
219 | while (offset < item.Length)
220 | {
221 | string key = null;
222 | while (true)
223 | {
224 | char c = (char)item[offset++];
225 | if (c == ':')
226 | break;
227 |
228 | key += c;
229 | }
230 |
231 | int valueOffset = offset;
232 | int value = 0;
233 | while (true)
234 | {
235 | byte b = item[offset++];
236 | if (b == 201)
237 | break;
238 |
239 | if (b <= 200)
240 | value += b;
241 | else if (b == 250)
242 | value = 0;
243 | }
244 |
245 | if (offset == valueOffset + 1)
246 | value = -1;
247 |
248 | dictElem.Add(new XElement("entry", new XAttribute("key", key), value.ToString()));
249 | }
250 |
251 | return dictElem;
252 | }
253 |
254 | private static byte[] ItemFromXml(XElement elem)
255 | {
256 | switch (elem.Attribute("type")?.Value)
257 | {
258 | case "binary":
259 | return BinaryItemFromXml(elem);
260 |
261 | case "text":
262 | return TextItemFromXml(elem);
263 |
264 | case "dict":
265 | return DictionaryItemFromXml(elem);
266 |
267 | default:
268 | throw new InvalidDataException("Unrecognized type for - ");
269 | }
270 | }
271 |
272 | private static byte[] BinaryItemFromXml(XElement elem)
273 | {
274 | return HexToBytes(elem.Value);
275 | }
276 |
277 | private static byte[] TextItemFromXml(XElement elem)
278 | {
279 | return SjisEncoding.GetBytes(elem.Value);
280 | }
281 |
282 | private static byte[] DictionaryItemFromXml(XElement elem)
283 | {
284 | MemoryStream stream = new MemoryStream();
285 | stream.WriteByte((byte)'#');
286 |
287 | foreach (XElement entry in elem.Elements("entry"))
288 | {
289 | string key = entry.Attribute("key").Value;
290 | foreach (char c in key)
291 | {
292 | stream.WriteByte((byte)c);
293 | }
294 |
295 | stream.WriteByte((byte)':');
296 |
297 | int value = int.Parse(entry.Value);
298 | if (value < -1)
299 | {
300 | throw new InvalidDataException("Dictionary values can't be less than -1");
301 | }
302 | else if (value == -1)
303 | {
304 | }
305 | else if (value == 0)
306 | {
307 | stream.WriteByte(250);
308 | }
309 | else
310 | {
311 | while (value > 200)
312 | {
313 | stream.WriteByte(200);
314 | value -= 200;
315 | }
316 | stream.WriteByte((byte)value);
317 | }
318 |
319 | stream.WriteByte(201);
320 | }
321 |
322 | byte[] item = new byte[stream.Length];
323 | stream.Position = 0;
324 | stream.Read(item, 0, item.Length);
325 | return item;
326 | }
327 |
328 | private static string BytesToHex(byte[] bytes)
329 | {
330 | StringBuilder hex = new StringBuilder();
331 | foreach (byte b in bytes)
332 | {
333 | hex.AppendFormat("{0:X02} ", b);
334 | }
335 |
336 | if (hex.Length > 0)
337 | hex.Length--;
338 |
339 | return hex.ToString();
340 | }
341 |
342 | private static byte[] HexToBytes(string hex)
343 | {
344 | hex = hex.Replace(" ", "");
345 | if (hex.Length % 2 != 0)
346 | throw new InvalidDataException("Hex string must have an even number of digits");
347 |
348 | byte[] bytes = new byte[hex.Length / 2];
349 | for (int i = 0; i < bytes.Length; i++)
350 | {
351 | bytes[i] = byte.Parse(hex.Substring(2 * i, 2), NumberStyles.HexNumber);
352 | }
353 | return bytes;
354 | }
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Text.RegularExpressions;
7 | using System.Xml.Linq;
8 |
9 | namespace CSystemArc
10 | {
11 | public static class Program
12 | {
13 | public static void Main(string[] args)
14 | {
15 | if (args.Length == 0)
16 | {
17 | PrintUsage();
18 | return;
19 | }
20 |
21 | try
22 | {
23 | switch (args[0])
24 | {
25 | case "unpack":
26 | if (args.Length < 4)
27 | {
28 | PrintUsage();
29 | return;
30 | }
31 |
32 | Unpack(args[1], args.Skip(2).Take(args.Length - 3).ToList(), args.Last());
33 | break;
34 |
35 | case "pack":
36 | if (args.Length < 5)
37 | {
38 | PrintUsage();
39 | return;
40 | }
41 |
42 | if (!int.TryParse(args[1], out int version))
43 | {
44 | Console.WriteLine($"{args[1]} is not a valid version number");
45 | break;
46 | }
47 |
48 | Pack(version, args[2], args[3], args.Skip(4).ToList());
49 | break;
50 |
51 | case "readconfig":
52 | if (args.Length != 3)
53 | {
54 | PrintUsage();
55 | return;
56 | }
57 |
58 | ReadConfig(args[1], args[2]);
59 | break;
60 |
61 | case "writeconfig":
62 | if (args.Length != 3)
63 | {
64 | PrintUsage();
65 | return;
66 | }
67 |
68 | WriteConfig(args[1], args[2]);
69 | break;
70 |
71 | default:
72 | PrintUsage();
73 | break;
74 | }
75 | }
76 | catch (Exception ex)
77 | {
78 | Console.WriteLine(ex.Message);
79 | }
80 | }
81 |
82 | private static void Unpack(string indexFilePath, IList contentFilePaths, string folderPath)
83 | {
84 | VerifyFileExists(indexFilePath);
85 | foreach (string contentFilePath in contentFilePaths)
86 | {
87 | VerifyFileExists(contentFilePath);
88 | }
89 | VerifyFolderExists(folderPath);
90 |
91 | using ArchiveReader reader = new ArchiveReader(indexFilePath, contentFilePaths);
92 | IEnumerable entries = reader.Entries;
93 | List failedEntries;
94 | bool versionPrinted = false;
95 | do
96 | {
97 | failedEntries = new List();
98 | foreach (ArchiveEntry entry in entries)
99 | {
100 | if (!versionPrinted)
101 | {
102 | Console.WriteLine($"Archive version: {entry.Version}");
103 | versionPrinted = true;
104 | }
105 |
106 | if (!TryUnpackEntry(reader, entry, folderPath))
107 | failedEntries.Add(entry);
108 | }
109 | entries = failedEntries;
110 | } while (failedEntries.Count > 0);
111 | }
112 |
113 | private static bool TryUnpackEntry(ArchiveReader reader, ArchiveEntry entry, string folderPath)
114 | {
115 | Console.WriteLine($"Unpacking {entry.Id:d06} (type {entry.Type}{entry.SubType})");
116 | byte[] content = reader.GetEntryContent(entry);
117 | switch (entry.Type)
118 | {
119 | case 'b':
120 | return TryUnpackImage(reader, entry, content, folderPath);
121 |
122 | default:
123 | UnpackRaw(entry, content, folderPath);
124 | return true;
125 | }
126 | }
127 |
128 | private static bool TryUnpackImage(ArchiveReader reader, ArchiveEntry entry, byte[] content, string folderPath)
129 | {
130 | CSystemImage image = new CSystemImage();
131 | image.Read(new MemoryStream(content));
132 |
133 | string fileName = GetImageFileName(entry.Id);
134 | if (image.BaseIndex == -1 || image.BaseIndex == entry.Index)
135 | {
136 | image.SaveAsStandardImage(Path.Combine(folderPath, fileName));
137 | return true;
138 | }
139 |
140 | ArchiveEntry baseEntry = reader.GetEntry(entry.Type, image.BaseIndex);
141 | string baseFolderPath = Path.Combine(folderPath, GetImageFolderName(baseEntry.Id));
142 | Directory.CreateDirectory(baseFolderPath);
143 |
144 | string baseFileName = GetImageFileName(baseEntry.Id);
145 | if (File.Exists(Path.Combine(folderPath, baseFileName)))
146 | File.Move(Path.Combine(folderPath, baseFileName), Path.Combine(baseFolderPath, baseFileName));
147 |
148 | if (!File.Exists(Path.Combine(baseFolderPath, baseFileName)))
149 | return false;
150 |
151 | CSystemImage baseImage = new CSystemImage();
152 | baseImage.LoadStandardImageAsCSystem(Path.Combine(baseFolderPath, baseFileName));
153 | image.ConvertDeltaToFull(baseImage);
154 | image.SaveAsStandardImage(Path.Combine(baseFolderPath, fileName));
155 | return true;
156 | }
157 |
158 | private static void UnpackRaw(ArchiveEntry entry, byte[] content, string folderPath)
159 | {
160 | string filePath = Path.Combine(folderPath, GetRawFileName(entry.Id, entry.Type, entry.SubType));
161 | File.WriteAllBytes(filePath, content);
162 | }
163 |
164 | private static void Pack(int version, string folderPath, string indexFilePath, IList contentFilePaths)
165 | {
166 | VerifyFolderExists(folderPath);
167 |
168 | using ArchiveWriter writer = new ArchiveWriter(version, indexFilePath, contentFilePaths);
169 | PackRawFiles(folderPath, writer);
170 | PackImages(folderPath, writer);
171 | }
172 |
173 | private static void PackRawFiles(string folderPath, ArchiveWriter writer)
174 | {
175 | foreach (string filePath in Directory.EnumerateFiles(folderPath))
176 | {
177 | if (!TryParseRawFileName(Path.GetFileName(filePath), out int id, out char type, out char subType))
178 | continue;
179 |
180 | Console.WriteLine($"Packing {id:d06} (type {type}{subType})");
181 | byte[] content = File.ReadAllBytes(filePath);
182 | writer.Write(id, type, subType, content);
183 | }
184 | }
185 |
186 | private static void PackImages(string folderPath, ArchiveWriter writer)
187 | {
188 | List rootIds = new List();
189 | rootIds.AddRange(
190 | Directory.EnumerateFiles(folderPath, "*.png")
191 | .Select(f => int.Parse(Path.GetFileNameWithoutExtension(f)))
192 | );
193 | rootIds.AddRange(
194 | Directory.EnumerateDirectories(folderPath)
195 | .Select(f => int.Parse(Path.GetFileName(f)))
196 | );
197 | rootIds.Sort();
198 |
199 | int index = 0;
200 |
201 | foreach (int id in rootIds)
202 | {
203 | Console.WriteLine($"Packing {id:d04} (type b)");
204 |
205 | string deltaFolderPath = Path.Combine(folderPath, GetImageFolderName(id));
206 | if (Directory.Exists(deltaFolderPath))
207 | PackDeltaImages(ref index, deltaFolderPath, writer);
208 | else
209 | PackFullImage(ref index, id, Path.Combine(folderPath, GetImageFileName(id)), writer);
210 | }
211 | }
212 |
213 | private static void PackDeltaImages(ref int index, string folderPath, ArchiveWriter writer)
214 | {
215 | int baseIndex = 0;
216 | CSystemImage baseImage = null;
217 | foreach (string filePath in Directory.EnumerateFiles(folderPath, "*.png"))
218 | {
219 | int id = int.Parse(Path.GetFileNameWithoutExtension(filePath));
220 |
221 | if (baseImage == null)
222 | {
223 | baseIndex = index;
224 | baseImage = new CSystemImage();
225 | baseImage.LoadStandardImageAsCSystem(filePath);
226 |
227 | PackFullImage(ref index, id, filePath, writer);
228 | }
229 | else
230 | {
231 | CSystemImage deltaImage = new CSystemImage(baseIndex);
232 | deltaImage.LoadStandardImageAsCSystem(filePath);
233 | deltaImage.ConvertFullToDelta(baseImage);
234 | PackImage(ref index, id, deltaImage, writer);
235 | }
236 | }
237 | }
238 |
239 | private static void PackFullImage(ref int index, int id, string filePath, ArchiveWriter writer)
240 | {
241 | CSystemImage image = new CSystemImage(index);
242 | image.LoadStandardImageAsWrapper(filePath);
243 | PackImage(ref index, id, image, writer);
244 | }
245 |
246 | private static void PackImage(ref int index, int id, CSystemImage image, ArchiveWriter writer)
247 | {
248 | MemoryStream contentStream = new MemoryStream();
249 | image.Write(contentStream);
250 |
251 | contentStream.TryGetBuffer(out ArraySegment content);
252 | writer.Write(id, 'b', '0', content);
253 |
254 | index++;
255 | }
256 |
257 | private static string GetRawFileName(int id, char type, char subType)
258 | {
259 | return $"{id:d06}.{type}{subType}";
260 | }
261 |
262 | private static bool TryParseRawFileName(string fileName, out int id, out char type, out char subType)
263 | {
264 | Match match = Regex.Match(fileName, @"^(\d+)\.(\w)(\w)?$");
265 | if (!match.Success)
266 | {
267 | id = 0;
268 | type = '\0';
269 | subType = '\0';
270 | return false;
271 | }
272 |
273 | id = int.Parse(match.Groups[1].Value);
274 | type = match.Groups[2].Value[0];
275 | subType = match.Groups[3].Success ? match.Groups[3].Value[0] : '0';
276 | return true;
277 | }
278 |
279 | private static string GetImageFolderName(int id)
280 | {
281 | return $"{id:d06}";
282 | }
283 |
284 | private static string GetImageFileName(int id)
285 | {
286 | return $"{id:d06}.png";
287 | }
288 |
289 | private static void ReadConfig(string datFilePath, string xmlFilePath)
290 | {
291 | VerifyFileExists(datFilePath);
292 |
293 | CSystemConfig config = new CSystemConfig();
294 |
295 | byte[] datContent = File.ReadAllBytes(datFilePath);
296 | config.Read(new MemoryStream(datContent));
297 |
298 | XDocument doc = config.ToXml();
299 | doc.Save(xmlFilePath);
300 | }
301 |
302 | private static void WriteConfig(string xmlFilePath, string datFilePath)
303 | {
304 | VerifyFileExists(xmlFilePath);
305 |
306 | CSystemConfig config = new CSystemConfig();
307 | XDocument doc = XDocument.Load(xmlFilePath);
308 | config.FromXml(doc);
309 |
310 | using Stream datStream = File.Open(datFilePath, FileMode.Create, FileAccess.Write);
311 | config.Write(datStream);
312 | }
313 |
314 | private static void VerifyFileExists(string filePath)
315 | {
316 | if (!File.Exists(filePath))
317 | throw new FileNotFoundException($"File {filePath} does not exist.");
318 | }
319 |
320 | private static void VerifyFolderExists(string folderPath)
321 | {
322 | if (!Directory.Exists(folderPath))
323 | throw new DirectoryNotFoundException($"Folder {folderPath} does not exist.");
324 | }
325 |
326 | private static void PrintUsage()
327 | {
328 | string assembly = Assembly.GetEntryAssembly().GetName().Name;
329 | Console.WriteLine("Usage:");
330 | Console.WriteLine($" {assembly} unpack index.dat content1.dat content2.dat ... folder");
331 | Console.WriteLine($" {assembly} pack version folder index.dat content1.dat content2.dat ...");
332 | Console.WriteLine($" {assembly} readconfig config.dat config.xml");
333 | Console.WriteLine($" {assembly} writeconfig config.xml config.dat");
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/CSystemImage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Drawing;
4 | using System.Drawing.Imaging;
5 | using System.IO;
6 |
7 | namespace CSystemArc
8 | {
9 | internal class CSystemImage
10 | {
11 | private const byte AttrValueBase = 0xE9;
12 | private const byte AttrValueEndMarker = 0xEF;
13 | private const byte AttrValueZeroMarker = 0xFB;
14 |
15 | private byte[] _mask;
16 | private byte[] _alpha;
17 | private byte[] _color;
18 | private byte[] _standardImage;
19 |
20 | public CSystemImage()
21 | {
22 | BaseIndex = -1;
23 | }
24 |
25 | public CSystemImage(int baseIndex)
26 | {
27 | BaseIndex = baseIndex;
28 | }
29 |
30 | public int BaseIndex
31 | {
32 | get;
33 | private set;
34 | }
35 |
36 | public int Width
37 | {
38 | get;
39 | private set;
40 | }
41 |
42 | public int Height
43 | {
44 | get;
45 | private set;
46 | }
47 |
48 | public void Read(Stream stream)
49 | {
50 | BinaryReader reader = new BinaryReader(stream);
51 |
52 | char type = (char)reader.ReadByte();
53 | switch (type)
54 | {
55 | case 'a': // Custom bitmap (full with alpha, or delta on top of other custom bitmap without alpha)
56 | case 'd': // Custom bitmap delta on top of standard image (with alpha)
57 | ReadCSystemImage(reader);
58 | break;
59 |
60 | case 'c': // Standard image
61 | ReadStandardWrapperImage(reader);
62 | break;
63 |
64 | default:
65 | throw new NotSupportedException();
66 | }
67 | }
68 |
69 | private void ReadCSystemImage(BinaryReader reader)
70 | {
71 | reader.BaseStream.Position += 1;
72 |
73 | BaseIndex = ReadAttrValue(reader);
74 | ReadAttrValue(reader); // field_C
75 | ReadAttrValue(reader); // field_38
76 | Width = ReadAttrValue(reader);
77 | ReadAttrValue(reader); // field_3C
78 | Height = ReadAttrValue(reader);
79 | ReadAttrValue(reader); // field_18
80 | ReadAttrValue(reader); // field_34
81 | ReadAttrValue(reader); // field_1C
82 | int alphaSize = ReadAttrValue(reader);
83 | ReadAttrValue(reader); // field_2C
84 | int colorSize = ReadAttrValue(reader);
85 | ReadAttrValue(reader); // field_28
86 | ReadAttrValue(reader); // flags - 1: has mask, 4: has alpha
87 | ReadAttrValue(reader); // field_40
88 | ReadAttrValue(reader); // field_44
89 | ReadAttrValue(reader); // field_48
90 | int maskSize = ReadAttrValue(reader);
91 | ReadAttrValue(reader); // field_4C
92 | ReadAttrValue(reader); // field_8
93 |
94 | if (maskSize > 0)
95 | _mask = reader.ReadBytes(maskSize);
96 |
97 | if (alphaSize > 0)
98 | _alpha = reader.ReadBytes(alphaSize);
99 |
100 | if (colorSize > 0)
101 | _color = reader.ReadBytes(colorSize);
102 | }
103 |
104 | private void ReadStandardWrapperImage(BinaryReader reader)
105 | {
106 | int length = reader.ReadByte();
107 | length = (length << 8) | reader.ReadByte();
108 | length = (length << 8) | reader.ReadByte();
109 | length = (length << 8) | reader.ReadByte();
110 | _standardImage = reader.ReadBytes(length);
111 | }
112 |
113 | public void Write(Stream stream)
114 | {
115 | BinaryWriter writer = new BinaryWriter(stream);
116 | if (_standardImage != null)
117 | WriteStandardWrapperImage(writer);
118 | else
119 | WriteCSystemImage(writer);
120 | }
121 |
122 | private void WriteCSystemImage(BinaryWriter writer)
123 | {
124 | int flags = 1;
125 | if (_alpha != null)
126 | flags |= 6;
127 |
128 | writer.Write(_mask == null ? (byte)'a' : (byte)'d');
129 | writer.Write(AttrValueEndMarker);
130 | WriteAttrValue(writer, BaseIndex);
131 | WriteAttrValue(writer, 0); // field_C
132 | WriteAttrValue(writer, 0); // field_38
133 | WriteAttrValue(writer, Width);
134 | WriteAttrValue(writer, 0); // field_3C
135 | WriteAttrValue(writer, Height);
136 | WriteAttrValue(writer, 0); // field_18
137 | WriteAttrValue(writer, 0); // field_34
138 | WriteAttrValue(writer, 0); // field_1C
139 | WriteAttrValue(writer, _alpha?.Length ?? 0);
140 | WriteAttrValue(writer, 0); // field_2C
141 | WriteAttrValue(writer, _color?.Length ?? 0);
142 | WriteAttrValue(writer, 0); // field_28
143 | WriteAttrValue(writer, flags);
144 | WriteAttrValue(writer, 0); // field_40
145 | WriteAttrValue(writer, 0); // field_44
146 | WriteAttrValue(writer, 0); // field_48
147 | WriteAttrValue(writer, _mask?.Length ?? 0);
148 | WriteAttrValue(writer, 0); // field_4C
149 | WriteAttrValue(writer, 0); // field_8
150 |
151 | if (_mask != null)
152 | writer.Write(_mask);
153 |
154 | if (_alpha != null)
155 | writer.Write(_alpha);
156 |
157 | if (_color != null)
158 | writer.Write(_color);
159 | }
160 |
161 | private void WriteStandardWrapperImage(BinaryWriter writer)
162 | {
163 | writer.Write((byte)'c');
164 | writer.Write((byte)(_standardImage.Length >> 24));
165 | writer.Write((byte)(_standardImage.Length >> 16));
166 | writer.Write((byte)(_standardImage.Length >> 8));
167 | writer.Write((byte)(_standardImage.Length));
168 | writer.Write(_standardImage);
169 | }
170 |
171 | public void ConvertDeltaToFull(CSystemImage baseImage)
172 | {
173 | if (_mask == null)
174 | throw new InvalidOperationException("Image is already a full image");
175 |
176 | if (baseImage._mask != null)
177 | throw new InvalidOperationException("Base image is not a full image");
178 |
179 | if (baseImage._standardImage != null)
180 | throw new InvalidOperationException("Base image must be a CSystem image");
181 |
182 | Width = baseImage.Width;
183 | Height = baseImage.Height;
184 |
185 | byte[] mergedColor = new byte[(Width * 3 + (Width & 3)) * Height];
186 | byte[] mergedAlpha = baseImage._alpha != null ? new byte[mergedColor.Length] : null;
187 |
188 | int fullOffset = 0;
189 | int maskOffset = 0;
190 | int maskBit = 1;
191 | int deltaColorOffset = 0;
192 | int deltaAlphaOffset = 0;
193 | for (int y = 0; y < Height; y++)
194 | {
195 | for (int x = 0; x < Width; x++)
196 | {
197 | if ((_mask[maskOffset] & maskBit) != 0)
198 | {
199 | mergedColor[fullOffset + 0] = _color[deltaColorOffset + 0];
200 | mergedColor[fullOffset + 1] = _color[deltaColorOffset + 1];
201 | mergedColor[fullOffset + 2] = _color[deltaColorOffset + 2];
202 |
203 | if (mergedAlpha != null)
204 | {
205 | byte alpha = _alpha?[deltaAlphaOffset] ?? baseImage._alpha[fullOffset];
206 | mergedAlpha[fullOffset + 0] = alpha;
207 | mergedAlpha[fullOffset + 1] = alpha;
208 | mergedAlpha[fullOffset + 2] = alpha;
209 | }
210 |
211 | deltaColorOffset += 3;
212 | deltaAlphaOffset++;
213 | }
214 | else
215 | {
216 | mergedColor[fullOffset + 0] = baseImage._color[fullOffset + 0];
217 | mergedColor[fullOffset + 1] = baseImage._color[fullOffset + 1];
218 | mergedColor[fullOffset + 2] = baseImage._color[fullOffset + 2];
219 |
220 | if (mergedAlpha != null)
221 | {
222 | mergedAlpha[fullOffset + 0] = baseImage._alpha[fullOffset + 0];
223 | mergedAlpha[fullOffset + 1] = baseImage._alpha[fullOffset + 0];
224 | mergedAlpha[fullOffset + 2] = baseImage._alpha[fullOffset + 0];
225 | }
226 | }
227 |
228 | fullOffset += 3;
229 |
230 | maskBit <<= 1;
231 | if (maskBit == 0x100)
232 | {
233 | maskOffset++;
234 | maskBit = 1;
235 | }
236 | }
237 | fullOffset += Width & 3;
238 | }
239 |
240 | _mask = null;
241 | _color = mergedColor;
242 | _alpha = mergedAlpha;
243 | }
244 |
245 | public void ConvertFullToDelta(CSystemImage baseImage)
246 | {
247 | if (_mask != null)
248 | throw new InvalidOperationException("Image is already a delta image");
249 |
250 | if (baseImage._mask != null)
251 | throw new InvalidOperationException("Base image is not a full image");
252 |
253 | if (baseImage._standardImage != null)
254 | throw new InvalidOperationException("Base image must be a CSystem image");
255 |
256 | List newMask = new List();
257 | List newColor = new List();
258 | List newAlpha = new List();
259 |
260 | int fullOffset = 0;
261 |
262 | int maskBit = 1;
263 | byte maskByte = 0;
264 | for (int y = 0; y < Height; y++)
265 | {
266 | for (int x = 0; x < Width; x++)
267 | {
268 | if (_color[fullOffset + 0] != baseImage._color[fullOffset + 0] ||
269 | _color[fullOffset + 1] != baseImage._color[fullOffset + 1] ||
270 | _color[fullOffset + 2] != baseImage._color[fullOffset + 2] ||
271 | (_alpha?[fullOffset + 0] ?? 0xFF) != (baseImage._alpha?[fullOffset + 0] ?? 0xFF))
272 | {
273 | maskByte = (byte)(maskByte | maskBit);
274 | newColor.Add(_color[fullOffset + 0]);
275 | newColor.Add(_color[fullOffset + 1]);
276 | newColor.Add(_color[fullOffset + 2]);
277 | newAlpha.Add(_alpha?[fullOffset + 0] ?? 0xFF);
278 | }
279 |
280 | fullOffset += 3;
281 |
282 | maskBit <<= 1;
283 | if (maskBit == 0x100)
284 | {
285 | newMask.Add(maskByte);
286 | maskByte = 0;
287 | maskBit = 1;
288 | }
289 | }
290 |
291 | fullOffset += Width & 3;
292 | }
293 |
294 | if (maskBit != 1)
295 | newMask.Add(maskByte);
296 |
297 | _mask = newMask.ToArray();
298 | _color = newColor.ToArray();
299 | _alpha = newAlpha.ToArray();
300 | }
301 |
302 | public unsafe void LoadStandardImageAsCSystem(string filePath)
303 | {
304 | using Image image = Image.FromFile(filePath);
305 | using Bitmap bitmap = new Bitmap(image);
306 | Width = bitmap.Width;
307 | Height = bitmap.Height;
308 | BitmapData data = bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
309 |
310 | _mask = null;
311 | _color = new byte[(Width * 3 + (Width & 3)) * Height];
312 | _alpha = new byte[(Width * 3 + (Width & 3)) * Height];
313 | _standardImage = null;
314 |
315 | bool alphaNeeded = false;
316 |
317 | byte* pInputRow = (byte*)data.Scan0;
318 | int outputOffset = 0;
319 | for (int y = 0; y < Height; y++)
320 | {
321 | for (int x = 0; x < Width; x++)
322 | {
323 | _color[outputOffset + 0] = pInputRow[4 * x + 0];
324 | _color[outputOffset + 1] = pInputRow[4 * x + 1];
325 | _color[outputOffset + 2] = pInputRow[4 * x + 2];
326 |
327 | _alpha[outputOffset + 0] = pInputRow[4 * x + 3];
328 | _alpha[outputOffset + 1] = pInputRow[4 * x + 3];
329 | _alpha[outputOffset + 2] = pInputRow[4 * x + 3];
330 |
331 | if (_alpha[outputOffset + 0] != 0xFF)
332 | alphaNeeded = true;
333 |
334 | outputOffset += 3;
335 | }
336 | pInputRow += data.Stride;
337 | outputOffset += Width & 3;
338 | }
339 |
340 | if (!alphaNeeded)
341 | _alpha = null;
342 |
343 | bitmap.UnlockBits(data);
344 | }
345 |
346 | public void LoadStandardImageAsWrapper(string filePath)
347 | {
348 | _mask = null;
349 | _color = null;
350 | _alpha = null;
351 | _standardImage = File.ReadAllBytes(filePath);
352 |
353 | using Image image = Image.FromStream(new MemoryStream(_standardImage));
354 | Width = image.Width;
355 | Height = image.Height;
356 | }
357 |
358 | public unsafe void SaveAsStandardImage(string filePath)
359 | {
360 | if (_standardImage != null)
361 | {
362 | using Image nativeImage = Image.FromStream(new MemoryStream(_standardImage));
363 | nativeImage.Save(filePath);
364 | return;
365 | }
366 |
367 | if (_mask != null)
368 | throw new InvalidOperationException("Can't save delta images (apply base first)");
369 |
370 | byte[] argb = new byte[Width * 4 * Height];
371 | int inputOffset = 0;
372 | int outputOffset = 0;
373 | for (int y = 0; y < Height; y++)
374 | {
375 | for (int x = 0; x < Width; x++)
376 | {
377 | argb[outputOffset + 0] = _color[inputOffset + 0];
378 | argb[outputOffset + 1] = _color[inputOffset + 1];
379 | argb[outputOffset + 2] = _color[inputOffset + 2];
380 | argb[outputOffset + 3] = _alpha?[inputOffset + 0] ?? 0xFF;
381 | inputOffset += 3;
382 | outputOffset += 4;
383 | }
384 |
385 | inputOffset += Width & 3;
386 | }
387 |
388 | fixed (byte* pArgb = argb)
389 | {
390 | using Image nativeImage = new Bitmap(Width, Height, 4 * Width, PixelFormat.Format32bppArgb, (IntPtr)pArgb);
391 | nativeImage.Save(filePath);
392 | }
393 | }
394 |
395 | private static int ReadAttrValue(BinaryReader reader)
396 | {
397 | byte b = reader.ReadByte();
398 | int units = b != AttrValueZeroMarker ? b : 0;
399 |
400 | int hundreds = 0;
401 | int tens = 0;
402 | while (true)
403 | {
404 | b = reader.ReadByte();
405 | if (b == AttrValueEndMarker)
406 | break;
407 |
408 | if (b == AttrValueBase)
409 | hundreds++;
410 | else
411 | tens = b != AttrValueZeroMarker ? b : 0;
412 | }
413 |
414 | return (hundreds * AttrValueBase + tens) * AttrValueBase + units;
415 | }
416 |
417 | private static void WriteAttrValue(BinaryWriter writer, int value)
418 | {
419 | int hundreds = Math.DivRem(Math.DivRem(value, AttrValueBase, out int units), AttrValueBase, out int tens);
420 | writer.Write((byte)(units == 0 ? AttrValueZeroMarker : units));
421 | if (tens != 0)
422 | writer.Write((byte)tens);
423 |
424 | for (int i = 0; i < hundreds; i++)
425 | {
426 | writer.Write(AttrValueBase);
427 | }
428 |
429 | writer.Write(AttrValueEndMarker);
430 | }
431 | }
432 | }
433 |
--------------------------------------------------------------------------------
/LzssStream.cs:
--------------------------------------------------------------------------------
1 |
2 | /*MIT License
3 |
4 | Copyright(c) 2019 differentrain
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
24 | */
25 | using System.Collections.Concurrent;
26 |
27 | namespace System.IO.Compression
28 | {
29 | ///
30 | /// Provides methods and properties for compressing and decompressing streams by using the LZSS algorithm.
31 | ///
32 | internal class LzssStream : Stream
33 | {
34 | private const int DefaultWindowSize = 0x1000;
35 | private const byte DefaultCharFiller = 0x00;
36 | private const int DefaultMaxMatchLength = 18;
37 | private const byte DefaultMatchThresold = 2;
38 |
39 |
40 | private readonly CompressionMode _mode;
41 | private readonly bool _leaveOpen;
42 |
43 | private SaluteToHaruhikoTheOkami _LzssHelper;
44 |
45 | ///
46 | /// Gets a reference to the underlying stream.
47 | ///
48 | /// A stream object that represents the underlying stream.
49 | public Stream BaseStream { get; private set; }
50 |
51 | ///
52 | /// The compressed size.
53 | ///
54 | public int LastCodeLength { get; private set; }
55 |
56 | ///
57 | /// Gets the size of sliding window.
58 | /// Default value is 4096.
59 | ///
60 | protected virtual int WindowSize => DefaultWindowSize;
61 | ///
62 | /// Gets a byte value represents an ASCII char to fill the array.
63 | /// Default value is 0x20 (White space).
64 | ///
65 | protected virtual byte CharFiller => DefaultCharFiller;
66 | ///
67 | /// Gets the upper limit for the length of match.
68 | /// Default value is 18.
69 | ///
70 | protected virtual int MaxMatchLength => DefaultMaxMatchLength;
71 | ///
72 | /// Gets the value of triggering threshold if the match length is greater than whom should be encoded.
73 | /// Default value is 2.
74 | ///
75 | protected virtual byte MatchThresold => DefaultMatchThresold;
76 |
77 |
78 | ///
79 | /// Initializes a new instance of the class by using the specified stream and compression mode.
80 | ///
81 | /// The stream to compress or decompress.
82 | /// One of the enumeration values that indicates whether to compress or decompress the stream.
83 | /// is null.
84 | ///
85 | /// is a valid value.
86 | /// -or-
87 | /// is and is false.
88 | /// -or-
89 | /// is and is false.
90 | ///
91 | public LzssStream(Stream stream, CompressionMode mode) : this(stream, mode, false) { }
92 |
93 | ///
94 | /// Initializes a new instance of the class by using the specified stream and compression mode, and optionally leaves the stream open.
95 | ///
96 | /// The stream to compress or decompress.
97 | /// One of the enumeration values that indicates whether to compress or decompress the stream.
98 | /// true to leave the stream open after disposing the DeflateStream object; otherwise, false.
99 | /// is null.
100 | ///
101 | /// is a valid value.
102 | /// -or-
103 | /// is and is false.
104 | /// -or-
105 | /// is and is false.
106 | ///
107 | public LzssStream(Stream stream, CompressionMode mode, bool leaveOpen)
108 | {
109 | if (CompressionMode.Compress != mode && CompressionMode.Decompress != mode) throw new ArgumentException("mode is a valid CompressionMode value.", nameof(mode));
110 |
111 | BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
112 | _mode = mode;
113 | _leaveOpen = leaveOpen;
114 |
115 | switch (_mode)
116 | {
117 | case CompressionMode.Decompress:
118 | if (!BaseStream.CanRead) throw new ArgumentException("mode is CompressionMode.Decompress and CanRead is false.", nameof(stream));
119 | break;
120 | case CompressionMode.Compress:
121 |
122 | if (!BaseStream.CanWrite) throw new ArgumentException("mode is CompressionMode.Compress and CanWrite is false.", nameof(stream));
123 | break;
124 | }
125 | _LzssHelper = SaluteToHaruhikoTheOkami.RentInstance(this);
126 | }
127 |
128 | ///
129 | /// Reads a number of decompressed bytes into the specified byte array.
130 | ///
131 | /// The array to store decompressed bytes.
132 | /// The byte offset in at which the read bytes will be placed.
133 | /// The maximum number of decompressed bytes to read.
134 | /// The number of bytes that were read into the byte array.
135 | /// is null.
136 | /// The value was when the object was created.
137 | /// -or-
138 | /// The underlying stream does not support reading.
139 | ///
140 | ///
141 | /// or is less than zero.
142 | /// -or-
143 | /// length minus the index starting point is less than .
144 | ///
145 | /// The data is in an invalid format.
146 | /// The stream is closed.
147 | public override int Read(byte[] array, int offset, int count)
148 | {
149 | CheckReadAndWriteMethodCore(array, offset, count, CompressionMode.Compress);
150 | return _LzssHelper.Decode(array, offset, count, BaseStream);
151 | }
152 |
153 |
154 | ///
155 | /// Writes compressed bytes to the underlying stream from the specified byte array.
156 | ///
157 | /// The buffer that contains the data to compress.
158 | /// The byte offset in from which the bytes will be read.
159 | /// The maximum number of bytes to write.
160 | /// is null.
161 | /// The value was when the object was created.
162 | /// -or-
163 | /// The underlying stream does not support writing.
164 | ///
165 | ///
166 | /// or is less than zero.
167 | /// -or-
168 | /// length minus the index starting point is less than .
169 | ///
170 | /// The stream is closed.
171 | public override void Write(byte[] array, int offset, int count)
172 | {
173 | CheckReadAndWriteMethodCore(array, offset, count, CompressionMode.Decompress);
174 | LastCodeLength = _LzssHelper.Encode(array, offset, count, BaseStream);
175 | }
176 |
177 |
178 |
179 | private void EnsureNotDisposed()
180 | {
181 | if (BaseStream == null) throw new ObjectDisposedException(null, "the Stream is closed.");
182 | }
183 |
184 | private void CheckReadAndWriteMethodCore(byte[] array, int offset, int count, CompressionMode wrongMode)
185 | {
186 | EnsureNotDisposed();
187 | if (array == null)
188 | throw new ArgumentNullException(nameof(array));
189 |
190 | if (offset < 0)
191 | throw new ArgumentOutOfRangeException(nameof(offset));
192 |
193 | if (count < 0)
194 | throw new ArgumentOutOfRangeException(nameof(count));
195 |
196 | if (array.Length - offset < count)
197 | throw new ArgumentException("array length minus the index starting point is less than count.");
198 |
199 | if (_mode == wrongMode) throw new InvalidOperationException($"The CompressionMode value was {wrongMode} when the object was created.");
200 | }
201 |
202 | ///
203 | /// Dispose mode.
204 | ///
205 | ///
206 | protected override void Dispose(bool disposing)
207 | {
208 | try
209 | {
210 | if (disposing)
211 | {
212 | if (_LzssHelper != null)
213 | {
214 | SaluteToHaruhikoTheOkami.ReturnInstance(_LzssHelper);
215 | }
216 | if (!_leaveOpen && BaseStream != null)
217 | {
218 | BaseStream.Dispose();
219 | }
220 | }
221 | }
222 | finally
223 | {
224 | _LzssHelper = null;
225 | BaseStream = null;
226 | base.Dispose(disposing);
227 | }
228 | }
229 |
230 | #region implements the members of base class.
231 |
232 | ///
233 | /// Gets a value indicating whether the stream supports reading while decompressing a file.
234 | ///
235 | /// true if the value is , and the underlying stream is opened and supports reading; otherwise, false.
236 | public override bool CanRead => BaseStream != null && _mode == CompressionMode.Decompress && BaseStream.CanRead;
237 |
238 | ///
239 | /// Gets a value indicating whether the stream supports seeking.
240 | ///
241 | /// false in all cases.
242 | public override bool CanSeek => false;
243 |
244 | ///
245 | /// Gets a value indicating whether the stream supports writing.
246 | ///
247 | /// true if the value is , and the underlying stream is opened and supports writing; otherwise, false.
248 | public override bool CanWrite => BaseStream != null && _mode == CompressionMode.Compress && BaseStream.CanWrite;
249 |
250 | ///
251 | /// This property is not supported and always throws a .
252 | ///
253 | /// A long value.
254 | /// This property is not supported on this stream.
255 | public override long Length => throw new NotSupportedException();
256 |
257 | ///
258 | /// This property is not supported and always throws a .
259 | ///
260 | /// A long value.
261 | /// This property is not supported on this stream.
262 | public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
263 |
264 | ///
265 | /// The current implementation of this method has no functionality.
266 | ///
267 | /// The Stream is closed.
268 | public override void Flush() => EnsureNotDisposed();
269 |
270 | ///
271 | /// This operation is not supported and always throws a .
272 | ///
273 | /// The location in the stream.
274 | /// One of the values.
275 | /// A long value.
276 | /// This operation is not supported on this stream.
277 | public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
278 |
279 | ///
280 | /// This operation is not supported and always throws a .
281 | ///
282 | /// The length of the stream.
283 | /// This operation is not supported on this stream.
284 | public override void SetLength(long value) => throw new NotSupportedException();
285 |
286 | #endregion
287 |
288 |
289 |
290 | ///
291 | /// The people who have ever known about lzss certainly know the name Haruhiko Okumura, and his "lzss.c".
292 | /// As a foolish mortal, I just copy his code here directly.
293 | ///
294 | private sealed class SaluteToHaruhikoTheOkami
295 | {
296 |
297 | #region object pool
298 |
299 | private static readonly ConcurrentDictionary>
300 | _instancePool = new ConcurrentDictionary>();
301 |
302 |
303 | public static SaluteToHaruhikoTheOkami RentInstance(LzssStream lzssStream)
304 | {
305 | var lOpion = new LzssOption(lzssStream);
306 |
307 | var bag = _instancePool.GetOrAdd(lOpion, new ConcurrentBag());
308 |
309 | if (!bag.TryTake(out var inst))
310 | {
311 | inst = new SaluteToHaruhikoTheOkami(lOpion);
312 | }
313 | return inst;
314 | }
315 |
316 | public static void ReturnInstance(SaluteToHaruhikoTheOkami inst) => _instancePool[inst._lzssOption].Add(inst);
317 |
318 | private sealed class LzssOption
319 | {
320 | ///
321 | /// size of ring buffer
322 | ///
323 | public int N { get; }
324 | ///
325 | /// upper limit for match_length
326 | ///
327 | public int F { get; }
328 | ///
329 | /// encode string into position and length if match_length is greater than this
330 | ///
331 | public int THRESHOLD { get; }
332 | ///
333 | /// Clear the buffer with any character that will appear often.
334 | ///
335 | public byte FILL { get; }
336 |
337 | public LzssOption(LzssStream lzss)
338 | {
339 | N = lzss.WindowSize;
340 | F = lzss.MaxMatchLength;
341 | THRESHOLD = lzss.MatchThresold;
342 | FILL = lzss.CharFiller;
343 | }
344 |
345 | public override bool Equals(object obj) => (!(obj is LzssOption inst) ||
346 | inst.FILL != this.FILL ||
347 | inst.N != this.N ||
348 | inst.F != this.F ||
349 | inst.THRESHOLD != this.THRESHOLD
350 | ) ? false : true;
351 |
352 | public override int GetHashCode() => CombineHashCodes(FILL, N, F, THRESHOLD);
353 |
354 | private static int CombineHashCodes(params int[] hashCodes)
355 | {
356 | int hash1 = (5381 << 16) + 5381;
357 | int hash2 = hash1;
358 |
359 | int i = 0;
360 | foreach (var hashCode in hashCodes)
361 | {
362 | if (i % 2 == 0)
363 | hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ hashCode;
364 | else
365 | hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ hashCode;
366 |
367 | ++i;
368 | }
369 | return hash1 + (hash2 * 1566083941);
370 | }
371 | }
372 |
373 |
374 | #endregion
375 |
376 | private readonly LzssOption _lzssOption;
377 |
378 | ///
379 | /// index for root of binary search trees
380 | ///
381 | private readonly int NIL;
382 |
383 | ///
384 | /// ring buffer of size N, with extra F-1 bytes to facilitate string comparison.
385 | ///
386 | private readonly byte[] text_buf;
387 |
388 | ///
389 | /// left children & right children & parents -- These constitute binary search trees.
390 | ///
391 | private readonly int[] lson, rson, dad;
392 |
393 | ///
394 | /// of longest match. These are set by the InsertNode() procedure.
395 | ///
396 | private int match_position, match_length;
397 |
398 | private SaluteToHaruhikoTheOkami(LzssOption lzssOp)
399 | {
400 | _lzssOption = lzssOp;
401 | NIL = _lzssOption.N;
402 | text_buf = new byte[_lzssOption.N + _lzssOption.F - 1];
403 | lson = new int[_lzssOption.N + 1];
404 | rson = new int[_lzssOption.N + 257];
405 | dad = new int[_lzssOption.N + 1];
406 | }
407 |
408 | ///
409 | /// initialize trees.
410 | ///
411 | private void InitTree()
412 | {
413 | int i;
414 |
415 | /* For i = 0 to N - 1, rson[i] and lson[i] will be the right and
416 | left children of node i. These nodes need not be initialized.
417 | Also, dad[i] is the parent of node i. These are initialized to
418 | NIL (= N), which stands for 'not used.'
419 | For i = 0 to 255, rson[N + i + 1] is the root of the tree
420 | for strings that begin with character i. These are initialized
421 | to NIL. Note there are 256 trees. */
422 |
423 | unsafe
424 | {
425 | fixed (int* pr = rson, pd = dad)
426 | {
427 | for (i = _lzssOption.N + 1; i <= _lzssOption.N + 256; i++)
428 | {
429 | pr[i] = NIL;
430 | }
431 |
432 | for (i = 0; i < _lzssOption.N; i++)
433 | {
434 | pd[i] = NIL;
435 | }
436 | }
437 | }
438 | }
439 |
440 | private void InsertNode(int r)
441 | {
442 | /* Inserts string of length F, text_buf[r..r+F-1], into one of the
443 | trees (text_buf[r]'th tree) and returns the longest-match position
444 | and length via the global variables match_position and match_length.
445 | If match_length = F, then removes the old node in favor of the new
446 | one, because the old one will be deleted sooner.
447 | Note r plays double role, as tree node and position in buffer. */
448 |
449 | int i;
450 | var cmp = 1;
451 | match_length = 0;
452 | unsafe
453 | {
454 | fixed (byte* key = &text_buf[r], pBuf = text_buf)
455 | {
456 | var p = _lzssOption.N + 1 + key[0];
457 | fixed (int* pR = rson, pL = lson, pD = dad)
458 | {
459 | pR[r] = pL[r] = NIL;
460 | for (; ; )
461 | {
462 | if (cmp >= 0)
463 | {
464 | if (pR[p] != NIL)
465 | {
466 | p = pR[p];
467 | }
468 | else
469 | {
470 | pR[p] = r;
471 | pD[r] = p;
472 | return;
473 | }
474 | }
475 | else
476 | {
477 | if (pL[p] != NIL)
478 | {
479 | p = pL[p];
480 | }
481 | else
482 | {
483 | pL[p] = r;
484 | pD[r] = p;
485 | return;
486 | }
487 | }
488 | for (i = 1; i < _lzssOption.F; i++)
489 | {
490 | if ((cmp = key[i] - pBuf[p + i]) != 0) break;
491 | }
492 |
493 | if (i > match_length)
494 | {
495 | match_position = p;
496 | if ((match_length = i) >= _lzssOption.F) break;
497 | }
498 | }
499 | pD[r] = pD[p]; pL[r] = pL[p]; pR[r] = pR[p];
500 | pD[pL[p]] = r; pD[pR[p]] = r;
501 | if (pR[pD[p]] == p)
502 | {
503 | pR[pD[p]] = r;
504 | }
505 | else
506 | {
507 | pL[pD[p]] = r;
508 | }
509 | pD[p] = NIL; /* remove p */
510 | }
511 | }
512 | }
513 |
514 |
515 |
516 |
517 | }
518 |
519 | ///
520 | /// deletes node p from tree
521 | ///
522 | ///
523 | private void DeleteNode(int p)
524 | {
525 | int q;
526 |
527 | unsafe
528 | {
529 | fixed (int* pR = rson, pL = lson, pD = dad)
530 | {
531 | if (pD[p] == NIL) return; /* not in tree */
532 | if (pR[p] == NIL)
533 | {
534 | q = pL[p];
535 | }
536 | else if (pL[p] == NIL)
537 | {
538 | q = pR[p];
539 | }
540 | else
541 | {
542 | q = pL[p];
543 | if (pR[q] != NIL)
544 | {
545 | do
546 | {
547 | q = pR[q];
548 | } while (pR[q] != NIL);
549 | pR[pD[q]] = pL[q]; pD[pL[q]] = pD[q];
550 | pL[q] = pL[p]; pD[pL[p]] = q;
551 | }
552 | pR[q] = pR[p]; pD[pR[p]] = q;
553 | }
554 | pD[q] = pD[p];
555 | if (pR[pD[p]] == p)
556 | {
557 | pR[pD[p]] = q;
558 | }
559 | else
560 | {
561 | pL[pD[p]] = q;
562 | }
563 | pD[p] = NIL;
564 | }
565 | }
566 |
567 |
568 | }
569 |
570 |
571 | public int Encode(byte[] array, int offset, int count, Stream stream)
572 | {
573 | int i, len, last_match_length;
574 | byte mask, c;
575 | var codesize = 0;
576 |
577 | var code_buf_ptr = mask = 1;
578 | var s = 0;
579 | var r = _lzssOption.N - _lzssOption.F;
580 | var stopPos = count + offset;
581 | var arrayIdx = offset;
582 | byte[] code_buf = new byte[17];
583 |
584 | InitTree(); /* initialize trees */
585 |
586 | unsafe
587 | {
588 | fixed (byte* pcode_buf = code_buf, ptext_buf = text_buf, pArray = array)
589 | {
590 | pcode_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and
591 | code_buf[0] works as eight flags, "1" representing that the unit
592 | is an unencoded letter (1 byte), "0" a position-and-length pair
593 | (2 bytes). Thus, eight units require at most 16 bytes of code. */
594 |
595 | for (i = s; i < r; i++) /* Clear the buffer with any character that will appear often. */
596 | {
597 | ptext_buf[i] = _lzssOption.FILL;
598 | }
599 |
600 | for (len = 0; len < _lzssOption.F && arrayIdx < stopPos; len++)
601 | {
602 | ptext_buf[r + len] = pArray[arrayIdx++]; /* Read F bytes into the last F bytes of the buffer */
603 | }
604 |
605 | if (len == 0)
606 | {
607 | return 0; /* text of size zero */
608 | }
609 | for (i = 1; i <= _lzssOption.F; i++)
610 | {
611 | /* Insert the F strings,
612 | each of which begins with one or more 'space' characters. Note
613 | the order in which these strings are inserted. This way,
614 | degenerate trees will be less likely to occur. */
615 | InsertNode(r - i);
616 | }
617 | InsertNode(r); /* Finally, insert the whole string just read. The
618 | global variables match_length and match_position are set. */
619 | do
620 | {
621 | if (match_length > len)
622 | {
623 | match_length = len; /* match_length
624 | may be spuriously long near the end of text. */
625 | }
626 | if (match_length <= _lzssOption.THRESHOLD)
627 | {
628 | match_length = 1; /* Not long enough match. Send one byte. */
629 | pcode_buf[0] |= mask; /* 'send one byte' flag */
630 | pcode_buf[code_buf_ptr++] = ptext_buf[r]; /* Send uncoded. */
631 | }
632 | else
633 | {
634 | pcode_buf[code_buf_ptr++] = (byte)match_position;
635 | pcode_buf[code_buf_ptr++] = (byte)(((match_position >> 4) & 0xf0)
636 | | (match_length - (_lzssOption.THRESHOLD + 1))); /* Send position and
637 | length pair. Note match_length > THRESHOLD. */
638 | }
639 | if ((mask <<= 1) == 0) /* Shift mask left one bit. */
640 | {
641 | stream.Write(code_buf, 0, code_buf_ptr); /* Send at most 8 units of code together */
642 | codesize += code_buf_ptr;
643 | pcode_buf[0] = 0; code_buf_ptr = mask = 1;
644 | }
645 | last_match_length = match_length;
646 | for (i = 0; i < last_match_length && arrayIdx < stopPos; i++)
647 | {
648 | DeleteNode(s); /* Delete old strings and */
649 | c = pArray[arrayIdx++];
650 | ptext_buf[s] = c; /* read new bytes */
651 | if (s < _lzssOption.F - 1)
652 | {
653 | ptext_buf[s + _lzssOption.N] = c; /* If the position is near the end of buffer, extend the buffer to make string comparison easier. */
654 | }
655 | s = (s + 1) & (_lzssOption.N - 1);
656 | r = (r + 1) & (_lzssOption.N - 1);
657 | /* Since this is a ring buffer, increment the position
658 | modulo N. */
659 | InsertNode(r); /* Register the string in text_buf[r..r+F-1] */
660 | }
661 |
662 | while (i++ < last_match_length)
663 | { /* After the end of text, */
664 | DeleteNode(s); /* no need to read, but */
665 | s = (s + 1) & (_lzssOption.N - 1);
666 | r = (r + 1) & (_lzssOption.N - 1);
667 | --len;
668 | if (len != 0) InsertNode(r); /* buffer may not be empty. */
669 | }
670 | } while (len > 0); /* until length of string to be processed is zero */
671 |
672 | if (code_buf_ptr > 1)
673 | { /* Send remaining code. */
674 | stream.Write(code_buf, 0, code_buf_ptr);
675 | codesize += code_buf_ptr;
676 | }
677 | stream.Flush();
678 | return codesize;
679 | }
680 | }
681 | }
682 |
683 | public int Decode(byte[] array, int offset, int count, Stream stream) /* Just the reverse of Encode(). */
684 | {
685 | int i, j, k, c;
686 | var flags = 0U;
687 | int r = _lzssOption.N - _lzssOption.F;
688 | var stopPos = count + offset;
689 | int arrayIdx = offset;
690 | unsafe
691 | {
692 | fixed (byte* pArray = array, ptext_buf = text_buf)
693 | {
694 | for (i = 0; i < _lzssOption.N - _lzssOption.F; i++)
695 | {
696 | ptext_buf[i] = _lzssOption.FILL;
697 | }
698 | for (; ; )
699 | {
700 | if (((flags >>= 1) & 256) == 0)
701 | {
702 | if ((c = stream.ReadByte()) == -1) break;
703 | flags = (uint)c | 0xff00U; /* uses higher byte cleverly to count eight */
704 | }
705 | if ((flags & 1) != 0)
706 | {
707 | if ((c = stream.ReadByte()) == -1 || arrayIdx == stopPos) break;
708 | pArray[arrayIdx++] = ptext_buf[r++] = (byte)c;
709 | r &= (_lzssOption.N - 1);
710 | }
711 | else
712 | {
713 | if ((i = stream.ReadByte()) == -1 || (j = stream.ReadByte()) == -1) break;
714 | i |= ((j & 0xf0) << 4);
715 | j = (j & 0x0f) + _lzssOption.THRESHOLD;
716 | for (k = 0; k <= j; k++)
717 | {
718 | if (arrayIdx == stopPos) break;
719 | c = ptext_buf[(i + k) & (_lzssOption.N - 1)];
720 | pArray[arrayIdx++] = ptext_buf[r++] = (byte)c;
721 | r &= (_lzssOption.N - 1);
722 | }
723 | }
724 | }
725 | }
726 | }
727 | return arrayIdx - offset;
728 | }
729 | }
730 | }
731 | }
732 |
--------------------------------------------------------------------------------