├── 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 | --------------------------------------------------------------------------------