Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

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:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View 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;
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}