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,20 @@
#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
namespace OpenRA.Mods.Common.Installer
{
public enum Availability
{
Unavailable,
GameSource,
DigitalInstall
}
}

View File

@@ -0,0 +1,21 @@
#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;
namespace OpenRA.Mods.Common.Installer
{
public interface ISourceAction
{
void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage);
}
}

View File

@@ -0,0 +1,19 @@
#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
namespace OpenRA.Mods.Common.Installer
{
public interface ISourceResolver
{
string FindSourcePath(ModContent.ModSource modSource);
Availability GetAvailability();
}
}

View File

@@ -0,0 +1,80 @@
#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.IO;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public static class InstallerUtils
{
public static bool IsValidSourcePath(string path, ModContent.ModSource source)
{
if (source.IDFiles == null)
return true;
try
{
foreach (var kv in source.IDFiles.Nodes)
{
var filePath = FS.ResolveCaseInsensitivePath(Path.Combine(path, kv.Key));
if (!File.Exists(filePath))
return false;
using (var fileStream = File.OpenRead(filePath))
{
var offsetNode = kv.Value.NodeWithKeyOrDefault("Offset");
var lengthNode = kv.Value.NodeWithKeyOrDefault("Length");
if (offsetNode != null || lengthNode != null)
{
var offset = 0L;
if (offsetNode != null)
offset = FieldLoader.GetValue<long>("Offset", offsetNode.Value.Value);
var length = fileStream.Length - offset;
if (lengthNode != null)
length = FieldLoader.GetValue<long>("Length", lengthNode.Value.Value);
fileStream.Position = offset;
var data = fileStream.ReadBytes((int)length);
if (CryptoUtil.SHA1Hash(data) != kv.Value.Value)
return false;
}
else if (CryptoUtil.SHA1Hash(fileStream) != kv.Value.Value)
return false;
}
}
}
catch (Exception)
{
return false;
}
return true;
}
public static void CopyStream(Stream input, Stream output, long length, Action<long> onProgress = null)
{
var buffer = new byte[4096];
var copied = 0L;
while (copied < length)
{
var read = (int)Math.Min(buffer.Length, length - copied);
var write = input.Read(buffer, 0, read);
output.Write(buffer, 0, write);
copied += write;
onProgress?.Invoke(copied);
}
}
}
}

View File

@@ -0,0 +1,59 @@
#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.Mods.Common.Widgets.Logic;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public class CopySourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage)
{
var sourceDir = Path.Combine(path, actionYaml.Value);
foreach (var node in actionYaml.Nodes)
{
var sourcePath = FS.ResolveCaseInsensitivePath(Path.Combine(sourceDir, node.Value.Value));
var targetPath = Platform.ResolvePath(node.Key);
if (File.Exists(targetPath))
{
Log.Write("install", "Ignoring installed file " + targetPath);
continue;
}
Log.Write("install", $"Copying {sourcePath} -> {targetPath}");
extracted.Add(targetPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
using (var source = File.OpenRead(sourcePath))
using (var target = File.OpenWrite(targetPath))
{
var displayFilename = Path.GetFileName(targetPath);
var length = source.Length;
Action<long> onProgress = null;
if (length < InstallFromSourceLogic.ShowPercentageThreshold)
updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.CopyingFilename,
"filename", displayFilename));
else
onProgress = b => updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.CopyingFilenameProgress,
"filename", displayFilename,
"progress", 100 * b / length));
InstallerUtils.CopyStream(source, target, length, onProgress);
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
#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.Mods.Common.Installer
{
public class DeleteSourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage)
{
// Yaml path must be specified relative to a named directory (e.g. ^SupportDir)
if (!actionYaml.Value.StartsWith('^'))
return;
var sourcePath = Platform.ResolvePath(actionYaml.Value);
Log.Write("debug", $"Deleting {sourcePath}");
File.Delete(sourcePath);
}
}
}

View 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;
using System.Collections.Generic;
using System.IO;
using OpenRA.Mods.Common.FileFormats;
using OpenRA.Mods.Common.Widgets.Logic;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public class ExtractBlastSourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage)
{
// Yaml path may be specified relative to a named directory (e.g. ^SupportDir) or the detected source path
var sourcePath = actionYaml.Value.StartsWith('^')
? Platform.ResolvePath(actionYaml.Value)
: FS.ResolveCaseInsensitivePath(Path.Combine(path, actionYaml.Value));
using (var source = File.OpenRead(sourcePath))
{
source.Position = 12;
var numFiles = source.ReadUInt16();
source.Position = 51;
source.Position = source.ReadUInt32();
var entries = new Dictionary<string, (uint Length, uint Offset)>();
for (var i = 0; i < numFiles; i++)
{
source.Position += 7;
var entry = (source.ReadUInt32(), source.ReadUInt32());
source.Position += 14;
var key = source.ReadASCII(source.ReadByte());
source.Position += 13;
// This does not apply on game relevant data.
entries.TryAdd(key, entry);
}
foreach (var node in actionYaml.Nodes)
{
var targetPath = Platform.ResolvePath(node.Key);
if (File.Exists(targetPath))
{
Log.Write("install", "Skipping installed file " + targetPath);
continue;
}
var (length, offset) = entries[node.Value.Value];
source.Position = offset;
extracted.Add(targetPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
var displayFilename = Path.GetFileName(Path.GetFileName(targetPath));
Action<long> onProgress = null;
if (length < InstallFromSourceLogic.ShowPercentageThreshold)
updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.Extracting,
"filename", displayFilename));
else
onProgress = b => updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.ExtractingProgress,
"filename", displayFilename,
"progress", 100 * b / length));
using (var target = File.OpenWrite(targetPath))
{
Log.Write("install", $"Extracting {sourcePath} -> {targetPath}");
Blast.Decompress(source, target, (read, _) => onProgress?.Invoke(read));
}
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
#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.Mods.Common.FileFormats;
using OpenRA.Mods.Common.Widgets.Logic;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public class ExtractIscabSourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage)
{
// Yaml path may be specified relative to a named directory (e.g. ^SupportDir) or the detected source path
var sourcePath = actionYaml.Value.StartsWith('^')
? Platform.ResolvePath(actionYaml.Value)
: FS.ResolveCaseInsensitivePath(Path.Combine(path, actionYaml.Value));
var volumeNode = actionYaml.NodeWithKeyOrDefault("Volumes");
if (volumeNode == null)
throw new InvalidDataException("extract-iscab entry doesn't define a Volumes node");
var extractNode = actionYaml.NodeWithKeyOrDefault("Extract");
if (extractNode == null)
throw new InvalidDataException("extract-iscab entry doesn't define an Extract node");
var volumes = new Dictionary<int, Stream>();
try
{
foreach (var node in volumeNode.Value.Nodes)
{
var volume = FieldLoader.GetValue<int>("(key)", node.Key);
var stream = File.OpenRead(FS.ResolveCaseInsensitivePath(Path.Combine(path, node.Value.Value)));
volumes.Add(volume, stream);
}
using (var source = File.OpenRead(sourcePath))
{
var reader = new InstallShieldCABCompression(source, volumes);
foreach (var node in extractNode.Value.Nodes)
{
var targetPath = Platform.ResolvePath(node.Key);
if (File.Exists(targetPath))
{
Log.Write("install", "Skipping installed file " + targetPath);
continue;
}
extracted.Add(targetPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
using (var target = File.OpenWrite(targetPath))
{
Log.Write("install", $"Extracting {sourcePath} -> {targetPath}");
var displayFilename = Path.GetFileName(Path.GetFileName(targetPath));
void OnProgress(int percent) => updateMessage(FluentProvider.GetMessage(
InstallFromSourceLogic.ExtractingProgress,
"filename", displayFilename, "progress", percent));
reader.ExtractFile(node.Value.Value, target, OnProgress);
}
}
}
}
finally
{
foreach (var kv in volumes)
kv.Value.Dispose();
}
}
}
}

View File

@@ -0,0 +1,58 @@
#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.Mods.Common.FileFormats;
using OpenRA.Mods.Common.Widgets.Logic;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public class ExtractMscabSourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage)
{
// Yaml path may be specified relative to a named directory (e.g. ^SupportDir) or the detected source path
var sourcePath = actionYaml.Value.StartsWith('^')
? Platform.ResolvePath(actionYaml.Value)
: FS.ResolveCaseInsensitivePath(Path.Combine(path, actionYaml.Value));
using (var source = File.OpenRead(sourcePath))
{
var reader = new MSCabCompression(source);
foreach (var node in actionYaml.Nodes)
{
var targetPath = Platform.ResolvePath(node.Key);
if (File.Exists(targetPath))
{
Log.Write("install", "Skipping installed file " + targetPath);
continue;
}
extracted.Add(targetPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
using (var target = File.OpenWrite(targetPath))
{
Log.Write("install", $"Extracting {sourcePath} -> {targetPath}");
var displayFilename = Path.GetFileName(Path.GetFileName(targetPath));
void OnProgress(int percent) => updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.ExtractingProgress,
"filename", displayFilename,
"progress", percent));
reader.ExtractFile(node.Value.Value, target, OnProgress);
}
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
#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.Mods.Common.Widgets.Logic;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public class ExtractRawSourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted, Action<string> updateMessage)
{
// Yaml path may be specified relative to a named directory (e.g. ^SupportDir) or the detected source path
var sourcePath = actionYaml.Value.StartsWith('^')
? Platform.ResolvePath(actionYaml.Value)
: FS.ResolveCaseInsensitivePath(Path.Combine(path, actionYaml.Value));
using (var source = File.OpenRead(sourcePath))
{
foreach (var node in actionYaml.Nodes)
{
var targetPath = Platform.ResolvePath(node.Key);
if (File.Exists(targetPath))
{
Log.Write("install", "Skipping installed file " + targetPath);
continue;
}
var offsetNode = node.Value.NodeWithKeyOrDefault("Offset");
if (offsetNode == null)
{
Log.Write("install", "Skipping entry with missing Offset definition " + targetPath);
continue;
}
var lengthNode = node.Value.NodeWithKeyOrDefault("Length");
if (lengthNode == null)
{
Log.Write("install", "Skipping entry with missing Length definition " + targetPath);
continue;
}
var length = FieldLoader.GetValue<int>("Length", lengthNode.Value.Value);
source.Position = FieldLoader.GetValue<int>("Offset", offsetNode.Value.Value);
extracted.Add(targetPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
var displayFilename = Path.GetFileName(Path.GetFileName(targetPath));
Action<long> onProgress = null;
if (length < InstallFromSourceLogic.ShowPercentageThreshold)
updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.Extracting,
"filename", displayFilename));
else
onProgress = b => updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.ExtractingProgress,
"filename", displayFilename,
"progress", 100 * b / length));
using (var target = File.OpenWrite(targetPath))
{
Log.Write("install", $"Extracting {sourcePath} -> {targetPath}");
InstallerUtils.CopyStream(source, target, length, onProgress);
}
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
#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 ICSharpCode.SharpZipLib.Zip;
using OpenRA.Mods.Common.Widgets.Logic;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA.Mods.Common.Installer
{
public class ExtractZipSourceAction : ISourceAction
{
public void RunActionOnSource(MiniYaml actionYaml, string path, ModData modData, List<string> extracted,
Action<string> updateMessage)
{
var zipPath = actionYaml.Value.StartsWith('^')
? Platform.ResolvePath(actionYaml.Value)
: FS.ResolveCaseInsensitivePath(Path.Combine(path, actionYaml.Value));
using (var zipFile = new ZipFile(File.OpenRead(zipPath)))
{
foreach (var node in actionYaml.Nodes)
{
var targetPath = Platform.ResolvePath(node.Key);
var sourcePath = node.Value.Value;
var displayFilename = Path.GetFileName(targetPath);
if (File.Exists(targetPath))
{
Log.Write("install", "Skipping installed file " + targetPath);
continue;
}
Log.Write("install", $"Extracting {sourcePath} -> {targetPath}");
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
var sourceStream = zipFile.GetInputStream(zipFile.GetEntry(sourcePath));
using (var targetStream = File.OpenWrite(targetPath))
sourceStream.CopyTo(targetStream);
updateMessage(FluentProvider.GetMessage(InstallFromSourceLogic.ExtractingProgress,
"filename", displayFilename,
"progress", 100));
extracted.Add(targetPath);
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
#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.IO;
using System.Linq;
namespace OpenRA.Mods.Common.Installer
{
public class DiscSourceResolver : ISourceResolver
{
public string FindSourcePath(ModContent.ModSource source)
{
var volumes = DriveInfo.GetDrives()
.Where(d =>
{
if (d.DriveType == DriveType.CDRom && d.IsReady)
return true;
// HACK: the "TFD" DVD is detected as a fixed udf-formatted drive on OSX
if (d.DriveType == DriveType.Fixed && d.DriveFormat == "udf")
return true;
return false;
})
.Select(v => v.RootDirectory.FullName);
if (Platform.CurrentPlatform == PlatformType.Linux)
{
// Outside of Gnome, most mounting tools on Linux don't set DriveType.CDRom
// so provide a fallback by allowing users to manually mount images on known paths
volumes = volumes.Concat(
[
"/media/openra",
"/media/" + Environment.UserName + "/openra",
"/mnt/openra"
]);
}
foreach (var volume in volumes)
if (InstallerUtils.IsValidSourcePath(volume, source))
return volume;
return null;
}
public Availability GetAvailability()
{
return Availability.GameSource;
}
}
}

View File

@@ -0,0 +1,53 @@
#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.Runtime.InteropServices;
using Microsoft.Win32;
namespace OpenRA.Mods.Common.Installer
{
public class GogSourceResolver : ISourceResolver
{
public string FindSourcePath(ModContent.ModSource modSource)
{
var appId = modSource.Type.NodeWithKeyOrDefault("AppId");
if (appId == null)
return null;
if (Platform.CurrentPlatform != PlatformType.Windows)
return null;
// We need an extra check for the platform here to silence a warning when the registry is accessed
// TODO: Remove this once our platform checks use the same method
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
var prefixes = new[] { "HKEY_LOCAL_MACHINE\\Software\\", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\" };
foreach (var prefix in prefixes)
{
if (Registry.GetValue($"{prefix}GOG.com\\Games\\{appId.Value.Value}", "path", null) is not string installDir)
continue;
if (InstallerUtils.IsValidSourcePath(installDir, modSource))
return installDir;
}
return null;
}
public Availability GetAvailability()
{
return Platform.CurrentPlatform != PlatformType.Windows ? Availability.DigitalInstall : Availability.Unavailable;
}
}
}

View 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.IO;
using System.Runtime.InteropServices;
namespace OpenRA.Mods.Common.Installer
{
public class RegistryDirectoryFromFileSourceResolver : ISourceResolver
{
public string FindSourcePath(ModContent.ModSource source)
{
if (source.RegistryKey == null)
return null;
if (Platform.CurrentPlatform != PlatformType.Windows)
return null;
// We need an extra check for the platform here to silence a warning when the registry is accessed
// TODO: Remove this once our platform checks use the same method
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
foreach (var prefix in source.RegistryPrefixes)
{
if (Microsoft.Win32.Registry.GetValue(prefix + source.RegistryKey, source.RegistryValue, null) is not string path)
continue;
path = Path.GetDirectoryName(path);
return InstallerUtils.IsValidSourcePath(path, source) ? path : null;
}
return null;
}
public Availability GetAvailability()
{
return Platform.CurrentPlatform != PlatformType.Windows ? Availability.DigitalInstall : Availability.Unavailable;
}
}
}

View File

@@ -0,0 +1,51 @@
#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.IO;
using System.Runtime.InteropServices;
namespace OpenRA.Mods.Common.Installer
{
public class RegistryDirectorySourceResolver : ISourceResolver
{
public string FindSourcePath(ModContent.ModSource source)
{
if (source.RegistryKey == null)
return null;
if (Platform.CurrentPlatform != PlatformType.Windows)
return null;
// We need an extra check for the platform here to silence a warning when the registry is accessed
// TODO: Remove this once our platform checks use the same method
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
foreach (var prefix in source.RegistryPrefixes)
{
if (Microsoft.Win32.Registry.GetValue(prefix + source.RegistryKey, source.RegistryValue, null) is not string path)
continue;
// Resolve 8.3 format (DOS-style) paths to the full path.
path = Path.GetFullPath(path);
return InstallerUtils.IsValidSourcePath(path, source) ? path : null;
}
return null;
}
public Availability GetAvailability()
{
return Platform.CurrentPlatform != PlatformType.Windows ? Availability.DigitalInstall : Availability.Unavailable;
}
}
}

View File

@@ -0,0 +1,172 @@
#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 System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace OpenRA.Mods.Common.Installer
{
public class SteamSourceResolver : ISourceResolver
{
public string FindSourcePath(ModContent.ModSource modSource)
{
var appId = modSource.Type.NodeWithKeyOrDefault("AppId");
if (appId == null)
return null;
foreach (var steamDirectory in SteamDirectory())
{
var manifestPath = Path.Combine(steamDirectory, "steamapps", $"appmanifest_{appId.Value.Value}.acf");
if (!File.Exists(manifestPath))
continue;
var data = ParseGameManifest(manifestPath);
if (!data.TryGetValue("StateFlags", out var stateFlags) || stateFlags != "4")
continue;
if (!data.TryGetValue("installdir", out var installDir))
continue;
if (installDir == null)
continue;
var path = Path.Combine(steamDirectory, "steamapps", "common", installDir);
if (InstallerUtils.IsValidSourcePath(path, modSource))
return path;
}
return null;
}
public Availability GetAvailability()
{
return Availability.DigitalInstall;
}
static IEnumerable<string> SteamDirectory()
{
var candidatePaths = new List<string>();
switch (Platform.CurrentPlatform)
{
case PlatformType.Windows:
{
// We need an extra check for the platform here to silence a warning when the registry is accessed
// TODO: Remove this once our platform checks use the same method
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
break;
var prefixes = new[] { "HKEY_LOCAL_MACHINE\\Software\\", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\" };
foreach (var prefix in prefixes)
{
if (Registry.GetValue($"{prefix}Valve\\Steam", "InstallPath", null) is string path)
candidatePaths.Add(path);
}
break;
}
case PlatformType.OSX:
candidatePaths.Add(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Steam"));
break;
case PlatformType.Linux:
// Direct distro install
candidatePaths.Add(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".steam",
"root"));
// Flatpak installed via Flathub
candidatePaths.Add(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".var",
"app",
"com.valvesoftware.Steam",
".steam",
"root"));
break;
}
foreach (var libraryPath in candidatePaths.Where(Directory.Exists))
{
yield return libraryPath;
var libraryFoldersPath = Path.Combine(libraryPath, "steamapps", "libraryfolders.vdf");
if (!File.Exists(libraryFoldersPath))
continue;
foreach (var e in ParseLibraryManifest(libraryFoldersPath).Where(e => e.Item1 == "path"))
yield return Unescape(e.Item2);
}
}
static string Unescape(string path)
{
return path.Replace(@"\\", @"\");
}
static Dictionary<string, string> ParseGameManifest(string path)
{
var regex = new Regex("^\\s*\"(?<key>[^\"]*)\"\\s*\"(?<value>[^\"]*)\"\\s*$");
var result = new Dictionary<string, string>();
using (var s = new FileStream(path, FileMode.Open))
{
foreach (var line in s.ReadAllLines())
{
var match = regex.Match(line);
if (match.Success)
result[match.Groups["key"].Value] = match.Groups["value"].Value;
}
}
return result;
}
static List<Tuple<string, string>> ParseLibraryManifest(string path)
{
var regex = new Regex("^\\s*\"(?<key>[^\"]*)\"\\s*\"(?<value>[^\"]*)\"\\s*$");
var result = new List<Tuple<string, string>>();
using (var s = new FileStream(path, FileMode.Open))
{
foreach (var line in s.ReadAllLines())
{
var match = regex.Match(line);
if (match.Success)
result.Add(new Tuple<string, string>(match.Groups["key"].Value, match.Groups["value"].Value));
}
}
return result;
}
}
}