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:
124
OpenRA.Mods.Cnc/FileSystem/BigFile.cs
Normal file
124
OpenRA.Mods.Cnc/FileSystem/BigFile.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
#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 OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
using FS = OpenRA.FileSystem.FileSystem;
|
||||
|
||||
namespace OpenRA.Mods.Cnc.FileSystem
|
||||
{
|
||||
public class BigLoader : IPackageLoader
|
||||
{
|
||||
sealed class BigFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; }
|
||||
public IEnumerable<string> Contents => index.Keys;
|
||||
|
||||
readonly Dictionary<string, Entry> index;
|
||||
readonly Stream s;
|
||||
|
||||
public BigFile(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
try
|
||||
{
|
||||
s.ReadASCII(4); // signature
|
||||
|
||||
// Total archive size.
|
||||
s.ReadUInt32();
|
||||
|
||||
var entryCount = s.ReadUInt32();
|
||||
if (BitConverter.IsLittleEndian)
|
||||
entryCount = int2.Swap(entryCount);
|
||||
|
||||
// First entry offset? This is apparently bogus for EA's .big files
|
||||
// and we don't have to try seeking there since the entries typically start next in EA's .big files.
|
||||
s.ReadUInt32();
|
||||
|
||||
index = new Dictionary<string, Entry>((int)entryCount);
|
||||
for (var i = 0; i < entryCount; i++)
|
||||
{
|
||||
var entry = new Entry(s);
|
||||
index.Add(entry.Path, entry);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Entry
|
||||
{
|
||||
public readonly uint Offset;
|
||||
public readonly uint Size;
|
||||
public readonly string Path;
|
||||
|
||||
public Entry(Stream s)
|
||||
{
|
||||
Offset = s.ReadUInt32();
|
||||
Size = s.ReadUInt32();
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Offset = int2.Swap(Offset);
|
||||
Size = int2.Swap(Size);
|
||||
}
|
||||
|
||||
Path = s.ReadASCIIZ();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
var entry = index[filename];
|
||||
return SegmentStream.CreateWithoutOwningStream(s, entry.Offset, (int)entry.Size);
|
||||
}
|
||||
|
||||
public bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FS context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FS context, out IReadOnlyPackage package)
|
||||
{
|
||||
// Take a peek at the file signature
|
||||
var signature = s.ReadASCII(4);
|
||||
s.Position -= 4;
|
||||
|
||||
if (signature != "BIGF")
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new BigFile(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
141
OpenRA.Mods.Cnc/FileSystem/MegFile.cs
Normal file
141
OpenRA.Mods.Cnc/FileSystem/MegFile.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
#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 OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Cnc.FileSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// This class supports loading unencrypted V3 .meg files using
|
||||
/// reference documentation from here https://modtools.petrolution.net/docs/MegFileFormat.
|
||||
/// </summary>
|
||||
public class MegV3Loader : IPackageLoader
|
||||
{
|
||||
const uint UnencryptedMegID = 0xFFFFFFFF;
|
||||
|
||||
// Float value 0.99, but it is simpler to read and compare as an integer
|
||||
const uint MegVersion = 0x3F7D70A4;
|
||||
|
||||
public bool TryParsePackage(Stream s, string filename, OpenRA.FileSystem.FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
var position = s.Position;
|
||||
|
||||
var id = s.ReadUInt32();
|
||||
var version = s.ReadUInt32();
|
||||
|
||||
s.Position = position;
|
||||
|
||||
if (id != UnencryptedMegID || version != MegVersion)
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new MegFile(s, filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
public sealed class MegFile : IReadOnlyPackage
|
||||
{
|
||||
readonly Stream s;
|
||||
|
||||
readonly Dictionary<string, (uint Offset, int Length)> contents;
|
||||
|
||||
public MegFile(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
var id = s.ReadUInt32();
|
||||
var version = s.ReadUInt32();
|
||||
|
||||
if (id != UnencryptedMegID || version != MegVersion)
|
||||
throw new Exception("Invalid file signature for meg file");
|
||||
|
||||
var headerSize = s.ReadUInt32();
|
||||
var numStrings = s.ReadUInt32();
|
||||
var numFiles = s.ReadUInt32();
|
||||
var stringsSize = s.ReadUInt32();
|
||||
var stringsStart = s.Position;
|
||||
|
||||
var filenames = new List<string>();
|
||||
|
||||
// The file names are an indexed array of strings
|
||||
for (var i = 0; i < numStrings; i++)
|
||||
{
|
||||
var length = s.ReadUInt16();
|
||||
filenames.Add(s.ReadASCII(length));
|
||||
}
|
||||
|
||||
// The header indicates where we should be, so verify it
|
||||
if (s.Position != stringsSize + stringsStart)
|
||||
throw new Exception("File name table in .meg file inconsistent");
|
||||
|
||||
// Now we load each file entry and associated info
|
||||
contents = new Dictionary<string, (uint Offset, int Length)>((int)numFiles);
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
// Ignore flags, crc, index
|
||||
s.Position += 10;
|
||||
var size = s.ReadUInt32();
|
||||
var offset = s.ReadUInt32();
|
||||
var nameIndex = s.ReadUInt16();
|
||||
contents[filenames[nameIndex]] = (offset, (int)size);
|
||||
}
|
||||
|
||||
contents.TrimExcess();
|
||||
|
||||
if (s.Position != headerSize)
|
||||
throw new Exception("Expected to be at data start offset");
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public IEnumerable<string> Contents => contents.Keys;
|
||||
|
||||
public bool Contains(string filename)
|
||||
{
|
||||
return contents.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
// Look up the index of the filename
|
||||
if (!contents.TryGetValue(filename, out var index))
|
||||
return null;
|
||||
|
||||
return SegmentStream.CreateWithoutOwningStream(s, index.Offset, index.Length);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, OpenRA.FileSystem.FileSystem context)
|
||||
{
|
||||
var childStream = GetStream(filename);
|
||||
if (childStream == null)
|
||||
return null;
|
||||
|
||||
if (context.TryParsePackage(childStream, filename, out var package))
|
||||
return package;
|
||||
|
||||
childStream.Dispose();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
248
OpenRA.Mods.Cnc/FileSystem/MixFile.cs
Normal file
248
OpenRA.Mods.Cnc/FileSystem/MixFile.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
#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 OpenRA.FileSystem;
|
||||
using OpenRA.Mods.Cnc.FileFormats;
|
||||
using OpenRA.Primitives;
|
||||
using FS = OpenRA.FileSystem.FileSystem;
|
||||
|
||||
namespace OpenRA.Mods.Cnc.FileSystem
|
||||
{
|
||||
public class MixLoader : IPackageLoader
|
||||
{
|
||||
public sealed class MixFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; }
|
||||
public IEnumerable<string> Contents => index.Keys;
|
||||
|
||||
readonly Dictionary<string, PackageEntry> index;
|
||||
readonly long dataStart;
|
||||
readonly Stream s;
|
||||
|
||||
public MixFile(Stream s, string filename, string[] globalFilenames)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
try
|
||||
{
|
||||
// Detect format type
|
||||
var isCncMix = s.ReadUInt16() != 0;
|
||||
|
||||
// The C&C mix format doesn't contain any flags or encryption
|
||||
var isEncrypted = false;
|
||||
if (!isCncMix)
|
||||
isEncrypted = (s.ReadUInt16() & 0x2) != 0;
|
||||
|
||||
List<PackageEntry> entries;
|
||||
if (isEncrypted)
|
||||
entries = ParseHeader(DecryptHeader(s, 4, out dataStart), 0, out _);
|
||||
else
|
||||
entries = ParseHeader(s, isCncMix ? 0 : 4, out dataStart);
|
||||
|
||||
index = ParseIndex(entries.ToDictionaryWithConflictLog(x => x.Hash,
|
||||
$"{filename} ({(isCncMix ? "C&C" : "RA/TS/RA2")} format, Encrypted: {isEncrypted}, DataStart: {dataStart})",
|
||||
null, x => $"(offs={x.Offset}, len={x.Length})"), globalFilenames);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, PackageEntry> ParseIndex(Dictionary<uint, PackageEntry> entries, string[] globalFilenames)
|
||||
{
|
||||
var allPossibleFilenames = new HashSet<string>(globalFilenames);
|
||||
|
||||
// Try and find a local mix database
|
||||
var dbNameClassic = PackageEntry.HashFilename("local mix database.dat", PackageHashType.Classic);
|
||||
var dbNameCRC = PackageEntry.HashFilename("local mix database.dat", PackageHashType.CRC32);
|
||||
foreach (var kv in entries)
|
||||
{
|
||||
if (kv.Key == dbNameClassic || kv.Key == dbNameCRC)
|
||||
{
|
||||
using (var content = GetContent(kv.Value))
|
||||
{
|
||||
var db = new XccLocalDatabase(content);
|
||||
allPossibleFilenames.EnsureCapacity(allPossibleFilenames.Count + db.Entries.Length);
|
||||
allPossibleFilenames.UnionWith(db.Entries);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var classicIndex = new Dictionary<string, PackageEntry>(entries.Count);
|
||||
var crcIndex = new Dictionary<string, PackageEntry>(entries.Count);
|
||||
|
||||
foreach (var filename in allPossibleFilenames)
|
||||
{
|
||||
var classicHash = PackageEntry.HashFilename(filename, PackageHashType.Classic);
|
||||
var crcHash = PackageEntry.HashFilename(filename, PackageHashType.CRC32);
|
||||
|
||||
if (entries.TryGetValue(classicHash, out var e))
|
||||
classicIndex.Add(filename, e);
|
||||
|
||||
if (entries.TryGetValue(crcHash, out e))
|
||||
crcIndex.Add(filename, e);
|
||||
}
|
||||
|
||||
var bestIndex = crcIndex.Count > classicIndex.Count ? crcIndex : classicIndex;
|
||||
|
||||
var unknown = entries.Count - bestIndex.Count;
|
||||
if (unknown > 0)
|
||||
Log.Write("debug", $"{Name}: failed to resolve filenames for {unknown} unknown hashes");
|
||||
|
||||
bestIndex.TrimExcess();
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
static List<PackageEntry> ParseHeader(Stream s, long offset, out long headerEnd)
|
||||
{
|
||||
s.Seek(offset, SeekOrigin.Begin);
|
||||
var numFiles = s.ReadUInt16();
|
||||
s.ReadUInt32(); // dataSize
|
||||
|
||||
var items = new List<PackageEntry>();
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
items.Add(new PackageEntry(s));
|
||||
|
||||
headerEnd = offset + 6 + numFiles * PackageEntry.Size;
|
||||
return items;
|
||||
}
|
||||
|
||||
static MemoryStream DecryptHeader(Stream s, long offset, out long headerEnd)
|
||||
{
|
||||
s.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// Decrypt blowfish key
|
||||
var keyblock = s.ReadBytes(80);
|
||||
var blowfishKey = new BlowfishKeyProvider().DecryptKey(keyblock);
|
||||
var fish = new Blowfish(blowfishKey);
|
||||
|
||||
// Decrypt first block to work out the header length
|
||||
var ms = Decrypt(ReadBlocks(s, offset + 80, 1), fish);
|
||||
var numFiles = ms.ReadUInt16();
|
||||
|
||||
// Decrypt the full header - round bytes up to a full block
|
||||
var blockCount = (13 + numFiles * PackageEntry.Size) / 8;
|
||||
headerEnd = offset + 80 + blockCount * 8;
|
||||
|
||||
return Decrypt(ReadBlocks(s, offset + 80, blockCount), fish);
|
||||
}
|
||||
|
||||
static MemoryStream Decrypt(uint[] h, Blowfish fish)
|
||||
{
|
||||
var decrypted = fish.Decrypt(h);
|
||||
|
||||
var ms = new MemoryStream(decrypted.Length * 4);
|
||||
var writer = new BinaryWriter(ms);
|
||||
foreach (var t in decrypted)
|
||||
writer.Write(t);
|
||||
writer.Flush();
|
||||
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
return ms;
|
||||
}
|
||||
|
||||
static uint[] ReadBlocks(Stream s, long offset, int count)
|
||||
{
|
||||
if (offset < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), "Non-negative number required.");
|
||||
|
||||
if (count < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), "Non-negative number required.");
|
||||
|
||||
if (offset + count * 2 > s.Length)
|
||||
throw new ArgumentException($"Bytes to read {count * 2} and offset {offset} greater than stream length {s.Length}.");
|
||||
|
||||
s.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
// A block is a single encryption unit (represented as two 32-bit integers)
|
||||
var ret = new uint[2 * count];
|
||||
for (var i = 0; i < ret.Length; i++)
|
||||
ret[i] = s.ReadUInt32();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Stream GetContent(PackageEntry entry)
|
||||
{
|
||||
return SegmentStream.CreateWithoutOwningStream(s, dataStart + entry.Offset, (int)entry.Length);
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
if (!index.TryGetValue(filename, out var e))
|
||||
return null;
|
||||
|
||||
return GetContent(e);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, PackageEntry> Index
|
||||
{
|
||||
get
|
||||
{
|
||||
var absoluteIndex = index.ToDictionary(e => e.Key, e => new PackageEntry(e.Value.Hash, (uint)(e.Value.Offset + dataStart), e.Value.Length));
|
||||
return new ReadOnlyDictionary<string, PackageEntry>(absoluteIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FS context)
|
||||
{
|
||||
var childStream = GetStream(filename);
|
||||
if (childStream == null)
|
||||
return null;
|
||||
|
||||
if (context.TryParsePackage(childStream, filename, out var package))
|
||||
return package;
|
||||
|
||||
childStream.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
string[] globalFilenames;
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FS context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the global mix database
|
||||
if (globalFilenames == null && context.TryOpen("global mix database.dat", out var mixDatabase))
|
||||
using (var db = new XccGlobalDatabase(mixDatabase))
|
||||
globalFilenames = db.Entries.ToHashSet().ToArray();
|
||||
|
||||
package = new MixFile(s, filename, globalFilenames ?? []);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
118
OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs
Normal file
118
OpenRA.Mods.Cnc/FileSystem/PackageEntry.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
#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 System.Text;
|
||||
using OpenRA.Mods.Cnc.FileFormats;
|
||||
|
||||
namespace OpenRA.Mods.Cnc.FileSystem
|
||||
{
|
||||
public enum PackageHashType { Classic, CRC32 }
|
||||
|
||||
public class PackageEntry
|
||||
{
|
||||
public const int Size = 12;
|
||||
public readonly uint Hash;
|
||||
public readonly uint Offset;
|
||||
public readonly uint Length;
|
||||
|
||||
public PackageEntry(uint hash, uint offset, uint length)
|
||||
{
|
||||
Hash = hash;
|
||||
Offset = offset;
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public PackageEntry(Stream s)
|
||||
{
|
||||
Hash = s.ReadUInt32();
|
||||
Offset = s.ReadUInt32();
|
||||
Length = s.ReadUInt32();
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter w)
|
||||
{
|
||||
w.Write(Hash);
|
||||
w.Write(Offset);
|
||||
w.Write(Length);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Names.TryGetValue(Hash, out var filename))
|
||||
return $"{filename} - offset 0x{Offset:x8} - length 0x{Length:x8}";
|
||||
else
|
||||
return $"0x{Hash:x8} - offset 0x{Offset:x8} - length 0x{Length:x8}";
|
||||
}
|
||||
|
||||
public static uint HashFilename(string name, PackageHashType type)
|
||||
{
|
||||
var padding = name.Length % 4 != 0 ? 4 - name.Length % 4 : 0;
|
||||
var paddedLength = name.Length + padding;
|
||||
|
||||
// Avoid stack overflows by only allocating small buffers on the stack, and larger ones on the heap.
|
||||
// 64 chars covers most real filenames.
|
||||
var upperPaddedName = paddedLength < 64 ? stackalloc char[paddedLength] : new char[paddedLength];
|
||||
name.AsSpan().ToUpperInvariant(upperPaddedName);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case PackageHashType.Classic:
|
||||
{
|
||||
for (var p = 0; p < padding; p++)
|
||||
upperPaddedName[paddedLength - 1 - p] = '\0';
|
||||
|
||||
var asciiBytes = paddedLength < 64 ? stackalloc byte[paddedLength] : new byte[paddedLength];
|
||||
Encoding.ASCII.GetBytes(upperPaddedName, asciiBytes);
|
||||
|
||||
var data = MemoryMarshal.Cast<byte, uint>(asciiBytes);
|
||||
var result = 0u;
|
||||
foreach (var next in data)
|
||||
result = ((result << 1) | (result >> 31)) + next;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case PackageHashType.CRC32:
|
||||
{
|
||||
var length = name.Length;
|
||||
var lengthRoundedDownToFour = length / 4 * 4;
|
||||
if (length != lengthRoundedDownToFour)
|
||||
{
|
||||
upperPaddedName[length] = (char)(length - lengthRoundedDownToFour);
|
||||
for (var p = 1; p < padding; p++)
|
||||
upperPaddedName[length + p] = upperPaddedName[lengthRoundedDownToFour];
|
||||
}
|
||||
|
||||
var asciiBytes = paddedLength < 64 ? stackalloc byte[paddedLength] : new byte[paddedLength];
|
||||
Encoding.ASCII.GetBytes(upperPaddedName, asciiBytes);
|
||||
|
||||
return CRC32.Calculate(asciiBytes);
|
||||
}
|
||||
|
||||
default: throw new NotImplementedException($"Unknown hash type `{type}`");
|
||||
}
|
||||
}
|
||||
|
||||
static readonly Dictionary<uint, string> Names = [];
|
||||
|
||||
public static void AddStandardName(string s)
|
||||
{
|
||||
var hash = HashFilename(s, PackageHashType.Classic); // RA1 and TD
|
||||
Names.Add(hash, s);
|
||||
var crcHash = HashFilename(s, PackageHashType.CRC32); // TS
|
||||
Names.Add(crcHash, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
OpenRA.Mods.Cnc/FileSystem/Pak.cs
Normal file
103
OpenRA.Mods.Cnc/FileSystem/Pak.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
#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 OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
using FS = OpenRA.FileSystem.FileSystem;
|
||||
|
||||
namespace OpenRA.Mods.Cnc.FileSystem
|
||||
{
|
||||
public class PakFileLoader : IPackageLoader
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
public uint Offset;
|
||||
public uint Length;
|
||||
public string Filename;
|
||||
}
|
||||
|
||||
sealed class PakFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; }
|
||||
public IEnumerable<string> Contents => index.Keys;
|
||||
|
||||
readonly Dictionary<string, Entry> index = [];
|
||||
readonly Stream stream;
|
||||
|
||||
public PakFile(Stream stream, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.stream = stream;
|
||||
|
||||
try
|
||||
{
|
||||
var offset = stream.ReadUInt32();
|
||||
while (offset != 0)
|
||||
{
|
||||
var file = stream.ReadASCIIZ();
|
||||
var next = stream.ReadUInt32();
|
||||
var length = (next == 0 ? (uint)stream.Length : next) - offset;
|
||||
|
||||
// Ignore duplicate files
|
||||
if (index.TryAdd(file, new Entry { Offset = offset, Length = length, Filename = file }))
|
||||
offset = next;
|
||||
}
|
||||
|
||||
index.TrimExcess();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
if (!index.TryGetValue(filename, out var entry))
|
||||
return null;
|
||||
|
||||
return SegmentStream.CreateWithoutOwningStream(stream, entry.Offset, (int)entry.Length);
|
||||
}
|
||||
|
||||
public bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FS context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FS context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!filename.EndsWith(".pak", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new PakFile(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user