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,304 @@
#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 OpenRA.Primitives;
namespace OpenRA.FileSystem
{
public interface IReadOnlyFileSystem
{
Stream Open(string filename);
bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename);
bool TryOpen(string filename, out Stream s);
bool Exists(string filename);
bool IsExternalFile(string filename);
}
public class FileSystem : IReadOnlyFileSystem
{
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = [];
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = [];
readonly string modID;
// Mod packages that should not be disposed
readonly List<IReadOnlyPackage> modPackages = [];
readonly IReadOnlyDictionary<string, Manifest> installedMods;
readonly IPackageLoader[] packageLoaders;
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => []);
public FileSystem(string modID, IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
{
this.modID = modID;
this.installedMods = installedMods;
this.packageLoaders = packageLoaders
.Append(new ZipFileLoader())
.ToArray();
}
public bool TryParsePackage(Stream stream, string filename, out IReadOnlyPackage package)
{
package = null;
foreach (var packageLoader in packageLoaders)
if (packageLoader.TryParsePackage(stream, filename, this, out package))
return true;
return false;
}
public IReadOnlyPackage OpenPackage(string filename)
{
// Raw directories are the easiest and one of the most common cases, so try these first
var resolvedPath = Platform.ResolvePath(filename);
if (!resolvedPath.Contains('|') && Directory.Exists(resolvedPath))
return new Folder(resolvedPath);
// Children of another package require special handling
if (TryGetPackageContaining(filename, out var parent, out var subPath))
return parent.OpenPackage(subPath, this);
// Try and open it normally
var stream = Open(filename);
if (TryParsePackage(stream, filename, out var package))
return package;
// No package loaders took ownership of the stream, so clean it up
stream.Dispose();
return null;
}
public void Mount(string name, string explicitName = null)
{
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
try
{
IReadOnlyPackage package;
if (name.StartsWith('$'))
{
name = name[1..];
if (!installedMods.TryGetValue(name, out var mod))
throw new InvalidOperationException($"Could not load mod '{name}'. Available mods: {installedMods.Keys.JoinWith(", ")}");
package = mod.Package;
modPackages.Add(package);
}
else
{
package = OpenPackage(name);
if (package == null)
throw new InvalidOperationException($"Could not open package '{name}', file not found or its format is not supported.");
}
Mount(package, explicitName);
}
catch when (optional)
{
}
}
public void Mount(IReadOnlyPackage package, string explicitName = null)
{
if (mountedPackages.TryGetValue(package, out var mountCount))
{
// Package is already mounted
// Increment the mount count and bump up the file loading priority
mountedPackages[package] = mountCount + 1;
foreach (var filename in package.Contents)
{
fileIndex[filename].Remove(package);
fileIndex[filename].Add(package);
}
}
else
{
// Mounting the package for the first time
mountedPackages.Add(package, 1);
if (explicitName != null)
explicitMounts.Add(explicitName, package);
foreach (var filename in package.Contents)
fileIndex[filename].Add(package);
}
}
public bool Unmount(IReadOnlyPackage package)
{
if (!mountedPackages.TryGetValue(package, out var mountCount))
return false;
if (--mountCount <= 0)
{
foreach (var packagesForFile in fileIndex.Values)
packagesForFile.RemoveAll(p => p == package);
mountedPackages.Remove(package);
var explicitKeys = explicitMounts.Where(kv => kv.Value == package)
.Select(kv => kv.Key)
.ToList();
foreach (var key in explicitKeys)
explicitMounts.Remove(key);
// Mod packages aren't owned by us, so we shouldn't dispose them
if (!modPackages.Remove(package))
package.Dispose();
}
else
mountedPackages[package] = mountCount;
return true;
}
public void UnmountAll()
{
foreach (var package in mountedPackages.Keys)
if (!modPackages.Contains(package))
package.Dispose();
mountedPackages.Clear();
explicitMounts.Clear();
modPackages.Clear();
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => []);
}
public void TrimExcess()
{
mountedPackages.TrimExcess();
explicitMounts.TrimExcess();
modPackages.TrimExcess();
foreach (var packages in fileIndex.Values)
packages.TrimExcess();
}
Stream GetFromCache(string filename)
{
var package = fileIndex[filename]
.LastOrDefault(x => x.Contains(filename));
return package?.GetStream(filename);
}
public Stream Open(string filename)
{
if (!TryOpen(filename, out var s))
throw new FileNotFoundException($"File not found: {filename}", filename);
return s;
}
public bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename)
{
var explicitSplit = path.IndexOf('|');
if (explicitSplit > 0 && explicitMounts.TryGetValue(path[..explicitSplit], out package))
{
filename = path[(explicitSplit + 1)..];
return true;
}
package = fileIndex[path].LastOrDefault(x => x.Contains(path));
filename = path;
return package != null;
}
public bool TryOpen(string filename, out Stream s)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
}
s = GetFromCache(filename);
if (s != null)
return true;
// The file should be in an explicit package (but we couldn't find it)
// Thus don't try to find it using the filename (which contains the invalid '|' char)
// This can be removed once the TODO below is resolved
if (explicitSplit > 0)
return false;
// Ask each package individually
// TODO: This fallback can be removed once the filesystem cleanups are complete
var package = mountedPackages.Keys.LastOrDefault(x => x.Contains(filename));
if (package != null)
{
s = package.GetStream(filename);
return s != null;
}
s = null;
return false;
}
public bool Exists(string filename)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0 &&
explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage) &&
explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
return fileIndex.ContainsKey(filename);
}
/// <summary>
/// Returns true if the given filename references any file outside the mod mount.
/// </summary>
public bool IsExternalFile(string filename)
{
return !filename.StartsWith($"{modID}|", StringComparison.Ordinal);
}
public static string ResolveCaseInsensitivePath(string path)
{
var resolved = Path.GetPathRoot(path);
if (resolved == null)
return null;
foreach (var name in path[resolved.Length..].Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
{
// Filter out paths of the form /foo/bar/./baz
if (name == ".")
continue;
resolved = Directory.GetFileSystemEntries(resolved)
.FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
if (resolved == null)
return null;
}
return resolved;
}
public string GetPrefix(IReadOnlyPackage package)
{
return explicitMounts.ContainsValue(package) ? explicitMounts.First(f => f.Value == package).Key : null;
}
}
}

View File

@@ -0,0 +1,110 @@
#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;
namespace OpenRA.FileSystem
{
public sealed class Folder : IReadWritePackage
{
public string Name { get; }
public Folder(string path)
{
Name = path;
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
public IEnumerable<string> Contents
{
get
{
// Order may vary on different file systems and it matters for hashing.
return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly)
.Concat(Directory.GetDirectories(Name))
.Select(Path.GetFileName)
.Order();
}
}
public Stream GetStream(string filename)
{
var combined = Path.Combine(Name, filename);
if (!File.Exists(combined))
return null;
try { return File.OpenRead(combined); }
catch { return null; }
}
public bool Contains(string filename)
{
var combined = Path.Combine(Name, filename);
return combined.StartsWith(Name, StringComparison.Ordinal) && File.Exists(combined);
}
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
{
var resolvedPath = Platform.ResolvePath(Path.Combine(Name, filename));
if (Directory.Exists(resolvedPath))
return new Folder(resolvedPath);
// Zip files loaded from Folders (and *only* from Folders) can be read-write
if (ZipFileLoader.TryParseReadWritePackage(resolvedPath, out var readWritePackage))
return readWritePackage;
// Other package types can be loaded normally
var s = GetStream(filename);
if (s == null)
return null;
if (context.TryParsePackage(s, filename, out var package))
return package;
s.Dispose();
return null;
}
public void Update(string filename, byte[] contents)
{
// HACK: ZipFiles can't be loaded as read-write from a stream, so we are
// forced to bypass the parent package and load them with their full path
// in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages.
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
using (var s = File.Create(filePath))
s.Write(contents, 0, contents.Length);
}
public void Delete(string filename)
{
// HACK: ZipFiles can't be loaded as read-write from a stream, so we are
// forced to bypass the parent package and load them with their full path
// in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages.
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
if (Directory.Exists(filePath))
Directory.Delete(filePath, true);
else if (File.Exists(filePath))
File.Delete(filePath);
}
public void Dispose() { }
}
}

View File

@@ -0,0 +1,42 @@
#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;
namespace OpenRA.FileSystem
{
public interface IPackageLoader
{
/// <summary>
/// Attempt to parse a stream as this type of package.
/// If successful, the loader is expected to take ownership of `s` and dispose it once done.
/// If unsuccessful, the loader is expected to return the stream position to where it started.
/// </summary>
bool TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package);
}
public interface IReadOnlyPackage : IDisposable
{
string Name { get; }
IEnumerable<string> Contents { get; }
Stream GetStream(string filename);
bool Contains(string filename);
IReadOnlyPackage OpenPackage(string filename, FileSystem context);
}
public interface IReadWritePackage : IReadOnlyPackage
{
void Update(string filename, byte[] contents);
void Delete(string filename);
}
}

View File

@@ -0,0 +1,262 @@
#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;
namespace OpenRA.FileSystem
{
public class ZipFileLoader : IPackageLoader
{
const uint ZipSignature = 0x04034b50;
public class ReadOnlyZipFile : IReadOnlyPackage
{
public string Name { get; protected set; }
protected ZipFile pkg;
// Dummy constructor for use with ReadWriteZipFile
protected ReadOnlyZipFile() { }
public ReadOnlyZipFile(Stream s, string filename)
{
Name = filename;
pkg = new ZipFile(s);
}
public Stream GetStream(string filename)
{
var entry = pkg.GetEntry(filename);
if (entry == null)
return null;
using (var z = pkg.GetInputStream(entry))
{
var ms = new MemoryStream((int)entry.Size);
z.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return ms;
}
}
public IEnumerable<string> Contents
{
get
{
foreach (ZipEntry entry in pkg)
if (entry.IsFile)
yield return entry.Name;
}
}
public bool Contains(string filename)
{
return pkg.GetEntry(filename) != null;
}
public void Dispose()
{
pkg?.Close();
GC.SuppressFinalize(this);
}
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
{
// Directories are stored with a trailing "/" in the index
var entry = pkg.GetEntry(filename) ?? pkg.GetEntry(filename + "/");
if (entry == null)
return null;
if (entry.IsDirectory)
return new ZipFolder(this, filename);
// Other package types can be loaded normally
var s = GetStream(filename);
if (s == null)
return null;
if (context.TryParsePackage(s, filename, out var package))
return package;
s.Dispose();
return null;
}
}
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
{
readonly MemoryStream pkgStream = new();
public ReadWriteZipFile(string filename = null, bool create = false)
{
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
// We can work around this by creating a clean in-memory-only file, cutting all outside references
if (!string.IsNullOrEmpty(filename) && !create)
{
using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
{
pkgStream.Capacity = (int)copy.Length;
copy.CopyTo(pkgStream);
}
}
pkgStream.Position = 0;
pkg = new ZipFile(pkgStream);
Name = filename;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
entry.ExtraData = null;
}
public ReadWriteZipFile(byte[] data)
{
using (var copy = new MemoryStream(data))
{
pkgStream.Capacity = (int)copy.Length;
copy.CopyTo(pkgStream);
}
pkgStream.Position = 0;
pkg = new ZipFile(pkgStream);
Name = null;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
entry.ExtraData = null;
}
void Commit()
{
if (!string.IsNullOrEmpty(Name))
File.WriteAllBytes(Name, pkgStream.ToArray());
}
public void Update(string filename, byte[] contents)
{
pkg.BeginUpdate();
pkg.Add(new StaticStreamDataSource(new MemoryStream(contents)), filename);
pkg.CommitUpdate();
Commit();
}
public void Delete(string filename)
{
pkg.BeginUpdate();
pkg.Delete(filename);
pkg.CommitUpdate();
Commit();
}
}
sealed class ZipFolder : IReadOnlyPackage
{
public string Name { get; }
public ReadOnlyZipFile Parent { get; }
public ZipFolder(ReadOnlyZipFile parent, string path)
{
if (path.EndsWith('/'))
path = path[..^1];
Name = path;
Parent = parent;
}
public Stream GetStream(string filename)
{
// Zip files use '/' as a path separator
return Parent.GetStream(Name + '/' + filename);
}
public IEnumerable<string> Contents
{
get
{
foreach (var entry in Parent.Contents)
{
if (entry.StartsWith(Name, StringComparison.Ordinal) && entry != Name)
{
var filename = entry[(Name.Length + 1)..];
var dirLevels = filename.Split('/').Count(c => !string.IsNullOrEmpty(c));
if (dirLevels == 1)
yield return filename;
}
}
}
}
public bool Contains(string filename)
{
return Parent.Contains(Name + '/' + filename);
}
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
{
return Parent.OpenPackage(Name + '/' + filename, context);
}
public void Dispose() { /* nothing to do */ }
}
sealed class StaticStreamDataSource : IStaticDataSource
{
readonly Stream s;
public StaticStreamDataSource(Stream s)
{
this.s = s;
}
public Stream GetSource()
{
return s;
}
}
public bool TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
{
var readSignature = s.ReadUInt32();
s.Position -= 4;
if (readSignature != ZipSignature)
{
package = null;
return false;
}
package = new ReadOnlyZipFile(s, filename);
return true;
}
public static bool TryParseReadWritePackage(string filename, out IReadWritePackage package)
{
using (var s = File.OpenRead(filename))
{
if (s.ReadUInt32() != ZipSignature)
{
package = null;
return false;
}
}
package = new ReadWriteZipFile(filename);
return true;
}
public static IReadWritePackage Create(string filename)
{
return new ReadWriteZipFile(filename, true);
}
}
}