tools/installer: create TR2 installer

This creates a TR2 installer in similar fashion to TR1. The installers
themselves are basically directory tree copiers, rather than having to
extract from BIN/ISO like TR1.
This commit is contained in:
lahm86 2025-03-25 20:47:02 +00:00
parent b670eaa16a
commit 0618baebc9
24 changed files with 343 additions and 0 deletions

View file

@ -0,0 +1,4 @@
<Application
x:Class="TR2X_Installer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"/>

View file

@ -0,0 +1,21 @@
using System.Windows;
using TR2X_Installer.Installers;
using TRX_InstallerLib.Controls;
using TRX_InstallerLib.Installers;
namespace TR2X_Installer;
public partial class App : Application
{
public App()
{
Current.MainWindow = new TRXInstallWindow(new List<IInstallSource>
{
new SteamInstallSource(),
new GOGInstallSource(),
new CDRomInstallSource(),
new TR2XInstallSource(),
});
Current.MainWindow.Show();
}
}

View file

@ -0,0 +1,31 @@
using System.IO;
namespace TR2X_Installer.Installers;
public class CDRomInstallSource : GenericInstallSource
{
public override IEnumerable<string> DirectoriesToTry
{
get
{
DriveInfo[] allDrives = DriveInfo.GetDrives();
foreach (var drive in allDrives)
{
if (drive.DriveType == DriveType.CDRom && drive.IsReady)
{
yield return drive.RootDirectory.FullName;
}
}
}
}
public override bool IsImportingSavesSupported => false;
public override string SourceName => "CDRom";
public override bool IsGameFound(string sourceDirectory)
{
return File.Exists(Path.Combine(sourceDirectory, "fmv", "ancient.rpl"))
&& File.Exists(Path.Combine(sourceDirectory, "data", "wall.tr2"))
&& File.Exists(Path.Combine(sourceDirectory, "data", "main.sfx"));
}
}

View file

@ -0,0 +1,36 @@
using Microsoft.Win32;
using System.IO;
using System.Text.RegularExpressions;
namespace TR2X_Installer.Installers;
public class GOGInstallSource : GenericInstallSource
{
public override IEnumerable<string> DirectoriesToTry
{
get
{
yield return @"C:\Program Files (x86)\GOG Galaxy\Games\Tomb Raider 2";
using var key = Registry.ClassesRoot.OpenSubKey(@"goggalaxy\shell\open\command");
if (key is not null)
{
var value = key.GetValue("")?.ToString();
if (value is not null && new Regex(@"""(?<path>[^""]+)""").Match(value) is { Success: true } match)
{
yield return Path.Combine(Path.GetDirectoryName(match.Groups["path"].Value)!, @"Games\Tomb Raider 2");
}
}
}
}
public override bool IsImportingSavesSupported => true;
public override string SourceName => "GOG";
public override bool IsGameFound(string sourceDirectory)
{
return File.Exists(Path.Combine(sourceDirectory, "tomb2.exe"))
&& File.Exists(Path.Combine(sourceDirectory, "data", "wall.tr2"))
&& File.Exists(Path.Combine(sourceDirectory, "data", "main.sfx"));
}
}

View file

@ -0,0 +1,71 @@
using System.IO;
using System.Text.RegularExpressions;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Utils;
namespace TR2X_Installer.Installers;
public abstract class GenericInstallSource : BaseInstallSource
{
private static readonly Dictionary<string, List<string>> _targetFiles = new()
{
["data"] = new() { ".tr2", ".sfx", ".pcx" },
["fmv"] = new() { ".*" },
["music"] = new() { ".flac", ".ogg", ".mp3", ".wav" },
};
public override bool IsDownloadingMusicNeeded(string sourceDirectory)
{
return !Directory.Exists(Path.Combine(sourceDirectory, "audio"))
&& !Directory.Exists(Path.Combine(sourceDirectory, "music"));
}
public override bool IsDownloadingExpansionNeeded(string sourceDirectory)
{
return true;
}
public override async Task CopyOriginalGameFiles(
string sourceDirectory,
string targetDirectory,
IProgress<InstallProgress> progress,
bool importSaves
)
{
await InstallUtils.CopyDirectoryTree(
sourceDirectory,
targetDirectory,
progress,
file => IsMatch(sourceDirectory, file, importSaves),
path => ConvertTargetPath(path)
);
string musicDir = Path.Combine(targetDirectory, "music");
string audioDir = Path.Combine(sourceDirectory, "audio");
if ((Directory.Exists(musicDir) && Directory.EnumerateFiles(musicDir).Any()) || !Directory.Exists(audioDir))
{
return;
}
await InstallUtils.CopyDirectoryTree(
Path.Combine(sourceDirectory, "audio"),
Path.Combine(targetDirectory, "audio"),
progress,
null,
path => ConvertTargetPath(path)
);
}
private static bool IsMatch(string sourceDirectory, string path, bool importSaves)
{
string[] parts = Path.GetRelativePath(sourceDirectory, path).ToLower().Split('\\');
if (parts.Length == 1 && importSaves && Regex.IsMatch(parts[0], @"savegame.\d+", RegexOptions.IgnoreCase))
{
return true;
}
return parts.Length > 0
&& _targetFiles.ContainsKey(parts[0])
&& (_targetFiles[parts[0]].Contains(".*") || _targetFiles[parts[0]].Contains(Path.GetExtension(path).ToLower()));
}
}

View file

@ -0,0 +1,27 @@
using Microsoft.Win32;
using System.IO;
namespace TR2X_Installer.Installers;
public class SteamInstallSource : GOGInstallSource
{
public override IEnumerable<string> DirectoriesToTry
{
get
{
yield return @"C:\Program Files (x86)\Steam\steamapps\common\Tomb Raider (II)";
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam");
if (key is not null)
{
var value = key.GetValue("SteamPath")?.ToString();
if (value is not null)
{
yield return Path.Combine(value, @"steamapps\common\Tomb Raider (II)");
}
}
}
}
public override string SourceName => "Steam";
}

View file

@ -0,0 +1,63 @@
using System.IO;
using System.Text.RegularExpressions;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Utils;
namespace TR2X_Installer.Installers;
public class TR2XInstallSource : GenericInstallSource
{
public override IEnumerable<string> DirectoriesToTry
{
get
{
var previousPath = InstallUtils.GetPreviousInstallationPath();
if (previousPath is not null)
{
yield return previousPath;
}
foreach (var path in InstallUtils.GetDesktopShortcutDirectories())
{
yield return path;
}
}
}
public override string SuggestedInstallationDirectory
{
get
{
return InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory;
}
}
public override bool IsImportingSavesSupported => true;
public override string SourceName => "TR2X";
public override async Task CopyOriginalGameFiles(
string sourceDirectory,
string targetDirectory,
IProgress<InstallProgress> progress,
bool importSaves
)
{
var filterRegex = new Regex(importSaves ? @"(audio|data|fmv|music|saves)[\\/]|save.*\.\d+" : @"(audio|data|fmv|music)[\\/]", RegexOptions.IgnoreCase);
await InstallUtils.CopyDirectoryTree(
sourceDirectory,
targetDirectory,
progress,
file => filterRegex.IsMatch(file)
);
}
public override bool IsDownloadingExpansionNeeded(string sourceDirectory)
{
return !File.Exists(Path.Combine(sourceDirectory, "data", "title_gm.tr2"));
}
public override bool IsGameFound(string sourceDirectory)
{
return File.Exists(Path.Combine(sourceDirectory, "TR2X.exe"));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,10 @@
{
"Controls": {
"window_title_main": "TR2X Installer",
"step_source_content": "TR2X requires original game files to run.\nPlease choose the source location where to install the data files from.\nIf you're upgrading an existing installation, please choose TR2X.",
"step_settings_music_content": "This option lets you download compatible music files for the game automatically (60 MB). The legality of these files is disputable; the most legal way to import the music to PC is to obtain them from your own source - TR2 supports FLAC, OGG, MP3 and WAV files.",
"step_settings_expansion_heading": "Download The Golden Mask expansion pack",
"step_settings_expansion_content": "The Golden Mask expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (15 MB).",
"step_settings_saves_content": "Imports existing savegame files."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,11 @@
{
"Game": "TR2X",
"GoldGame": "TR2X - GM",
"GoldFileIdentifier": "title_gm.tr2",
"AllowExpansionTypeSelection": false,
"ShortcutTitle": "Tomb Raider II: Community Edition",
"GoldZips": {
"0": "trgm.zip",
"1": "trgm.zip"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<AssemblyName>TR2X_Installer</AssemblyName>
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TRX_InstallerLib\TRX_InstallerLib.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\const.json" />
<None Remove="Resources\icon.ico" />
<None Remove="Resources\Lang\en.json" />
<None Remove="Resources\release.zip" />
<None Remove="Resources\side1.jpg" />
<None Remove="Resources\side2.jpg" />
<None Remove="Resources\side3.jpg" />
<None Remove="Resources\side4.jpg" />
<None Remove="Resources\TR2X.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\CDRom.png" />
<Resource Include="Resources\GOG.png" />
<Resource Include="Resources\icon.ico" />
<Resource Include="Resources\side1.jpg" />
<Resource Include="Resources\side2.jpg" />
<Resource Include="Resources\side3.jpg" />
<Resource Include="Resources\side4.jpg" />
<Resource Include="Resources\Steam.png" />
<Resource Include="Resources\TR2X.png" />
<EmbeddedResource Include="Resources\const.json" />
<EmbeddedResource Include="Resources\Lang\en.json" />
<EmbeddedResource Include="Resources\release.zip" />
</ItemGroup>
</Project>

View file

@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TRX_InstallerLib", "TRX_Ins
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TR1X_Installer", "TR1X_Installer\TR1X_Installer.csproj", "{5B32640D-3997-472F-A1BA-FCE4128E0688}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TR2X_Installer", "TR2X_Installer\TR2X_Installer.csproj", "{DCCEAD2D-BC68-40D7-B1B9-981450416466}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -21,6 +23,10 @@ Global
{5B32640D-3997-472F-A1BA-FCE4128E0688}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B32640D-3997-472F-A1BA-FCE4128E0688}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B32640D-3997-472F-A1BA-FCE4128E0688}.Release|Any CPU.Build.0 = Release|Any CPU
{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCCEAD2D-BC68-40D7-B1B9-981450416466}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -54,6 +54,16 @@ public static class InstallUtils
});
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
await Task.Run(() => File.Copy(sourcePath, targetPath, true));
try
{
var file = new FileInfo(targetPath);
if (file.Attributes.HasFlag(FileAttributes.ReadOnly))
{
file.IsReadOnly = false;
}
}
catch { }
}
else
{