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:
@@ -0,0 +1,112 @@
|
||||
#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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileSystem
|
||||
{
|
||||
[Desc("A file system that loads game assets installed by the user into their support directory.")]
|
||||
public class ContentInstallerFileSystemLoader : IFileSystemLoader, IFileSystemExternalContent
|
||||
{
|
||||
[FieldLoader.Require]
|
||||
[Desc("Mod to use for content installation.")]
|
||||
public readonly string ContentInstallerMod = null;
|
||||
|
||||
[Desc("A list of mod-provided packages. Anything required to display the initial load screen must be listed here.")]
|
||||
[FieldLoader.LoadUsing(nameof(LoadSystemPackages))]
|
||||
public readonly ImmutableArray<KeyValuePair<string, string>> SystemPackages = default;
|
||||
|
||||
[Desc("A list of user-installed packages. If missing (and not marked as optional), these will trigger the content installer.")]
|
||||
[FieldLoader.LoadUsing(nameof(LoadContentPackages))]
|
||||
public readonly ImmutableArray<KeyValuePair<string, string>> ContentPackages = default;
|
||||
|
||||
[Desc("Files that aren't mounted as packages, but still need to trigger the content installer if missing.")]
|
||||
[FieldLoader.LoadUsing(nameof(LoadRequiredContentFiles))]
|
||||
public readonly ImmutableArray<KeyValuePair<string, string>> RequiredContentFiles = default;
|
||||
|
||||
bool isContentAvailable = true;
|
||||
|
||||
static object LoadSystemPackages(MiniYaml yaml)
|
||||
{
|
||||
return LoadPackages(yaml, nameof(SystemPackages), true);
|
||||
}
|
||||
|
||||
static object LoadContentPackages(MiniYaml yaml)
|
||||
{
|
||||
return LoadPackages(yaml, nameof(ContentPackages), false);
|
||||
}
|
||||
|
||||
static object LoadRequiredContentFiles(MiniYaml yaml)
|
||||
{
|
||||
return LoadPackages(yaml, nameof(RequiredContentFiles), false);
|
||||
}
|
||||
|
||||
static object LoadPackages(MiniYaml yaml, string key, bool required)
|
||||
{
|
||||
var packageNode = yaml.NodeWithKeyOrDefault(key);
|
||||
if (packageNode == null)
|
||||
{
|
||||
if (required)
|
||||
throw new FieldLoader.MissingFieldsException([key]);
|
||||
return default(ImmutableArray<KeyValuePair<string, string>>);
|
||||
}
|
||||
|
||||
var packages = new List<KeyValuePair<string, string>>(packageNode.Value.Nodes.Length);
|
||||
foreach (var node in packageNode.Value.Nodes)
|
||||
packages.Add(KeyValuePair.Create(node.Key, node.Value.Value));
|
||||
|
||||
return packages.ToImmutableArray();
|
||||
}
|
||||
|
||||
public void Mount(Manifest manifest, OpenRA.FileSystem.FileSystem fileSystem, ObjectCreator objectCreator)
|
||||
{
|
||||
foreach (var kv in SystemPackages)
|
||||
fileSystem.Mount(kv.Key, kv.Value);
|
||||
|
||||
if (ContentPackages != null)
|
||||
{
|
||||
foreach (var kv in ContentPackages)
|
||||
{
|
||||
try
|
||||
{
|
||||
fileSystem.Mount(kv.Key, kv.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
isContentAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (RequiredContentFiles != null)
|
||||
foreach (var kv in RequiredContentFiles)
|
||||
if (!fileSystem.Exists(kv.Key))
|
||||
isContentAvailable = false;
|
||||
}
|
||||
|
||||
bool IFileSystemExternalContent.InstallContentIfRequired(ModData modData)
|
||||
{
|
||||
if (!isContentAvailable && Game.Mods.TryGetValue(ContentInstallerMod, out var mod))
|
||||
Game.InitializeMod(mod, new Arguments());
|
||||
|
||||
return !isContentAvailable;
|
||||
}
|
||||
|
||||
void IFileSystemExternalContent.ManageContent(ModData modData)
|
||||
{
|
||||
// Switching mods changes the world state (by disposing it),
|
||||
// so we can't do this inside the input handler.
|
||||
if (Game.Mods.TryGetValue(ContentInstallerMod, out var mod))
|
||||
Game.RunAfterTick(() => Game.InitializeMod(mod, new Arguments()));
|
||||
}
|
||||
}
|
||||
}
|
||||
50
OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs
Normal file
50
OpenRA.Mods.Common/FileSystem/DefaultFileSystemLoader.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
#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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileSystem
|
||||
{
|
||||
[RequireExplicitImplementation]
|
||||
public interface IFileSystemExternalContent
|
||||
{
|
||||
bool InstallContentIfRequired(ModData modData);
|
||||
void ManageContent(ModData modData);
|
||||
}
|
||||
|
||||
public class DefaultFileSystemLoader : IFileSystemLoader
|
||||
{
|
||||
[FieldLoader.LoadUsing(nameof(LoadPackages))]
|
||||
public readonly ImmutableArray<KeyValuePair<string, string>> Packages = default;
|
||||
|
||||
static object LoadPackages(MiniYaml yaml)
|
||||
{
|
||||
var packageNode = yaml.NodeWithKeyOrDefault(nameof(Packages));
|
||||
if (packageNode == null)
|
||||
return default(ImmutableArray<KeyValuePair<string, string>>);
|
||||
|
||||
var packages = new List<KeyValuePair<string, string>>(packageNode.Value.Nodes.Length);
|
||||
foreach (var node in packageNode.Value.Nodes)
|
||||
packages.Add(KeyValuePair.Create(node.Key, node.Value.Value));
|
||||
|
||||
return packages.ToImmutableArray();
|
||||
}
|
||||
|
||||
public void Mount(Manifest manifest, OpenRA.FileSystem.FileSystem fileSystem, ObjectCreator objectCreator)
|
||||
{
|
||||
if (Packages != null)
|
||||
foreach (var kv in Packages)
|
||||
fileSystem.Mount(kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
221
OpenRA.Mods.Common/FileSystem/ISO9660.cs
Normal file
221
OpenRA.Mods.Common/FileSystem/ISO9660.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
#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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
using FS = OpenRA.FileSystem.FileSystem;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileSystem
|
||||
{
|
||||
public class ISO9660Loader : IPackageLoader
|
||||
{
|
||||
public sealed class ISO9660Package : IReadOnlyPackage
|
||||
{
|
||||
public readonly record struct Entry(uint Offset, uint Length);
|
||||
|
||||
public string Name { get; }
|
||||
public string VolumeName { get; }
|
||||
|
||||
public IEnumerable<string> Contents => index.Keys;
|
||||
|
||||
readonly Dictionary<string, Entry> index = [];
|
||||
readonly Stream s;
|
||||
|
||||
readonly record struct DirectoryRecord(string Name, uint Offset, uint Length, bool IsDirectory);
|
||||
static bool TryReadDirectoryRecord(Stream s, out DirectoryRecord record, bool isJoliet = false)
|
||||
{
|
||||
var start = s.Position;
|
||||
var recordLength = s.ReadUInt8();
|
||||
if (recordLength == 0)
|
||||
{
|
||||
s.Position = start;
|
||||
record = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
s.Position++;
|
||||
var location = s.ReadUInt32();
|
||||
s.Position += 4;
|
||||
var length = s.ReadUInt32();
|
||||
s.Position += 11;
|
||||
var flags = s.ReadUInt8();
|
||||
s.Position += 6;
|
||||
|
||||
var identifierLength = s.ReadUInt8();
|
||||
var buffer = identifierLength < 128 ? stackalloc byte[identifierLength] : new byte[identifierLength];
|
||||
s.ReadBytes(buffer);
|
||||
var identifier = (isJoliet ? Encoding.BigEndianUnicode : Encoding.ASCII).GetString(buffer);
|
||||
|
||||
s.Position = start + recordLength;
|
||||
|
||||
record = new DirectoryRecord(identifier.Split(';')[0], 2048 * location, length, (flags & 2) != 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnumerateDirectories(DirectoryRecord parent, string path = null, bool isJoliet = false)
|
||||
{
|
||||
var pos = s.Position;
|
||||
s.Position = parent.Offset;
|
||||
|
||||
// Skip . and .. records
|
||||
TryReadDirectoryRecord(s, out _);
|
||||
TryReadDirectoryRecord(s, out _);
|
||||
while (s.Position < parent.Offset + parent.Length)
|
||||
{
|
||||
if (!TryReadDirectoryRecord(s, out var child, isJoliet))
|
||||
break;
|
||||
|
||||
var childPath = path != null ? $"{path}/{child.Name}" : child.Name;
|
||||
if (child.IsDirectory)
|
||||
EnumerateDirectories(child, childPath, isJoliet);
|
||||
else
|
||||
index[childPath] = new Entry(child.Offset, child.Length);
|
||||
}
|
||||
|
||||
s.Position = pos;
|
||||
}
|
||||
|
||||
public ISO9660Package(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
try
|
||||
{
|
||||
var complete = false;
|
||||
|
||||
// Skip system area
|
||||
s.Position = 32768;
|
||||
|
||||
// Parse volume descriptors
|
||||
while (!complete && s.Position < s.Length)
|
||||
{
|
||||
var start = s.Position;
|
||||
|
||||
var vdType = s.ReadUInt8();
|
||||
var vdIdentifier = s.ReadASCII(5);
|
||||
if (vdIdentifier != "CD001")
|
||||
throw new InvalidDataException("Invalid volume descriptor");
|
||||
|
||||
switch (vdType)
|
||||
{
|
||||
// Terminator
|
||||
case 0xFF:
|
||||
complete = true;
|
||||
break;
|
||||
|
||||
// Primary volume descriptor
|
||||
case 0x01:
|
||||
{
|
||||
s.Position = start + 40;
|
||||
VolumeName = s.ReadASCII(32).Trim();
|
||||
|
||||
s.Position = start + 156;
|
||||
TryReadDirectoryRecord(s, out var root);
|
||||
EnumerateDirectories(root);
|
||||
break;
|
||||
}
|
||||
|
||||
// Supplementary volume descriptor
|
||||
case 0x02:
|
||||
{
|
||||
s.Position = start + 7;
|
||||
var volumeFlags = s.ReadUInt8();
|
||||
s.Position = start + 88;
|
||||
var escape = s.ReadASCII(32).Trim('\x00');
|
||||
|
||||
// Joliet extension
|
||||
if (volumeFlags == 0 && escape is "%/@" or "%/C" or "%/E")
|
||||
{
|
||||
s.Position = start + 156;
|
||||
TryReadDirectoryRecord(s, out var root, isJoliet: true);
|
||||
EnumerateDirectories(root, isJoliet: true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s.Position = start + 2048;
|
||||
}
|
||||
|
||||
index.TrimExcess();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
if (!index.TryGetValue(filename, out var entry))
|
||||
return null;
|
||||
|
||||
return SegmentStream.CreateWithoutOwningStream(s, entry.Offset, (int)entry.Length);
|
||||
}
|
||||
|
||||
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 bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Entry> Index => new ReadOnlyDictionary<string, Entry>(index);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FS context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (s.Length < 34816)
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the volume descriptor
|
||||
var pos = s.Position;
|
||||
s.Position = 32769;
|
||||
var identifier = s.ReadASCII(5);
|
||||
s.Position = pos;
|
||||
|
||||
if (identifier != "CD001")
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new ISO9660Package(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
162
OpenRA.Mods.Common/FileSystem/InstallShieldPackage.cs
Normal file
162
OpenRA.Mods.Common/FileSystem/InstallShieldPackage.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
#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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Mods.Common.FileFormats;
|
||||
using FS = OpenRA.FileSystem.FileSystem;
|
||||
|
||||
namespace OpenRA.Mods.Common.FileSystem
|
||||
{
|
||||
public class InstallShieldLoader : IPackageLoader
|
||||
{
|
||||
public sealed class InstallShieldPackage : IReadOnlyPackage
|
||||
{
|
||||
public readonly record struct Entry(uint Offset, uint Length);
|
||||
|
||||
public string Name { get; }
|
||||
public IEnumerable<string> Contents => index.Keys;
|
||||
|
||||
readonly Dictionary<string, Entry> index;
|
||||
readonly Stream s;
|
||||
readonly long dataStart = 255;
|
||||
|
||||
public InstallShieldPackage(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
try
|
||||
{
|
||||
// Parse package header
|
||||
s.ReadUInt32(); // signature
|
||||
s.Position += 8;
|
||||
s.ReadUInt16(); // FileCount
|
||||
s.Position += 4;
|
||||
s.ReadUInt32(); // ArchiveSize
|
||||
s.Position += 19;
|
||||
var tocAddress = s.ReadInt32();
|
||||
s.Position += 4;
|
||||
var dirCount = s.ReadUInt16();
|
||||
|
||||
// Parse the directory list
|
||||
s.Position = tocAddress;
|
||||
|
||||
// Parse directories
|
||||
var directories = new Dictionary<string, uint>(dirCount);
|
||||
var totalFileCount = 0;
|
||||
for (var i = 0; i < dirCount; i++)
|
||||
{
|
||||
// Parse directory header
|
||||
var fileCount = s.ReadUInt16();
|
||||
var chunkSize = s.ReadUInt16();
|
||||
var nameLength = s.ReadUInt16();
|
||||
var dirName = s.ReadASCII(nameLength);
|
||||
|
||||
// Skip to the end of the chunk
|
||||
s.Position += chunkSize - nameLength - 6;
|
||||
directories.Add(dirName, fileCount);
|
||||
totalFileCount += fileCount;
|
||||
}
|
||||
|
||||
// Parse files
|
||||
index = new Dictionary<string, Entry>(totalFileCount);
|
||||
foreach (var dir in directories)
|
||||
for (var i = 0; i < dir.Value; i++)
|
||||
ParseFile(dir.Key);
|
||||
|
||||
index.TrimExcess();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
uint accumulatedData = 0;
|
||||
void ParseFile(string dirName)
|
||||
{
|
||||
s.Position += 7;
|
||||
var compressedSize = s.ReadUInt32();
|
||||
s.Position += 12;
|
||||
var chunkSize = s.ReadUInt16();
|
||||
s.Position += 4;
|
||||
var nameLength = s.ReadUInt8();
|
||||
var fileName = dirName + "\\" + s.ReadASCII(nameLength);
|
||||
|
||||
// Use index syntax to overwrite any duplicate entries with the last value
|
||||
index[fileName] = new Entry(accumulatedData, compressedSize);
|
||||
accumulatedData += compressedSize;
|
||||
|
||||
// Skip to the end of the chunk
|
||||
s.Position += chunkSize - nameLength - 30;
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
if (!index.TryGetValue(filename, out var e))
|
||||
return null;
|
||||
|
||||
s.Seek(dataStart + e.Offset, SeekOrigin.Begin);
|
||||
|
||||
var ret = new MemoryStream();
|
||||
Blast.Decompress(s, ret);
|
||||
ret.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
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 bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Entry> Index => new ReadOnlyDictionary<string, Entry>(index);
|
||||
|
||||
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.ReadUInt32();
|
||||
s.Position -= 4;
|
||||
|
||||
if (signature != 0x8C655D13)
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new InstallShieldPackage(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user