Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
296
OpenRA.Mods.Common/FileFormats/Blast.cs
Normal file
296
OpenRA.Mods.Common/FileFormats/Blast.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
#region Additional Copyright & License Information
|
||||
/*
|
||||
* This file is based on the blast routines (version 1.1 by Mark Adler)
|
||||
* included in zlib/contrib
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public static class Blast
|
||||
{
|
||||
public const int MAXBITS = 13; // maximum code length
|
||||
public const int MAXWIN = 4096; // maximum window size
|
||||
|
||||
static readonly byte[] LitLen =
|
||||
[
|
||||
11, 124, 8, 7, 28, 7, 188, 13, 76, 4,
|
||||
10, 8, 12, 10, 12, 10, 8, 23, 8, 9,
|
||||
7, 6, 7, 8, 7, 6, 55, 8, 23, 24,
|
||||
12, 11, 7, 9, 11, 12, 6, 7, 22, 5,
|
||||
7, 24, 6, 11, 9, 6, 7, 22, 7, 11,
|
||||
38, 7, 9, 8, 25, 11, 8, 11, 9, 12,
|
||||
8, 12, 5, 38, 5, 38, 5, 11, 7, 5,
|
||||
6, 21, 6, 10, 53, 8, 7, 24, 10, 27,
|
||||
44, 253, 253, 253, 252, 252, 252, 13, 12, 45,
|
||||
12, 45, 12, 61, 12, 45, 44, 173
|
||||
];
|
||||
|
||||
// bit lengths of length codes 0..15
|
||||
static readonly byte[] LenLen = [2, 35, 36, 53, 38, 23];
|
||||
|
||||
// bit lengths of distance codes 0..63
|
||||
static readonly byte[] DistLen = [2, 20, 53, 230, 247, 151, 248];
|
||||
|
||||
// base for length codes
|
||||
static readonly short[] LengthBase =
|
||||
[
|
||||
3, 2, 4, 5, 6, 7, 8, 9, 10, 12,
|
||||
16, 24, 40, 72, 136, 264
|
||||
];
|
||||
|
||||
// extra bits for length codes
|
||||
static readonly byte[] Extra =
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 2,
|
||||
3, 4, 5, 6, 7, 8
|
||||
];
|
||||
|
||||
static readonly Huffman LitCode = new(LitLen, 256);
|
||||
static readonly Huffman LenCode = new(LenLen, 16);
|
||||
static readonly Huffman DistCode = new(DistLen, 64);
|
||||
|
||||
/// <summary>PKWare Compression Library stream.</summary>
|
||||
/// <param name="input">Compressed input stream.</param>
|
||||
/// <param name="output">Stream to write the decompressed output.</param>
|
||||
/// <param name="onProgress">Progress callback, invoked with (read bytes, written bytes).</param>
|
||||
public static void Decompress(Stream input, Stream output, Action<long, long> onProgress = null)
|
||||
{
|
||||
var br = new BitReader(input);
|
||||
|
||||
// Are literals coded?
|
||||
var coded = br.ReadBits(8);
|
||||
|
||||
if (coded < 0 || coded > 1)
|
||||
throw new NotImplementedException("Invalid data stream");
|
||||
var encodedLiterals = coded == 1;
|
||||
|
||||
// log2(dictionary size) - 6
|
||||
var dict = br.ReadBits(8);
|
||||
if (dict < 4 || dict > 6)
|
||||
throw new InvalidDataException("Invalid dictionary size");
|
||||
|
||||
// output state
|
||||
ushort next = 0; // index of next write location in out[]
|
||||
var first = true; // true to check distances (for first 4K)
|
||||
var outBuffer = new byte[MAXWIN]; // output buffer and sliding window
|
||||
|
||||
var inputStart = input.Position;
|
||||
var outputStart = output.Position;
|
||||
|
||||
// decode literals and length/distance pairs
|
||||
do
|
||||
{
|
||||
// length/distance pair
|
||||
if (br.ReadBits(1) == 1)
|
||||
{
|
||||
// Length
|
||||
var symbol = Decode(LenCode, br);
|
||||
var len = LengthBase[symbol] + br.ReadBits(Extra[symbol]);
|
||||
|
||||
// Magic number for "done"
|
||||
if (len == 519)
|
||||
{
|
||||
for (var i = 0; i < next; i++)
|
||||
output.WriteByte(outBuffer[i]);
|
||||
|
||||
onProgress?.Invoke(input.Position - inputStart, output.Position - outputStart);
|
||||
break;
|
||||
}
|
||||
|
||||
// Distance
|
||||
symbol = len == 2 ? 2 : dict;
|
||||
var dist = Decode(DistCode, br) << symbol;
|
||||
dist += br.ReadBits(symbol);
|
||||
dist++;
|
||||
|
||||
if (first && dist > next)
|
||||
throw new InvalidDataException("Attempt to jump before data");
|
||||
|
||||
// copy length bytes from distance bytes back
|
||||
do
|
||||
{
|
||||
var dest = next;
|
||||
var source = dest - dist;
|
||||
|
||||
var copy = MAXWIN;
|
||||
if (next < dist)
|
||||
{
|
||||
source += copy;
|
||||
copy = dist;
|
||||
}
|
||||
|
||||
copy -= next;
|
||||
if (copy > len)
|
||||
copy = len;
|
||||
|
||||
len -= copy;
|
||||
next += (ushort)copy;
|
||||
|
||||
// copy with old-fashioned memcpy semantics
|
||||
// in case of overlapping ranges. this is NOT
|
||||
// the same as Array.Copy()
|
||||
while (copy-- > 0)
|
||||
outBuffer[dest++] = outBuffer[source++];
|
||||
|
||||
// Flush window to outstream
|
||||
if (next == MAXWIN)
|
||||
{
|
||||
for (var i = 0; i < next; i++)
|
||||
output.WriteByte(outBuffer[i]);
|
||||
next = 0;
|
||||
first = false;
|
||||
|
||||
onProgress?.Invoke(input.Position - inputStart, output.Position - outputStart);
|
||||
}
|
||||
}
|
||||
while (len != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// literal value
|
||||
var symbol = encodedLiterals ? Decode(LitCode, br) : br.ReadBits(8);
|
||||
outBuffer[next++] = (byte)symbol;
|
||||
if (next == MAXWIN)
|
||||
{
|
||||
for (var i = 0; i < next; i++)
|
||||
output.WriteByte(outBuffer[i]);
|
||||
next = 0;
|
||||
first = false;
|
||||
|
||||
onProgress?.Invoke(input.Position - inputStart, output.Position - outputStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
|
||||
// Decode a code using Huffman table h.
|
||||
static int Decode(Huffman h, BitReader br)
|
||||
{
|
||||
var code = 0; // len bits being decoded
|
||||
var first = 0; // first code of length len
|
||||
var index = 0; // index of first code of length len in symbol table
|
||||
short next = 1;
|
||||
while (true)
|
||||
{
|
||||
code |= br.ReadBits(1) ^ 1; // invert code
|
||||
int count = h.Count[next++];
|
||||
if (code < first + count)
|
||||
return h.Symbol[index + (code - first)];
|
||||
|
||||
index += count;
|
||||
first += count;
|
||||
first <<= 1;
|
||||
code <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class BitReader
|
||||
{
|
||||
readonly Stream stream;
|
||||
byte bitBuffer = 0;
|
||||
int bitCount = 0;
|
||||
|
||||
public BitReader(Stream stream)
|
||||
{
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
public int ReadBits(int count)
|
||||
{
|
||||
var ret = 0;
|
||||
for (var filled = 0; filled < count; filled++)
|
||||
{
|
||||
if (bitCount == 0)
|
||||
{
|
||||
bitBuffer = stream.ReadUInt8();
|
||||
bitCount = 8;
|
||||
}
|
||||
|
||||
ret |= (bitBuffer & 1) << filled;
|
||||
bitBuffer >>= 1;
|
||||
bitCount--;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a list of repeated code lengths rep[0..n-1], where each byte is a
|
||||
* count (high four bits + 1) and a code length (low four bits), generate the
|
||||
* list of code lengths. This compaction reduces the size of the object code.
|
||||
* Then given the list of code lengths length[0..n-1] representing a canonical
|
||||
* Huffman code for n symbols, construct the tables required to decode those
|
||||
* codes. Those tables are the number of codes of each length, and the symbols
|
||||
* sorted by length, retaining their original order within each length.
|
||||
*/
|
||||
sealed class Huffman
|
||||
{
|
||||
public short[] Count; // number of symbols of each length
|
||||
public short[] Symbol; // canonically ordered symbols
|
||||
|
||||
public Huffman(byte[] rep, short symbolCount)
|
||||
{
|
||||
var length = new short[256]; // code lengths
|
||||
var s = 0; // current symbol
|
||||
|
||||
// convert compact repeat counts into symbol bit length list
|
||||
foreach (var code in rep)
|
||||
{
|
||||
var num = (code >> 4) + 1; // Number of codes (top four bits plus 1)
|
||||
var len = (byte)(code & 15); // Code length (low four bits)
|
||||
do
|
||||
length[s++] = len;
|
||||
while (--num > 0);
|
||||
}
|
||||
|
||||
var n = s;
|
||||
|
||||
// count number of codes of each length
|
||||
Count = new short[Blast.MAXBITS + 1];
|
||||
for (var i = 0; i < n; i++)
|
||||
Count[length[i]]++;
|
||||
|
||||
// no codes!
|
||||
if (Count[0] == n)
|
||||
return;
|
||||
|
||||
// check for an over-subscribed or incomplete set of lengths
|
||||
var left = 1; // one possible code of zero length
|
||||
for (var len = 1; len <= Blast.MAXBITS; len++)
|
||||
{
|
||||
left <<= 1; // one more bit, double codes left
|
||||
left -= Count[len]; // deduct count from possible codes
|
||||
if (left < 0)
|
||||
throw new InvalidDataException("over subscribed code set");
|
||||
}
|
||||
|
||||
// generate offsets into symbol table for each length for sorting
|
||||
var offs = new short[Blast.MAXBITS + 1];
|
||||
for (var len = 1; len < Blast.MAXBITS; len++)
|
||||
offs[len + 1] = (short)(offs[len] + Count[len]);
|
||||
|
||||
// put symbols in table sorted by length, by symbol order within each length
|
||||
Symbol = new short[symbolCount];
|
||||
for (short i = 0; i < n; i++)
|
||||
if (length[i] != 0)
|
||||
Symbol[offs[length[i]]++] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
OpenRA.Mods.Common/FileFormats/FastByteReader.cs
Normal file
43
OpenRA.Mods.Common/FileFormats/FastByteReader.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public class FastByteReader
|
||||
{
|
||||
readonly byte[] src;
|
||||
int offset;
|
||||
|
||||
public FastByteReader(byte[] src, int offset = 0)
|
||||
{
|
||||
this.src = src;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public bool Done() { return offset >= src.Length; }
|
||||
public byte ReadByte() { return src[offset++]; }
|
||||
public int ReadWord()
|
||||
{
|
||||
var x = ReadByte();
|
||||
return x | (ReadByte() << 8);
|
||||
}
|
||||
|
||||
public void CopyTo(byte[] dest, int offset, int count)
|
||||
{
|
||||
Array.Copy(src, this.offset, dest, offset, count);
|
||||
this.offset += count;
|
||||
}
|
||||
|
||||
public int Remaining() { return src.Length - offset; }
|
||||
}
|
||||
}
|
||||
87
OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs
Normal file
87
OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public static class ImaAdpcmReader
|
||||
{
|
||||
static readonly int[] IndexAdjust = [-1, -1, -1, -1, 2, 4, 6, 8];
|
||||
static readonly int[] StepTable =
|
||||
[
|
||||
7, 8, 9, 10, 11, 12, 13, 14, 16,
|
||||
17, 19, 21, 23, 25, 28, 31, 34, 37,
|
||||
41, 45, 50, 55, 60, 66, 73, 80, 88,
|
||||
97, 107, 118, 130, 143, 157, 173, 190, 209,
|
||||
230, 253, 279, 307, 337, 371, 408, 449, 494,
|
||||
544, 598, 658, 724, 796, 876, 963, 1060, 1166,
|
||||
1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749,
|
||||
3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
|
||||
7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289,
|
||||
16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
|
||||
];
|
||||
|
||||
public static short DecodeImaAdpcmSample(byte b, ref int index, ref int current)
|
||||
{
|
||||
var sb = (b & 8) != 0;
|
||||
b &= 7;
|
||||
|
||||
var delta = StepTable[index] * b / 4 + StepTable[index] / 8;
|
||||
if (sb)
|
||||
delta = -delta;
|
||||
|
||||
current += delta;
|
||||
if (current > short.MaxValue)
|
||||
current = short.MaxValue;
|
||||
|
||||
if (current < short.MinValue)
|
||||
current = short.MinValue;
|
||||
|
||||
index += IndexAdjust[b];
|
||||
if (index < 0)
|
||||
index = 0;
|
||||
|
||||
if (index > 88)
|
||||
index = 88;
|
||||
|
||||
return (short)current;
|
||||
}
|
||||
|
||||
public static void LoadImaAdpcmSound(ReadOnlySpan<byte> raw, ref int index, Span<byte> output)
|
||||
{
|
||||
var currentSample = 0;
|
||||
LoadImaAdpcmSound(raw, ref index, ref currentSample, output);
|
||||
}
|
||||
|
||||
public static void LoadImaAdpcmSound(ReadOnlySpan<byte> raw, ref int index, ref int currentSample, Span<byte> output)
|
||||
{
|
||||
var dataSize = raw.Length;
|
||||
if (output.Length != raw.Length * 4)
|
||||
throw new ArgumentException($"{nameof(output)} must be 4 times the length of {nameof(raw)}.", nameof(output));
|
||||
|
||||
var offset = 0;
|
||||
|
||||
while (dataSize-- > 0)
|
||||
{
|
||||
var b = raw[offset / 4];
|
||||
|
||||
var t = DecodeImaAdpcmSample(b, ref index, ref currentSample);
|
||||
output[offset++] = (byte)t;
|
||||
output[offset++] = (byte)(t >> 8);
|
||||
|
||||
t = DecodeImaAdpcmSample((byte)(b >> 4), ref index, ref currentSample);
|
||||
output[offset++] = (byte)t;
|
||||
output[offset++] = (byte)(t >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
OpenRA.Mods.Common/FileFormats/IniFile.cs
Normal file
151
OpenRA.Mods.Common/FileFormats/IniFile.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public class IniFile
|
||||
{
|
||||
readonly Dictionary<string, IniSection> sections = [];
|
||||
|
||||
public IniFile(Stream s)
|
||||
{
|
||||
using (s)
|
||||
Load(s);
|
||||
}
|
||||
|
||||
public IniFile(params Stream[] streams)
|
||||
{
|
||||
foreach (var s in streams)
|
||||
Load(s);
|
||||
}
|
||||
|
||||
public void Load(Stream s)
|
||||
{
|
||||
IniSection currentSection = null;
|
||||
foreach (var line in s.ReadAllLines())
|
||||
{
|
||||
if (line.Length == 0) continue;
|
||||
|
||||
switch (line[0])
|
||||
{
|
||||
case ';': break;
|
||||
case '[': currentSection = ProcessSection(line); break;
|
||||
default:
|
||||
// Skip everything before the first section
|
||||
if (currentSection != null)
|
||||
ProcessEntry(line, currentSection);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly Regex sectionPattern = new(@"^\[([^]]*)\]");
|
||||
|
||||
IniSection ProcessSection(string line)
|
||||
{
|
||||
var m = sectionPattern.Match(line);
|
||||
if (!m.Success)
|
||||
return null;
|
||||
var sectionName = m.Groups[1].Value.ToLowerInvariant();
|
||||
|
||||
if (!sections.TryGetValue(sectionName, out var ret))
|
||||
sections.Add(sectionName, ret = new IniSection(sectionName));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool ProcessEntry(string line, IniSection currentSection)
|
||||
{
|
||||
var comment = line.IndexOf(';');
|
||||
if (comment >= 0)
|
||||
line = line[..comment];
|
||||
|
||||
line = line.Trim();
|
||||
if (line.Length == 0)
|
||||
return false;
|
||||
|
||||
var key = line;
|
||||
var value = "";
|
||||
var eq = line.IndexOf('=');
|
||||
if (eq >= 0)
|
||||
{
|
||||
key = line[..eq].Trim();
|
||||
value = line[(eq + 1)..].Trim();
|
||||
}
|
||||
|
||||
if (currentSection == null)
|
||||
throw new InvalidOperationException("No current INI section");
|
||||
|
||||
if (!currentSection.Contains(key))
|
||||
currentSection.Add(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public IniSection GetSection(string s)
|
||||
{
|
||||
return GetSection(s, false);
|
||||
}
|
||||
|
||||
public IniSection GetSection(string s, bool allowFail)
|
||||
{
|
||||
if (sections.TryGetValue(s.ToLowerInvariant(), out var section))
|
||||
return section;
|
||||
|
||||
if (allowFail)
|
||||
return new IniSection(s);
|
||||
throw new InvalidOperationException("Section does not exist in map or rules: " + s);
|
||||
}
|
||||
|
||||
public IEnumerable<IniSection> Sections => sections.Values;
|
||||
}
|
||||
|
||||
public class IniSection : IEnumerable<KeyValuePair<string, string>>
|
||||
{
|
||||
public string Name { get; }
|
||||
readonly Dictionary<string, string> values = [];
|
||||
|
||||
public IniSection(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public void Add(string key, string value)
|
||||
{
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return values.ContainsKey(key);
|
||||
}
|
||||
|
||||
public string GetValue(string key, string defaultValue)
|
||||
{
|
||||
return values.TryGetValue(key, out var s) ? s : defaultValue;
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
|
||||
{
|
||||
return values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
461
OpenRA.Mods.Common/FileFormats/InstallShieldCABCompression.cs
Normal file
461
OpenRA.Mods.Common/FileFormats/InstallShieldCABCompression.cs
Normal file
@@ -0,0 +1,461 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public sealed class InstallShieldCABCompression
|
||||
{
|
||||
const uint MaxFileGroupCount = 71;
|
||||
|
||||
[Flags]
|
||||
enum CABFlags : ushort
|
||||
{
|
||||
FileSplit = 0x1,
|
||||
FileObfuscated = 0x2,
|
||||
FileCompressed = 0x4,
|
||||
FileInvalid = 0x8,
|
||||
}
|
||||
|
||||
enum LinkFlags : byte
|
||||
{
|
||||
Prev = 0x1,
|
||||
Next = 0x2
|
||||
}
|
||||
|
||||
readonly struct FileGroup
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly uint FirstFile;
|
||||
public readonly uint LastFile;
|
||||
|
||||
public FileGroup(Stream stream, long offset, uint version)
|
||||
{
|
||||
var nameOffset = stream.ReadUInt32();
|
||||
stream.Position += 18;
|
||||
|
||||
if (version <= 5)
|
||||
stream.Position += 54;
|
||||
|
||||
FirstFile = stream.ReadUInt32();
|
||||
LastFile = stream.ReadUInt32();
|
||||
|
||||
var pos = stream.Position;
|
||||
stream.Position = offset + nameOffset;
|
||||
Name = stream.ReadASCIIZ();
|
||||
stream.Position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct CabDescriptor
|
||||
{
|
||||
public readonly long FileTableOffset;
|
||||
public readonly uint FileTableSize;
|
||||
public readonly uint FileTableSize2;
|
||||
public readonly uint DirectoryCount;
|
||||
|
||||
public readonly uint FileCount;
|
||||
public readonly long FileTableOffset2;
|
||||
|
||||
public CabDescriptor(Stream stream)
|
||||
{
|
||||
FileTableOffset = stream.ReadUInt32();
|
||||
stream.Position += 4;
|
||||
FileTableSize = stream.ReadUInt32();
|
||||
FileTableSize2 = stream.ReadUInt32();
|
||||
DirectoryCount = stream.ReadUInt32();
|
||||
stream.Position += 8;
|
||||
FileCount = stream.ReadUInt32();
|
||||
FileTableOffset2 = stream.ReadUInt32();
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct DirectoryDescriptor
|
||||
{
|
||||
public readonly string Name;
|
||||
|
||||
public DirectoryDescriptor(Stream stream, long nameTableOffset)
|
||||
{
|
||||
var nameOffset = stream.ReadUInt32();
|
||||
var pos = stream.Position;
|
||||
|
||||
stream.Position = nameTableOffset + nameOffset;
|
||||
|
||||
Name = stream.ReadASCIIZ();
|
||||
stream.Position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct FileDescriptor
|
||||
{
|
||||
public readonly uint Index;
|
||||
public readonly CABFlags Flags;
|
||||
public readonly uint ExpandedSize;
|
||||
public readonly uint CompressedSize;
|
||||
public readonly uint DataOffset;
|
||||
|
||||
public readonly byte[] MD5;
|
||||
public readonly uint NameOffset;
|
||||
public readonly uint DirectoryIndex;
|
||||
public readonly uint LinkToPrevious;
|
||||
|
||||
public readonly uint LinkToNext;
|
||||
public readonly LinkFlags LinkFlags;
|
||||
public readonly ushort Volume;
|
||||
public readonly string Filename;
|
||||
|
||||
public FileDescriptor(Stream stream, uint index, long tableOffset, uint version)
|
||||
{
|
||||
Index = index;
|
||||
|
||||
if (version <= 5)
|
||||
{
|
||||
NameOffset = stream.ReadUInt32();
|
||||
DirectoryIndex = stream.ReadUInt32();
|
||||
Flags = (CABFlags)stream.ReadUInt16();
|
||||
ExpandedSize = stream.ReadUInt32();
|
||||
CompressedSize = stream.ReadUInt32();
|
||||
stream.Position += 20;
|
||||
DataOffset = stream.ReadUInt32();
|
||||
|
||||
MD5 = new byte[16];
|
||||
LinkToPrevious = 0;
|
||||
LinkToNext = 0;
|
||||
LinkFlags = 0;
|
||||
Volume = 0;
|
||||
|
||||
if ((Flags & CABFlags.FileInvalid) == 0)
|
||||
{
|
||||
var pos = stream.Position;
|
||||
stream.Position = tableOffset + NameOffset;
|
||||
Filename = stream.ReadASCIIZ();
|
||||
stream.Position = pos;
|
||||
}
|
||||
else
|
||||
Filename = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
Flags = (CABFlags)stream.ReadUInt16();
|
||||
ExpandedSize = stream.ReadUInt32();
|
||||
stream.Position += 4;
|
||||
CompressedSize = stream.ReadUInt32();
|
||||
|
||||
stream.Position += 4;
|
||||
DataOffset = stream.ReadUInt32();
|
||||
stream.Position += 4;
|
||||
MD5 = stream.ReadBytes(16);
|
||||
|
||||
stream.Position += 16;
|
||||
NameOffset = stream.ReadUInt32();
|
||||
DirectoryIndex = stream.ReadUInt16();
|
||||
stream.Position += 12;
|
||||
LinkToPrevious = stream.ReadUInt32();
|
||||
LinkToNext = stream.ReadUInt32();
|
||||
|
||||
LinkFlags = (LinkFlags)stream.ReadUInt8();
|
||||
Volume = stream.ReadUInt16();
|
||||
|
||||
var pos = stream.Position;
|
||||
stream.Position = tableOffset + NameOffset;
|
||||
Filename = stream.ReadASCIIZ();
|
||||
stream.Position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct CommonHeader(Stream stream)
|
||||
{
|
||||
public const long Size = 16;
|
||||
public readonly uint Version = stream.ReadUInt32();
|
||||
public readonly uint VolumeInfo = stream.ReadUInt32();
|
||||
public readonly long CabDescriptorOffset = stream.ReadUInt32();
|
||||
public readonly uint CabDescriptorSize = stream.ReadUInt32();
|
||||
}
|
||||
|
||||
readonly struct VolumeHeader(Stream stream)
|
||||
{
|
||||
public readonly uint DataOffset = stream.ReadUInt32();
|
||||
public readonly uint DataOffsetHigh = stream.ReadUInt32();
|
||||
public readonly uint FirstFileIndex = stream.ReadUInt32();
|
||||
public readonly uint LastFileIndex = stream.ReadUInt32();
|
||||
|
||||
public readonly uint FirstFileOffset = stream.ReadUInt32();
|
||||
public readonly uint FirstFileOffsetHigh = stream.ReadUInt32();
|
||||
public readonly uint FirstFileSizeExpanded = stream.ReadUInt32();
|
||||
public readonly uint FirstFileSizeExpandedHigh = stream.ReadUInt32();
|
||||
|
||||
public readonly uint FirstFileSizeCompressed = stream.ReadUInt32();
|
||||
public readonly uint FirstFileSizeCompressedHigh = stream.ReadUInt32();
|
||||
public readonly uint LastFileOffset = stream.ReadUInt32();
|
||||
public readonly uint LastFileOffsetHigh = stream.ReadUInt32();
|
||||
|
||||
public readonly uint LastFileSizeExpanded = stream.ReadUInt32();
|
||||
public readonly uint LastFileSizeExpandedHigh = stream.ReadUInt32();
|
||||
public readonly uint LastFileSizeCompressed = stream.ReadUInt32();
|
||||
public readonly uint LastFileSizeCompressedHigh = stream.ReadUInt32();
|
||||
}
|
||||
|
||||
sealed class CabExtracter
|
||||
{
|
||||
readonly FileDescriptor file;
|
||||
readonly Dictionary<int, Stream> volumes;
|
||||
|
||||
uint remainingInArchive;
|
||||
uint toExtract;
|
||||
|
||||
int currentVolumeID;
|
||||
Stream currentVolume;
|
||||
|
||||
public CabExtracter(FileDescriptor file, Dictionary<int, Stream> volumes)
|
||||
{
|
||||
this.file = file;
|
||||
this.volumes = volumes;
|
||||
|
||||
remainingInArchive = 0;
|
||||
toExtract = file.Flags.HasFlag(CABFlags.FileCompressed) ? file.CompressedSize : file.ExpandedSize;
|
||||
|
||||
SetVolume(file.Volume);
|
||||
}
|
||||
|
||||
public void CopyTo(Stream output, Action<int> onProgress)
|
||||
{
|
||||
if (file.Flags.HasFlag(CABFlags.FileCompressed))
|
||||
{
|
||||
var inf = new Inflater(true);
|
||||
var buffer = new byte[165535];
|
||||
do
|
||||
{
|
||||
var bytesToExtract = currentVolume.ReadUInt16();
|
||||
remainingInArchive -= 2;
|
||||
toExtract -= 2;
|
||||
inf.SetInput(GetBytes(bytesToExtract));
|
||||
toExtract -= bytesToExtract;
|
||||
while (!inf.IsNeedingInput)
|
||||
{
|
||||
onProgress?.Invoke((int)(100 * output.Position / file.ExpandedSize));
|
||||
|
||||
var inflated = inf.Inflate(buffer);
|
||||
output.Write(buffer, 0, inflated);
|
||||
}
|
||||
|
||||
inf.Reset();
|
||||
}
|
||||
while (toExtract > 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
do
|
||||
{
|
||||
onProgress?.Invoke((int)(100 * output.Position / file.ExpandedSize));
|
||||
|
||||
toExtract -= remainingInArchive;
|
||||
output.Write(GetBytes(remainingInArchive), 0, (int)remainingInArchive);
|
||||
}
|
||||
while (toExtract > 0);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] GetBytes(uint count)
|
||||
{
|
||||
if (count < remainingInArchive)
|
||||
{
|
||||
remainingInArchive -= count;
|
||||
return currentVolume.ReadBytes((int)count);
|
||||
}
|
||||
else
|
||||
{
|
||||
var outArray = new byte[count];
|
||||
var read = currentVolume.Read(outArray, 0, (int)remainingInArchive);
|
||||
if (toExtract > remainingInArchive)
|
||||
{
|
||||
SetVolume(currentVolumeID + 1);
|
||||
remainingInArchive -= (uint)currentVolume.Read(outArray, read, (int)count - read);
|
||||
}
|
||||
|
||||
return outArray;
|
||||
}
|
||||
}
|
||||
|
||||
void SetVolume(int newVolume)
|
||||
{
|
||||
currentVolumeID = newVolume;
|
||||
if (!volumes.TryGetValue(currentVolumeID, out currentVolume))
|
||||
throw new FileNotFoundException($"Volume {currentVolumeID} is not available");
|
||||
|
||||
currentVolume.Position = 0;
|
||||
if (currentVolume.ReadUInt32() != 0x28635349)
|
||||
throw new InvalidDataException("Not an Installshield CAB package");
|
||||
|
||||
uint fileOffset;
|
||||
if (file.Flags.HasFlag(CABFlags.FileSplit))
|
||||
{
|
||||
currentVolume.Position += CommonHeader.Size;
|
||||
var head = new VolumeHeader(currentVolume);
|
||||
if (file.Index == head.LastFileIndex)
|
||||
{
|
||||
if (file.Flags.HasFlag(CABFlags.FileCompressed))
|
||||
remainingInArchive = head.LastFileSizeCompressed;
|
||||
else
|
||||
remainingInArchive = head.LastFileSizeExpanded;
|
||||
|
||||
fileOffset = head.LastFileOffset;
|
||||
}
|
||||
else if (file.Index == head.FirstFileIndex)
|
||||
{
|
||||
if (file.Flags.HasFlag(CABFlags.FileCompressed))
|
||||
remainingInArchive = head.FirstFileSizeCompressed;
|
||||
else
|
||||
remainingInArchive = head.FirstFileSizeExpanded;
|
||||
|
||||
fileOffset = head.FirstFileOffset;
|
||||
}
|
||||
else
|
||||
throw new InvalidDataException("Cannot Resolve Remaining Stream");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (file.Flags.HasFlag(CABFlags.FileCompressed))
|
||||
remainingInArchive = file.CompressedSize;
|
||||
else
|
||||
remainingInArchive = file.ExpandedSize;
|
||||
|
||||
fileOffset = file.DataOffset;
|
||||
}
|
||||
|
||||
currentVolume.Position = fileOffset;
|
||||
}
|
||||
}
|
||||
|
||||
readonly Dictionary<string, FileDescriptor> index = [];
|
||||
readonly Dictionary<int, Stream> volumes;
|
||||
readonly uint version;
|
||||
|
||||
public InstallShieldCABCompression(Stream header, Dictionary<int, Stream> volumes)
|
||||
{
|
||||
this.volumes = volumes;
|
||||
|
||||
if (header.ReadUInt32() != 0x28635349)
|
||||
throw new InvalidDataException("Not an Installshield CAB package");
|
||||
|
||||
var versionTmp = header.ReadUInt32();
|
||||
|
||||
// Logic taken from UnShield
|
||||
// https://github.com/twogood/unshield/blob/1.5.1/lib/libunshield.c#L277-L288
|
||||
if (versionTmp >> 24 == 1)
|
||||
version = (versionTmp >> 12) & 0xf;
|
||||
else if (versionTmp >> 24 == 2 || versionTmp >> 24 == 4)
|
||||
{
|
||||
version = versionTmp & 0xffff;
|
||||
if (version != 0)
|
||||
version /= 100;
|
||||
}
|
||||
|
||||
header.Position += 4;
|
||||
var cabDescriptorOffset = header.ReadUInt32();
|
||||
header.Position = cabDescriptorOffset + 12;
|
||||
var cabDescriptor = new CabDescriptor(header);
|
||||
header.Position += 14;
|
||||
|
||||
var fileGroupOffsets = new uint[MaxFileGroupCount];
|
||||
for (var i = 0; i < MaxFileGroupCount; i++)
|
||||
fileGroupOffsets[i] = header.ReadUInt32();
|
||||
|
||||
header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset;
|
||||
var directories = new DirectoryDescriptor[cabDescriptor.DirectoryCount];
|
||||
for (var i = 0; i < directories.Length; i++)
|
||||
directories[i] = new DirectoryDescriptor(header, cabDescriptorOffset + cabDescriptor.FileTableOffset);
|
||||
|
||||
var fileGroups = new List<FileGroup>();
|
||||
foreach (var offset in fileGroupOffsets)
|
||||
{
|
||||
var nextOffset = offset;
|
||||
while (nextOffset != 0)
|
||||
{
|
||||
header.Position = cabDescriptorOffset + (long)nextOffset + 4;
|
||||
var descriptorOffset = header.ReadUInt32();
|
||||
nextOffset = header.ReadUInt32();
|
||||
header.Position = cabDescriptorOffset + descriptorOffset;
|
||||
|
||||
fileGroups.Add(new FileGroup(header, cabDescriptorOffset, version));
|
||||
}
|
||||
}
|
||||
|
||||
header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2;
|
||||
foreach (var fileGroup in fileGroups)
|
||||
{
|
||||
for (var i = fileGroup.FirstFile; i <= fileGroup.LastFile; i++)
|
||||
{
|
||||
if (version <= 5)
|
||||
{
|
||||
header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2 + i * 4;
|
||||
header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + header.ReadUInt32();
|
||||
}
|
||||
else
|
||||
header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2 + i * 0x57;
|
||||
|
||||
var file = new FileDescriptor(header, i, cabDescriptorOffset + cabDescriptor.FileTableOffset, version);
|
||||
var path = $"{fileGroup.Name}\\{directories[file.DirectoryIndex].Name}\\{file.Filename}";
|
||||
index[path] = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ExtractFile(string filename, Stream output, Action<int> onProgress = null)
|
||||
{
|
||||
if (!index.TryGetValue(filename, out var file))
|
||||
throw new FileNotFoundException(filename);
|
||||
|
||||
ExtractFile(file, output, onProgress);
|
||||
}
|
||||
|
||||
void ExtractFile(FileDescriptor file, Stream output, Action<int> onProgress = null)
|
||||
{
|
||||
if (file.Flags.HasFlag(CABFlags.FileInvalid))
|
||||
throw new InvalidDataException("File Invalid");
|
||||
|
||||
if (file.LinkFlags.HasFlag(LinkFlags.Prev))
|
||||
{
|
||||
var prev = index.Values.First(f => f.Index == file.LinkToPrevious);
|
||||
ExtractFile(prev, output, onProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.Flags.HasFlag(CABFlags.FileObfuscated))
|
||||
throw new NotImplementedException("Obfuscated files are not supported");
|
||||
|
||||
var extracter = new CabExtracter(file, volumes);
|
||||
extracter.CopyTo(output, onProgress);
|
||||
|
||||
if (output.Length != file.ExpandedSize)
|
||||
throw new InvalidDataException($"File expanded to wrong length. Expected = {file.ExpandedSize}, Got = {output.Length}");
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<int, IEnumerable<string>> Contents
|
||||
{
|
||||
get
|
||||
{
|
||||
var contents = new Dictionary<int, List<string>>();
|
||||
foreach (var kv in index)
|
||||
contents.GetOrAdd(kv.Value.Volume).Add(kv.Key);
|
||||
|
||||
return new ReadOnlyDictionary<int, IEnumerable<string>>(contents
|
||||
.ToDictionary(x => x.Key, x => x.Value.AsEnumerable()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
OpenRA.Mods.Common/FileFormats/MSCabCompression.cs
Normal file
132
OpenRA.Mods.Common/FileFormats/MSCabCompression.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public sealed class MSCabCompression
|
||||
{
|
||||
sealed class CabFolder
|
||||
{
|
||||
public readonly uint BlockOffset;
|
||||
public readonly ushort BlockCount;
|
||||
public readonly ushort CompressionType;
|
||||
|
||||
public CabFolder(Stream stream)
|
||||
{
|
||||
BlockOffset = stream.ReadUInt32();
|
||||
BlockCount = stream.ReadUInt16();
|
||||
CompressionType = stream.ReadUInt16();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class CabFile
|
||||
{
|
||||
public readonly string FileName;
|
||||
public readonly uint DecompressedLength;
|
||||
public readonly uint DecompressedOffset;
|
||||
public readonly ushort FolderIndex;
|
||||
|
||||
public CabFile(Stream stream)
|
||||
{
|
||||
DecompressedLength = stream.ReadUInt32();
|
||||
DecompressedOffset = stream.ReadUInt32();
|
||||
FolderIndex = stream.ReadUInt16();
|
||||
stream.Position += 6;
|
||||
FileName = stream.ReadASCIIZ();
|
||||
}
|
||||
}
|
||||
|
||||
readonly CabFolder[] folders;
|
||||
readonly CabFile[] files;
|
||||
readonly Stream stream;
|
||||
|
||||
public MSCabCompression(Stream stream)
|
||||
{
|
||||
this.stream = stream;
|
||||
|
||||
var signature = stream.ReadASCII(4);
|
||||
if (signature != "MSCF")
|
||||
throw new InvalidDataException("Not a Microsoft CAB package!");
|
||||
|
||||
stream.Position += 12;
|
||||
var filesOffset = stream.ReadUInt32();
|
||||
stream.Position += 6;
|
||||
var folderCount = stream.ReadUInt16();
|
||||
var fileCount = stream.ReadUInt16();
|
||||
if (stream.ReadUInt16() != 0)
|
||||
throw new InvalidDataException("Only plain packages (without reserved header space or prev/next archives) are supported!");
|
||||
|
||||
stream.Position += 4;
|
||||
|
||||
folders = new CabFolder[folderCount];
|
||||
for (var i = 0; i < folderCount; i++)
|
||||
{
|
||||
folders[i] = new CabFolder(stream);
|
||||
if (folders[i].CompressionType != 1)
|
||||
throw new InvalidDataException("Compression type is not supported");
|
||||
}
|
||||
|
||||
files = new CabFile[fileCount];
|
||||
stream.Seek(filesOffset, SeekOrigin.Begin);
|
||||
for (var i = 0; i < fileCount; i++)
|
||||
files[i] = new CabFile(stream);
|
||||
}
|
||||
|
||||
public void ExtractFile(string filename, Stream output, Action<int> onProgress = null)
|
||||
{
|
||||
var file = files.FirstOrDefault(f => f.FileName == filename);
|
||||
if (file == null)
|
||||
throw new FileNotFoundException(filename);
|
||||
|
||||
var folder = folders[file.FolderIndex];
|
||||
stream.Seek(folder.BlockOffset, SeekOrigin.Begin);
|
||||
|
||||
var inflater = new Inflater(true);
|
||||
var buffer = new byte[4096];
|
||||
var decompressedBytes = 0;
|
||||
for (var i = 0; i < folder.BlockCount; i++)
|
||||
{
|
||||
onProgress?.Invoke((int)(100 * output.Position / file.DecompressedLength));
|
||||
|
||||
// Ignore checksums
|
||||
stream.Position += 4;
|
||||
var blockLength = stream.ReadUInt16();
|
||||
stream.Position += 4;
|
||||
|
||||
using (var batch = new MemoryStream(stream.ReadBytes(blockLength - 2)))
|
||||
using (var inflaterStream = new InflaterInputStream(batch, inflater))
|
||||
{
|
||||
int n;
|
||||
while ((n = inflaterStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
var offset = Math.Max(0, file.DecompressedOffset - decompressedBytes);
|
||||
var count = Math.Min(n - offset, file.DecompressedLength - decompressedBytes);
|
||||
if (offset < n)
|
||||
output.Write(buffer, (int)offset, (int)count);
|
||||
|
||||
decompressedBytes += n;
|
||||
}
|
||||
}
|
||||
|
||||
inflater.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> Contents { get { return files.Select(f => f.FileName); } }
|
||||
}
|
||||
}
|
||||
37
OpenRA.Mods.Common/FileFormats/RLEZerosCompression.cs
Normal file
37
OpenRA.Mods.Common/FileFormats/RLEZerosCompression.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
// Run length encoded sequences of zeros (aka Format2)
|
||||
public static class RLEZerosCompression
|
||||
{
|
||||
public static void DecodeInto(byte[] src, byte[] dest, int destIndex)
|
||||
{
|
||||
var r = new FastByteReader(src);
|
||||
|
||||
while (!r.Done())
|
||||
{
|
||||
var cmd = r.ReadByte();
|
||||
if (cmd == 0)
|
||||
{
|
||||
var count = r.ReadByte();
|
||||
Array.Clear(dest, destIndex, count);
|
||||
destIndex += count;
|
||||
}
|
||||
else
|
||||
dest[destIndex++] = cmd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
305
OpenRA.Mods.Common/FileFormats/WavReader.cs
Normal file
305
OpenRA.Mods.Common/FileFormats/WavReader.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public static class WavReader
|
||||
{
|
||||
enum WaveType : short { Pcm = 0x1, MsAdpcm = 0x2, ImaAdpcm = 0x11 }
|
||||
|
||||
public static bool LoadSound(Stream s, out Func<Stream> result, out short channels, out int sampleBits, out int sampleRate, out float lengthInSeconds)
|
||||
{
|
||||
result = null;
|
||||
channels = -1;
|
||||
sampleBits = -1;
|
||||
sampleRate = -1;
|
||||
lengthInSeconds = -1;
|
||||
|
||||
var type = s.ReadASCII(4);
|
||||
if (type != "RIFF")
|
||||
return false;
|
||||
|
||||
s.ReadInt32(); // File-size
|
||||
var format = s.ReadASCII(4);
|
||||
if (format != "WAVE")
|
||||
return false;
|
||||
|
||||
WaveType audioType = 0;
|
||||
var dataOffset = -1L;
|
||||
var dataSize = -1;
|
||||
var uncompressedSize = -1;
|
||||
short blockAlign = -1;
|
||||
while (s.Position < s.Length)
|
||||
{
|
||||
if ((s.Position & 1) == 1)
|
||||
s.ReadUInt8(); // Alignment
|
||||
|
||||
if (s.Position == s.Length)
|
||||
break; // Break if we aligned with end of stream
|
||||
|
||||
var blockType = s.ReadASCII(4);
|
||||
var chunkSize = s.ReadUInt32();
|
||||
|
||||
switch (blockType)
|
||||
{
|
||||
case "fmt ":
|
||||
var audioFormat = s.ReadInt16();
|
||||
audioType = (WaveType)audioFormat;
|
||||
|
||||
if (!Enum.IsDefined(audioType))
|
||||
throw new NotSupportedException($"Compression type {audioFormat} is not supported.");
|
||||
|
||||
channels = s.ReadInt16();
|
||||
sampleRate = s.ReadInt32();
|
||||
s.ReadInt32(); // Byte Rate
|
||||
blockAlign = s.ReadInt16();
|
||||
sampleBits = s.ReadInt16();
|
||||
lengthInSeconds = (float)(s.Length * 8) / (channels * sampleRate * sampleBits);
|
||||
s.Position += chunkSize - 16; // Ignoring any optional extra params
|
||||
break;
|
||||
case "fact":
|
||||
uncompressedSize = s.ReadInt32();
|
||||
s.Position += chunkSize - 4; // Ignoring other formats than ADPCM, fact chunk not in standard PCM files
|
||||
break;
|
||||
case "data":
|
||||
if (s.Position + chunkSize > s.Length)
|
||||
chunkSize = (uint)(s.Length - s.Position); // Handle defective data chunk size by assuming it's the remainder of the file
|
||||
|
||||
dataOffset = s.Position;
|
||||
dataSize = (int)chunkSize;
|
||||
s.Position += chunkSize;
|
||||
break;
|
||||
case "LIST":
|
||||
case "cue ":
|
||||
default:
|
||||
s.Position += chunkSize; // Ignoring chunks we don't want to/know how to handle
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// sampleBits refers to the output bitrate, which is always 16 for adpcm.
|
||||
if (audioType != WaveType.Pcm)
|
||||
sampleBits = 16;
|
||||
|
||||
if (channels != 1 && channels != 2)
|
||||
throw new NotSupportedException($"Expected 1 or 2 channels only for WAV file, received: {channels}");
|
||||
|
||||
var chan = channels;
|
||||
result = () =>
|
||||
{
|
||||
var audioStream = SegmentStream.CreateWithoutOwningStream(s, dataOffset, dataSize);
|
||||
if (audioType == WaveType.ImaAdpcm)
|
||||
return new WavStreamImaAdpcm(audioStream, dataSize, blockAlign, chan, uncompressedSize);
|
||||
if (audioType == WaveType.MsAdpcm)
|
||||
return new WavStreamMsAdpcm(audioStream, dataSize, blockAlign, chan);
|
||||
|
||||
return audioStream; // Data is already PCM format.
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
sealed class WavStreamImaAdpcm : ReadOnlyAdapterStream
|
||||
{
|
||||
readonly short channels;
|
||||
readonly int numBlocks;
|
||||
readonly int blockDataSize;
|
||||
readonly int outputSize;
|
||||
readonly byte[] blockData;
|
||||
|
||||
int outOffset;
|
||||
int currentBlock;
|
||||
|
||||
public WavStreamImaAdpcm(Stream stream, int dataSize, short blockAlign, short channels, int uncompressedSize)
|
||||
: base(stream)
|
||||
{
|
||||
this.channels = channels;
|
||||
numBlocks = dataSize / blockAlign;
|
||||
blockDataSize = blockAlign - channels * 4;
|
||||
outputSize = uncompressedSize * channels * 2;
|
||||
|
||||
blockData = new byte[blockDataSize];
|
||||
}
|
||||
|
||||
protected override bool BufferData(Stream baseStream, Queue<byte> data)
|
||||
{
|
||||
// Decode each block of IMA ADPCM data
|
||||
// Each block starts with a initial state per-channel
|
||||
Span<int> predictor = stackalloc int[channels];
|
||||
Span<int> index = stackalloc int[channels];
|
||||
|
||||
Span<byte> channelData = stackalloc byte[channels * 4];
|
||||
baseStream.ReadBytes(channelData);
|
||||
var cd = 0;
|
||||
for (var c = 0; c < channels; c++)
|
||||
{
|
||||
predictor[c] = (short)(channelData[cd++] | channelData[cd++] << 8);
|
||||
index[c] = channelData[cd++];
|
||||
cd++; // Unknown/Reserved
|
||||
|
||||
// Output first sample from input
|
||||
data.Enqueue((byte)predictor[c]);
|
||||
data.Enqueue((byte)(predictor[c] >> 8));
|
||||
outOffset += 2;
|
||||
|
||||
if (outOffset >= outputSize)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decode and output remaining data in this block
|
||||
Span<byte> decoded = stackalloc byte[16];
|
||||
Span<byte> interleaveBuffer = stackalloc byte[channels * 16];
|
||||
var blockDataSpan = blockData.AsSpan();
|
||||
baseStream.ReadBytes(blockDataSpan);
|
||||
var blockOffset = 0;
|
||||
while (blockOffset < blockDataSize)
|
||||
{
|
||||
for (var c = 0; c < channels; c++)
|
||||
{
|
||||
// Decode 4 bytes (to 16 bytes of output) per channel
|
||||
ImaAdpcmReader.LoadImaAdpcmSound(blockDataSpan.Slice(blockOffset, 4), ref index[c], ref predictor[c], decoded);
|
||||
|
||||
// Interleave output, one sample per channel
|
||||
var interleaveChannelOffset = 2 * c;
|
||||
for (var i = 0; i < decoded.Length; i += 2)
|
||||
{
|
||||
var interleaveSampleOffset = interleaveChannelOffset + i;
|
||||
interleaveBuffer[interleaveSampleOffset] = decoded[i];
|
||||
interleaveBuffer[interleaveSampleOffset + 1] = decoded[i + 1];
|
||||
interleaveChannelOffset += 2 * (channels - 1);
|
||||
}
|
||||
|
||||
blockOffset += 4;
|
||||
}
|
||||
|
||||
var outputRemaining = outputSize - outOffset;
|
||||
var toCopy = Math.Min(outputRemaining, interleaveBuffer.Length);
|
||||
for (var i = 0; i < toCopy; i++)
|
||||
data.Enqueue(interleaveBuffer[i]);
|
||||
|
||||
outOffset += 16 * channels;
|
||||
|
||||
if (outOffset >= outputSize)
|
||||
return true;
|
||||
}
|
||||
|
||||
return ++currentBlock >= numBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
// Format docs https://wiki.multimedia.cx/index.php/Microsoft_ADPCM
|
||||
public sealed class WavStreamMsAdpcm : ReadOnlyAdapterStream
|
||||
{
|
||||
static readonly int[] AdaptationTable =
|
||||
[
|
||||
230, 230, 230, 230, 307, 409, 512, 614,
|
||||
768, 614, 512, 409, 307, 230, 230, 230
|
||||
];
|
||||
|
||||
static readonly int[] AdaptCoeff1 = [256, 512, 0, 192, 240, 460, 392];
|
||||
|
||||
static readonly int[] AdaptCoeff2 = [0, -256, 0, 64, 0, -208, -232];
|
||||
|
||||
readonly short channels;
|
||||
readonly int blockDataSize;
|
||||
readonly int numBlocks;
|
||||
readonly byte[] blockData;
|
||||
|
||||
int currentBlock;
|
||||
|
||||
public WavStreamMsAdpcm(Stream stream, int dataSize, short blockAlign, short channels)
|
||||
: base(stream)
|
||||
{
|
||||
this.channels = channels;
|
||||
blockDataSize = blockAlign - channels * 7;
|
||||
numBlocks = dataSize / blockAlign;
|
||||
|
||||
blockData = new byte[blockDataSize];
|
||||
}
|
||||
|
||||
protected override bool BufferData(Stream baseStream, Queue<byte> data)
|
||||
{
|
||||
Span<byte> bpred = stackalloc byte[channels];
|
||||
Span<short> chanIdelta = stackalloc short[channels];
|
||||
|
||||
Span<short> s1 = stackalloc short[channels];
|
||||
Span<short> s2 = stackalloc short[channels];
|
||||
|
||||
baseStream.ReadBytes(bpred);
|
||||
baseStream.ReadBytes(MemoryMarshal.Cast<short, byte>(chanIdelta));
|
||||
baseStream.ReadBytes(MemoryMarshal.Cast<short, byte>(s1));
|
||||
baseStream.ReadBytes(MemoryMarshal.Cast<short, byte>(s2));
|
||||
|
||||
for (var c = 0; c < channels; c++)
|
||||
s2[c] = WriteSample(s2[c], data);
|
||||
|
||||
for (var c = 0; c < channels; c++)
|
||||
WriteSample(s1[c], data);
|
||||
|
||||
var channelNumber = channels > 1 ? 1 : 0;
|
||||
|
||||
baseStream.ReadBytes(blockData);
|
||||
for (var blockindx = 0; blockindx < blockDataSize; blockindx++)
|
||||
{
|
||||
var bytecode = blockData[blockindx];
|
||||
|
||||
// Decode the first nibble, this is always left channel
|
||||
WriteSample(DecodeNibble((short)((bytecode >> 4) & 0x0F), bpred[0], ref chanIdelta[0], ref s1[0], ref s2[0]), data);
|
||||
|
||||
// Decode the second nibble, for stereo this will be the right channel
|
||||
WriteSample(
|
||||
DecodeNibble(
|
||||
(short)(bytecode & 0x0F),
|
||||
bpred[channelNumber],
|
||||
ref chanIdelta[channelNumber],
|
||||
ref s1[channelNumber],
|
||||
ref s2[channelNumber]),
|
||||
data);
|
||||
}
|
||||
|
||||
return ++currentBlock >= numBlocks;
|
||||
}
|
||||
|
||||
static short WriteSample(short t, Queue<byte> data)
|
||||
{
|
||||
data.Enqueue((byte)t);
|
||||
data.Enqueue((byte)(t >> 8));
|
||||
return t;
|
||||
}
|
||||
|
||||
// This code contains elements from libsndfile
|
||||
static short DecodeNibble(short nibble, byte bpred, ref short idelta, ref short s1, ref short s2)
|
||||
{
|
||||
var predict = (s1 * AdaptCoeff1[bpred] + s2 * AdaptCoeff2[bpred]) >> 8;
|
||||
|
||||
var twosCompliment = (nibble & 0x8) > 0
|
||||
? nibble - 0x10
|
||||
: nibble;
|
||||
|
||||
s2 = s1;
|
||||
s1 = (short)(twosCompliment * idelta + predict).Clamp(-32768, 32767);
|
||||
|
||||
// Compute next Adaptive Scale Factor (ASF), saturating to lower bound of 16
|
||||
idelta = (short)((AdaptationTable[nibble] * idelta) >> 8);
|
||||
if (idelta < 16)
|
||||
idelta = 16;
|
||||
|
||||
return s1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
OpenRA.Mods.Common/FileFormats/WestwoodCompressedReader.cs
Normal file
86
OpenRA.Mods.Common/FileFormats/WestwoodCompressedReader.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
#region Copyright & License Information
|
||||
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileFormats
|
||||
{
|
||||
public static class WestwoodCompressedReader
|
||||
{
|
||||
static readonly int[] AudWsStepTable2 = [-2, -1, 0, 1];
|
||||
static readonly int[] AudWsStepTable4 = [-9, -8, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 8];
|
||||
|
||||
public static void DecodeWestwoodCompressedSample(ReadOnlySpan<byte> input, Span<byte> output)
|
||||
{
|
||||
if (input.Length == output.Length)
|
||||
{
|
||||
input.CopyTo(output);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var sample = 0x80;
|
||||
var r = 0;
|
||||
var w = 0;
|
||||
|
||||
while (r < input.Length)
|
||||
{
|
||||
var count = input[r++] & 0x3f;
|
||||
|
||||
switch (input[r - 1] >> 6)
|
||||
{
|
||||
case 0:
|
||||
for (count++; count > 0; count--)
|
||||
{
|
||||
var code = input[r++];
|
||||
output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 0) & 0x03]).Clamp(byte.MinValue, byte.MaxValue));
|
||||
output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 2) & 0x03]).Clamp(byte.MinValue, byte.MaxValue));
|
||||
output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 4) & 0x03]).Clamp(byte.MinValue, byte.MaxValue));
|
||||
output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 6) & 0x03]).Clamp(byte.MinValue, byte.MaxValue));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 1:
|
||||
for (count++; count > 0; count--)
|
||||
{
|
||||
var code = input[r++];
|
||||
output[w++] = (byte)(sample = (sample + AudWsStepTable4[(code >> 0) & 0x0f]).Clamp(byte.MinValue, byte.MaxValue));
|
||||
output[w++] = (byte)(sample = (sample + AudWsStepTable4[(code >> 4) & 0xff]).Clamp(byte.MinValue, byte.MaxValue));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 2 when (count & 0x20) != 0:
|
||||
output[w++] = (byte)(sample += (sbyte)((sbyte)count << 3) >> 3);
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
for (count++; count > 0; count--)
|
||||
output[w++] = input[r++];
|
||||
|
||||
sample = input[r - 1];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
for (count++; count > 0; count--)
|
||||
output[w++] = (byte)sample;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user