tools/installer: migrate TR1X installer

This migrates the TR1X installer to use the new common library.
This commit is contained in:
lahm86 2025-03-24 21:16:26 +00:00
parent 6142621274
commit cf8fc3d6bf
74 changed files with 147 additions and 2744 deletions

View file

@ -0,0 +1,4 @@
<Application
x:Class="TR1X_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,22 @@
using System.Windows;
using TR1X_Installer.Installers;
using TRX_InstallerLib.Controls;
using TRX_InstallerLib.Installers;
namespace TR1X_Installer;
public partial class App : Application
{
public App()
{
Current.MainWindow = new TRXInstallWindow(new List<IInstallSource>
{
new SteamInstallSource(),
new GOGInstallSource(),
new TombATIInstallSource(),
new CDRomInstallSource(),
new TR1XInstallSource(),
});
Current.MainWindow.Show();
}
}

View file

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Utils;
namespace Installer.Installers;
namespace TR1X_Installer.Installers;
public class CDRomInstallSource : BaseInstallSource
{
@ -47,7 +46,7 @@ public class CDRomInstallSource : BaseInstallSource
return true;
}
public override bool IsDownloadingUnfinishedBusinessNeeded(string sourceDirectory)
public override bool IsDownloadingExpansionNeeded(string sourceDirectory)
{
return true;
}

View file

@ -1,15 +1,13 @@
using DiscUtils.Iso9660;
using DiscUtils.Streams;
using Installer.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Models;
using TRX_InstallerLib.Utils;
namespace Installer.Installers;
namespace TR1X_Installer.Installers;
public class GOGInstallSource : BaseInstallSource
{
@ -50,7 +48,7 @@ public class GOGInstallSource : BaseInstallSource
}
catch (Exception e)
{
throw new ApplicationException($"Could not read CUE {cuePath}:\n{e.Message}");
throw new ApplicationException(string.Format(Language.Instance.Controls!["progress_cue_failure"], cuePath, e.Message));
}
try
@ -60,7 +58,7 @@ public class GOGInstallSource : BaseInstallSource
}
catch (Exception e)
{
throw new ApplicationException($"Could not convert BIN to ISO: {e.Message}");
throw new ApplicationException(string.Format(Language.Instance.Controls!["progress_converting_bin_failure"], e.Message));
}
try
@ -73,14 +71,14 @@ public class GOGInstallSource : BaseInstallSource
{
MaximumValue = 1,
CurrentValue = 0,
Description = "Scanning the source directory",
Description = Language.Instance.Controls!["progress_scanning_source"],
});
var filesToExtract = GetFilesToExtract(reader.Root);
progress.Report(new InstallProgress
{
MaximumValue = filesToExtract.Count(),
CurrentValue = 0,
Description = "Preparing to extract the ISO",
Description = Language.Instance.Controls!["progress_preparing_extract"],
});
foreach (var path in filesToExtract)
{
@ -90,7 +88,7 @@ public class GOGInstallSource : BaseInstallSource
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
using SparseStream sourceStream = reader.OpenFile(path, FileMode.Open, FileAccess.Read);
var readAllByte = new Byte[sourceStream.Length];
var readAllByte = new byte[sourceStream.Length];
sourceStream.Read(readAllByte, 0, readAllByte.Length);
using FileStream targetStream = new(targetPath, FileMode.Create);
@ -102,13 +100,13 @@ public class GOGInstallSource : BaseInstallSource
{
MaximumValue = filesToExtract.Count(),
CurrentValue = ++currentProgress,
Description = $"Extracting {path}",
Description = string.Format(Language.Instance.Controls!["progress_extracting"], path)
});
}
}
catch (Exception e)
{
throw new ApplicationException($"Could not open converted ISO: {e.Message}");
throw new ApplicationException(string.Format(Language.Instance.Controls!["progress_converting_iso_failure"], e.Message));
}
File.Delete(isoPath);
@ -121,7 +119,7 @@ public class GOGInstallSource : BaseInstallSource
return true;
}
public override bool IsDownloadingUnfinishedBusinessNeeded(string sourceDirectory)
public override bool IsDownloadingExpansionNeeded(string sourceDirectory)
{
return true;
}

View file

@ -1,8 +1,7 @@
using Microsoft.Win32;
using System.Collections.Generic;
using System.IO;
namespace Installer.Installers;
namespace TR1X_Installer.Installers;
public class SteamInstallSource : GOGInstallSource
{

View file

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Utils;
namespace Installer.Installers;
namespace TR1X_Installer.Installers;
public class TR1XInstallSource : BaseInstallSource
{
@ -27,10 +26,7 @@ public class TR1XInstallSource : BaseInstallSource
public override string SuggestedInstallationDirectory
{
get
{
return InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory;
}
get => InstallUtils.GetPreviousInstallationPath() ?? base.SuggestedInstallationDirectory;
}
public override bool IsImportingSavesSupported => true;
@ -57,7 +53,7 @@ public class TR1XInstallSource : BaseInstallSource
return !Directory.Exists(Path.Combine(sourceDirectory, "music"));
}
public override bool IsDownloadingUnfinishedBusinessNeeded(string sourceDirectory)
public override bool IsDownloadingExpansionNeeded(string sourceDirectory)
{
return !File.Exists(Path.Combine(sourceDirectory, "data", "cat.phd"));
}

View file

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Utils;
namespace Installer.Installers;
namespace TR1X_Installer.Installers;
public class TombATIInstallSource : BaseInstallSource
{
@ -48,7 +47,7 @@ public class TombATIInstallSource : BaseInstallSource
return !Directory.Exists(Path.Combine(sourceDirectory, "music"));
}
public override bool IsDownloadingUnfinishedBusinessNeeded(string sourceDirectory)
public override bool IsDownloadingExpansionNeeded(string sourceDirectory)
{
return !File.Exists(Path.Combine(sourceDirectory, "data", "cat.phd"));
}

View file

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

@ -0,0 +1,12 @@
{
"Controls": {
"window_title_main": "TR1X Installer",
"step_source_content": "TR1X 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 TR1X.",
"step_settings_music_content": "Neither the Steam nor GOG versions of the game ship with the full soundtrack found on the PlayStation or Saturn retail releases. This option lets you download the missing tracks automatically (164 MB). The legality of these files is disputable; the most legal way to import the music to PC is to rip the audio tracks yourself from a physical PlayStation or Saturn disc.",
"step_settings_expansion_heading": "Download Unfinished Business expansion pack",
"step_settings_expansion_content": "The Unfinished Business expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (6 MB).",
"step_settings_expansion_music": "Fan-made edition (includes music triggers)",
"step_settings_expansion_vanilla": "Original edition (does not include music triggers)",
"step_settings_saves_content": "Imports existing savegame files. Only TombATI and TR1X savegame format is supported at this time."
}
}

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

@ -0,0 +1,4 @@
{
"Game": "TR1X",
"AllowExpansionTypeSelection": true
}

View file

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -0,0 +1,56 @@
<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>TR1X_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>
<PackageReference Include="DiscUtils.Iso9660" Version="0.16.13" />
<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\TombATI.png" />
<None Remove="Resources\TR1X.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\TombATI.png" />
<Resource Include="Resources\TR1X.png" />
<EmbeddedResource Include="Resources\const.json" />
<EmbeddedResource Include="Resources\Lang\en.json" />
<EmbeddedResource Include="Resources\release.zip" />
</ItemGroup>
</Project>

View file

@ -5,6 +5,8 @@ VisualStudioVersion = 17.11.35219.272
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TRX_InstallerLib", "TRX_InstallerLib\TRX_InstallerLib.csproj", "{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TR1X_Installer", "TR1X_Installer\TR1X_Installer.csproj", "{5B32640D-3997-472F-A1BA-FCE4128E0688}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Release|Any CPU.Build.0 = Release|Any CPU
{5B32640D-3997-472F-A1BA-FCE4128E0688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,5 +1,4 @@
using Installer.Models;
using System.Windows;
using System.Windows;
using TRX_InstallerLib.Models;
using WC = System.Windows.Controls;

View file

@ -1,4 +1,3 @@
using Installer.Models;
using TRX_InstallerLib.Installers;
using TRX_InstallerLib.Utils;

View file

@ -1,4 +1,4 @@
namespace Installer.Models;
namespace TRX_InstallerLib.Models;
public class LogEventArgs
{

View file

@ -6,21 +6,23 @@
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\const.json" />
<None Remove="Resources\Lang\en.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\const.json" />
<EmbeddedResource Include="Resources\Lang\en.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\styles.xaml">
<Generator>MSBuild:Compile</Generator>
</Resource>
<EmbeddedResource Include="Resources\const.json" />
<EmbeddedResource Include="Resources\Lang\en.json" />
</ItemGroup>
</Project>

View file

@ -2,11 +2,11 @@
set -x
set -e
cd /app/tools/tr1/installer/
cd /app/tools/installer/
export DOTNET_CLI_HOME="/tmp/DOTNET_CLI_HOME"
shopt -s globstar
rm -rf **/bin **/obj **/out/*
dotnet restore
dotnet publish -c Release -o out
dotnet publish TR1X_Installer -c Release -o out

View file

@ -1,3 +0,0 @@
bin/
obj/
out/

View file

@ -1,8 +0,0 @@
<Application x:Class="Installer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Controls/MainWindow.xaml">
<Application.Resources>
<ResourceDictionary />
</Application.Resources>
</Application>

View file

@ -1,12 +0,0 @@
using System.Windows;
namespace Installer
{
public partial class App : Application
{
public App()
{
InitializeComponent();
}
}
}

View file

@ -1,34 +0,0 @@
<UserControl
x:Class="Installer.Controls.FinishStepControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="clr-namespace:Installer.Models" d:DataContext="{d:DesignInstance Type=models:FinishStep}"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical">
<TextBlock TextWrapping="Wrap" VerticalAlignment="Center" Margin="0,0,0,12" Style="{StaticResource heading}">
Step 4: Done
</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="0,0,0,12" TextWrapping="Wrap">
Installation complete. To configure more advanced features, you can edit the JSON files in the cfg/ directory with a text editor.
</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="0,0,0,12">
Happy raiding :)
</TextBlock>
<CheckBox IsChecked="{Binding FinishSettings.OpenGameDirectory}" Content="Open game directory after closing this window" Margin="0,0,0,12" />
<CheckBox IsChecked="{Binding FinishSettings.LaunchGame}" Content="Launch the game after closing this window" Margin="0,0,0,12" />
</StackPanel>
</UserControl>

View file

@ -1,11 +0,0 @@
using System.Windows.Controls;
namespace Installer.Controls;
public partial class FinishStepControl : UserControl
{
public FinishStepControl()
{
InitializeComponent();
}
}

View file

@ -1,153 +0,0 @@
<UserControl
x:Class="Installer.Controls.InstallSettingsStepControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Installer.Models"
xmlns:utils="clr-namespace:Installer.Utils"
d:DataContext="{d:DesignInstance Type=models:InstallSettingsStep}"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap" VerticalAlignment="Center" Margin="0,0,0,12" Style="{StaticResource heading}">
Step 2: Installation options
</TextBlock>
<ScrollViewer Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Auto">
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid x:Name="Grid" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollContentPresenter
Grid.Row="0"
Grid.Column="0"
x:Name="PART_ScrollContentPresenter"
CanContentScroll="{TemplateBinding CanContentScroll}"
CanHorizontallyScroll="False"
CanVerticallyScroll="False"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}" />
<Rectangle
Grid.Row="1"
Grid.Column="1"
x:Name="Corner"
Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
<ScrollBar
Grid.Row="0"
Grid.Column="1"
x:Name="PART_VerticalScrollBar"
AutomationProperties.AutomationId="VerticalScrollBar"
Cursor="Arrow"
Minimum="0"
Maximum="{TemplateBinding ScrollableHeight}"
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
ViewportSize="{TemplateBinding ViewportHeight}"
Margin="12,0,0,0" />
<ScrollBar
Grid.Row="1"
Grid.Column="0"
x:Name="PART_HorizontalScrollBar"
AutomationProperties.AutomationId="HorizontalScrollBar"
Orientation="Horizontal"
Cursor="Arrow"
Minimum="0"
Maximum="{TemplateBinding ScrollableWidth}"
Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
ViewportSize="{TemplateBinding ViewportWidth}" />
</Grid>
</ControlTemplate>
</ScrollViewer.Template>
<StackPanel Orientation="Vertical">
<CheckBox VerticalAlignment="Center" Margin="0,0,0,12" IsChecked="{Binding InstallSettings.DownloadMusic}" IsEnabled="{Binding InstallSettings.IsDownloadingMusicNeeded}">
<TextBlock TextWrapping="Wrap">
Download music tracks
<Run Foreground="ForestGreen" Text="{Binding InstallSettings.IsDownloadingMusicNeeded, Converter={utils:ConditionalMarkupConverter TrueValue='', FalseValue='(already found)'}, Mode=OneWay}" />
<LineBreak />
<Run Style="{StaticResource small}">
Neither the Steam nor GOG versions of the game ship with the
full soundtrack found on the PlayStation or Saturn retail
releases. This option lets you download the missing tracks
automatically (164 MB). The legality of these files is
disputable; the most legal way to import the music to PC is to
rip the audio tracks yourself from a physical PlayStation or
Saturn disc.
</Run>
</TextBlock>
</CheckBox>
<CheckBox VerticalAlignment="Center" Margin="0,0,0,6" IsChecked="{Binding InstallSettings.DownloadUnfinishedBusiness}" IsEnabled="{Binding InstallSettings.IsDownloadingUnfinishedBusinessNeeded}">
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<utils:ComparisonConverter x:Key="ComparisonConverter" />
</StackPanel.Resources>
<TextBlock TextWrapping="Wrap" Margin="0,0,0,6">
Download Unfinished Business expansion pack
<Run Foreground="ForestGreen" Text="{Binding InstallSettings.IsDownloadingUnfinishedBusinessNeeded, Converter={utils:ConditionalMarkupConverter TrueValue='', FalseValue='(already found)'}, Mode=OneWay}" />
<LineBreak />
<Run Style="{StaticResource small}">
The Unfinished Business expansion pack was made freeware. However, the Steam and GOG versions do not ship it. This option lets you download the expansion files automatically (6 MB).
</Run>
</TextBlock>
<RadioButton IsEnabled="{Binding InstallSettings.DownloadUnfinishedBusiness}" Style="{StaticResource small}" Margin="0,0,0,6" Content="Fan-made edition (includes music triggers)"
IsChecked="{Binding Path=InstallSettings.UnfinishedBusinessType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static models:UBPackType.Music}}"/>
<RadioButton IsEnabled="{Binding InstallSettings.DownloadUnfinishedBusiness}" Style="{StaticResource small}" Margin="0,0,0,6" Content="Original edition (does not include music triggers)"
IsChecked="{Binding Path=InstallSettings.UnfinishedBusinessType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static models:UBPackType.Vanilla}}"/>
</StackPanel>
</CheckBox>
<CheckBox VerticalAlignment="Center" Margin="0,0,0,12" IsChecked="{Binding InstallSettings.ImportSaves}" IsEnabled="{Binding InstallSettings.InstallSource.IsImportingSavesSupported}">
<TextBlock TextWrapping="Wrap">
Import saves
<LineBreak />
<Run Style="{StaticResource small}">
Imports existing savegame files. Only TombATI and TR1X savegame format is supported at this time.
</Run>
</TextBlock>
</CheckBox>
<CheckBox VerticalAlignment="Center" Margin="0,0,0,12" IsChecked="{Binding InstallSettings.CreateDesktopShortcut}">
Create desktop shortcut
</CheckBox>
<Separator />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" VerticalAlignment="Center" Margin="0,0,12,0" Padding="0" Content="Destination folder:" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{Binding InstallSettings.TargetDirectory}" TextTrimming="CharacterEllipsis" />
<Button Grid.Column="2" VerticalAlignment="Center" Margin="12,0,0,0" Command="{Binding ChooseLocationCommand}" Content="Change..." />
</Grid>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View file

@ -1,11 +0,0 @@
using System.Windows.Controls;
namespace Installer.Controls;
public partial class InstallSettingsStepControl : UserControl
{
public InstallSettingsStepControl()
{
InitializeComponent();
}
}

View file

@ -1,65 +0,0 @@
<UserControl
x:Class="Installer.Controls.InstallSourceControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Installer.Models"
xmlns:utils="clr-namespace:Installer.Utils"
d:DataContext="{d:DesignInstance Type=models:InstallSourceViewModel}"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
<utils:BoolToVisibilityConverter
x:Key="BoolToVisibleConverter"
FalseValue="Hidden"
TrueValue="Visible" />
<utils:BoolToVisibilityConverter
x:Key="BoolToHiddenConverter"
TrueValue="Hidden"
FalseValue="Visible" />
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Margin="0,0,12,0">
<Image Source="{Binding InstallSource.ImageSource}" Height="{Binding RelativeSource={RelativeSource AncestorType=Border}, Path=ActualWidth}" VerticalAlignment="Center" />
</Border>
<Grid Grid.Row="0" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Padding="0" Margin="0,-3,12,3" VerticalAlignment="Top" Style="{StaticResource subHeading}" Text="{Binding InstallSource.SourceName}" Height="16" />
<TextBlock Grid.Column="1" Padding="0" Margin="0,-3,0,3" HorizontalAlignment="Right" VerticalAlignment="Top" Style="{StaticResource subHeadingFound}" Visibility="{Binding IsAvailable, Converter={StaticResource BoolToVisibleConverter}}" Text="Found" />
<TextBlock Grid.Column="1" Padding="0" Margin="0,-3,0,3" HorizontalAlignment="Right" VerticalAlignment="Top" Style="{StaticResource subHeadingNotFound}" Visibility="{Binding IsAvailable, Converter={StaticResource BoolToHiddenConverter}}" Text="Not found" />
</Grid>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="{Binding SourceDirectory, TargetNullValue='(no folder selected)'}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="6,0,0,0">
<Hyperlink Command="{Binding ChooseLocationCommand}" >(change)</Hyperlink>
</TextBlock>
</Grid>
</Grid>
</UserControl>

View file

@ -1,11 +0,0 @@
using System.Windows.Controls;
namespace Installer.Controls;
public partial class InstallSourceControl : UserControl
{
public InstallSourceControl()
{
InitializeComponent();
}
}

View file

@ -1,44 +0,0 @@
<UserControl
x:Class="Installer.Controls.InstallStepControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Installer.Models"
d:DataContext="{d:DesignInstance Type=models:InstallStep}"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical">
<TextBlock TextWrapping="Wrap" VerticalAlignment="Center" Margin="0,0,0,12" Style="{StaticResource heading}">
Step 3: Installing
</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="0,0,0,12" Text="{Binding Description}" />
<ProgressBar VerticalAlignment="Center" Margin="0,0,0,12" Value="{Binding CurrentProgress}" Maximum="{Binding MaximumProgress}" MinHeight="16" />
</StackPanel>
<TextBox
x:Name="logTextBox"
Grid.Row="1"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
AcceptsReturn="True"
IsReadOnly="True"/>
</Grid>
</UserControl>

View file

@ -1,41 +0,0 @@
using Installer.Models;
using System;
using System.Windows;
using System.Windows.Controls;
namespace Installer.Controls;
public partial class InstallStepControl : UserControl
{
public InstallStepControl()
{
InitializeComponent();
DataContextChanged += (object sender, DependencyPropertyChangedEventArgs e) =>
{
var dataContext = DataContext as InstallStep;
if (dataContext is not null)
{
string? lastMessage = null;
dataContext.Logger.LogEvent += (object sender, LogEventArgs e) =>
{
if (e.Message != lastMessage)
{
lastMessage = e.Message;
AppendMessage(e.Message);
}
};
}
};
}
private void AppendMessage(string message)
{
logTextBox.Dispatcher.Invoke(() =>
{
logTextBox.AppendText(message + Environment.NewLine);
logTextBox.Focus();
logTextBox.CaretIndex = logTextBox.Text.Length;
logTextBox.ScrollToEnd();
});
}
}

View file

@ -1,96 +0,0 @@
<Window
x:Class="Installer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Installer.Controls"
xmlns:models="clr-namespace:Installer.Models"
xmlns:utils="clr-namespace:Installer.Utils"
d:DataContext="{d:DesignInstance Type=models:MainWindowViewModel}"
mc:Ignorable="d"
Title="TR1X Installer"
MinWidth="480"
MinHeight="360"
Width="{Binding WindowWidth, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged, FallbackValue=640}"
Height="500"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
<utils:BoolToVisibilityConverter
x:Key="BoolToHiddenConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<utils:BoolToVisibilityConverter
x:Key="BoolToVisibleConverter"
FalseValue="Collapsed"
TrueValue="Visible" />
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image
Grid.Row="0"
Grid.Column="0"
Source="{Binding CurrentStep.SidebarImage}"
Visibility="{Binding IsSidebarVisible, Converter={StaticResource BoolToVisibleConverter}}" />
<Border Grid.Row="0" Grid.Column="1">
<ContentControl Content="{Binding CurrentStep}" Margin="12,0,12,12">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type models:SourceStep}">
<controls:SourceStepControl />
</DataTemplate>
<DataTemplate DataType="{x:Type models:InstallSettingsStep}">
<controls:InstallSettingsStepControl />
</DataTemplate>
<DataTemplate DataType="{x:Type models:InstallStep}">
<controls:InstallStepControl />
</DataTemplate>
<DataTemplate DataType="{x:Type models:FinishStep}">
<controls:FinishStepControl />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Border>
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource ButtonStyle}">
<Setter Property="Margin" Value="12,0,0,0" />
</Style>
</Grid.Resources>
<StackPanel Margin="12" Orientation="Horizontal" Grid.Column="1">
<Button
Command="{Binding GoToPreviousStepCommand}"
Visibility="{Binding IsFinalStep, Converter={StaticResource BoolToHiddenConverter}}"
Content="_Back" />
<Button
Command="{Binding GoToNextStepCommand}"
Visibility="{Binding IsFinalStep, Converter={StaticResource BoolToHiddenConverter}}"
Content="_Next" />
<Button
Command="{Binding CloseWindowCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
Content="{Binding IsFinalStep, Converter={utils:ConditionalMarkupConverter TrueValue='_Close', FalseValue='_Cancel'}}" />
</StackPanel>
</Grid>
</Grid>
</Window>

View file

@ -1,13 +0,0 @@
using Installer.Models;
using System.Windows;
namespace Installer;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}

View file

@ -1,65 +0,0 @@
<UserControl
x:Class="Installer.Controls.SourceStepControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Installer.Controls"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="30" />
<RowDefinition Height="Auto" MinHeight="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" TextWrapping="Wrap" VerticalAlignment="Center" Margin="0,0,0,12" Style="{StaticResource heading}">
Step 1: Choose installation source
</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" TextWrapping="Wrap" VerticalAlignment="Center" Margin="0,0,0,10">
TR1X requires original game files to run.
<LineBreak />
Please choose the source location where to install the data files from.
<LineBreak />
If you're upgrading an existing installation, please choose TR1X.
</TextBlock>
<ListView
BorderThickness="0"
Grid.Row="2"
Grid.Column="0"
ItemsSource="{Binding InstallationSources}"
SelectedItem="{Binding SelectedInstallationSource, Mode=TwoWay}"
VerticalContentAlignment="Top"
HorizontalContentAlignment="Stretch"
ScrollViewer.CanContentScroll="False"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Padding" Value="6" />
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<controls:InstallSourceControl />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</UserControl>

View file

@ -1,11 +0,0 @@
using System.Windows.Controls;
namespace Installer.Controls;
public partial class SourceStepControl : UserControl
{
public SourceStepControl()
{
InitializeComponent();
}
}

View file

@ -1,92 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<AssemblyName>TR1X_Installer</AssemblyName>
<LangVersion>10.0</LangVersion>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<EnableDefaultPageItems>false</EnableDefaultPageItems>
<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>
<Compile Include="**/*.cs" Exclude="bin/**/*.cs;obj/**/*.cs;**/*.xaml.cs" />
<Page Update="**/*.xaml">
<SubType>Designer</SubType>
</Page>
<Compile Include="**/*.xaml.cs" Exclude="bin/**/*.xaml.cs;obj/**/*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<None Remove="release.zip" />
<None Remove="Resources\CDRom.png" />
<None Remove="Resources\GOG.png" />
<None Remove="Resources\side1.jpg" />
<None Remove="Resources\side2.jpg" />
<None Remove="Resources\side3.jpg" />
<None Remove="Resources\side4.jpg" />
<None Remove="Resources\Steam.png" />
<None Remove="Resources\Styles.xaml" />
<None Remove="Resources\TR1X.png" />
<None Remove="Resources\TombATI.png" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\icon.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\CDRom.png" />
<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" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\GOG.png" />
<EmbeddedResource Include="Resources\release.zip" />
<Resource Include="Resources\TR1X.png" />
<Resource Include="Resources\TombATI.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Iso9660" Version="0.16.13" />
</ItemGroup>
<ItemGroup>
<Page Include="Controls\FinishStepControl.xaml" />
<Page Include="Controls\InstallSourceControl.xaml" />
<Page Include="Controls\InstallStepControl.xaml" />
<Page Include="Controls\MainWindow.xaml" />
<Page Include="Controls\SourceStepControl.xaml" />
<Page Include="Controls\InstallSettingsStepControl.xaml" />
<Page Include="Resources\Styles.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<KnownFrameworkReference Update="Microsoft.WindowsDesktop.App" IsWindowsOnly="false" />
<KnownFrameworkReference Update="Microsoft.WindowsDesktop.App.WPF" IsWindowsOnly="false" />
<KnownFrameworkReference Update="Microsoft.WindowsDesktop.App.WindowsForms" IsWindowsOnly="false" />
</ItemGroup>
</Project>

View file

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Installer.Installers;
public abstract class BaseInstallSource : IInstallSource
{
public abstract IEnumerable<string> DirectoriesToTry { get; }
public virtual string ImageSource
{
get
{
return $"pack://application:,,,/TR1X_Installer;component/Resources/{SourceName}.png";
}
}
public abstract bool IsImportingSavesSupported { get; }
public abstract string SourceName { get; }
public virtual string SuggestedInstallationDirectory
{
get
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "TR1X");
}
}
public abstract Task CopyOriginalGameFiles(
string sourceDirectory,
string targetDirectory,
IProgress<InstallProgress> progress,
bool importSaves
);
public abstract bool IsDownloadingMusicNeeded(string sourceDirectory);
public abstract bool IsDownloadingUnfinishedBusinessNeeded(string sourceDirectory);
public abstract bool IsGameFound(string sourceDirectory);
}

View file

@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Installer.Installers;
public interface IInstallSource
{
public IEnumerable<string> DirectoriesToTry { get; }
public string ImageSource { get; }
public string SourceName { get; }
public string SuggestedInstallationDirectory { get; }
public Task CopyOriginalGameFiles(
string sourceDirectory,
string targetDirectory,
IProgress<InstallProgress> progress,
bool importSaves
);
bool IsDownloadingMusicNeeded(string sourceDirectory);
bool IsDownloadingUnfinishedBusinessNeeded(string sourceDirectory);
public bool IsGameFound(string sourceDirectory);
bool IsImportingSavesSupported { get; }
};

View file

@ -1,106 +0,0 @@
using Installer.Models;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Installer.Installers;
public class InstallExecutor
{
private static readonly string _resourceBaseURL = "https://lostartefacts.dev/aux/tr1x";
public InstallExecutor(InstallSettings settings)
{
_settings = settings;
}
public IInstallSource? InstallSource
{
get
{
return _settings.InstallSource;
}
}
public async Task ExecuteInstall(IProgress<InstallProgress> progress)
{
if (_settings.SourceDirectory is null)
{
throw new NullReferenceException();
}
if (_settings.TargetDirectory is null)
{
throw new NullReferenceException();
}
await CopyOriginalGameFiles(_settings.SourceDirectory, _settings.TargetDirectory, progress);
await CopyTR1XFiles(_settings.TargetDirectory, progress);
if (_settings.DownloadMusic)
{
await DownloadMusicFiles(_settings.TargetDirectory, progress);
}
if (_settings.DownloadUnfinishedBusiness)
{
await DownloadUnfinishedBusinessFiles(_settings.TargetDirectory, _settings.UnfinishedBusinessType, progress);
}
if (_settings.CreateDesktopShortcut)
{
CreateDesktopShortcut(_settings.TargetDirectory);
}
progress.Report(new InstallProgress { Description = "Finished", Finished = true });
}
protected async Task CopyOriginalGameFiles(string sourceDirectory, string targetDirectory, IProgress<InstallProgress> progress)
{
if (_settings.InstallSource is null)
{
throw new NullReferenceException();
}
await _settings.InstallSource.CopyOriginalGameFiles(sourceDirectory, targetDirectory, progress, _settings.ImportSaves);
}
protected static async Task CopyTR1XFiles(string targetDirectory, IProgress<InstallProgress> progress)
{
InstallUtils.StoreInstallationPath(targetDirectory);
progress.Report(new InstallProgress
{
CurrentValue = 0,
MaximumValue = 1,
Description = "Opening embedded ZIP",
});
var assembly = Assembly.GetExecutingAssembly();
var resourceName = assembly.GetManifestResourceNames().Where(n => n.EndsWith("release.zip")).First();
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new ApplicationException($"Could not open embedded ZIP.");
await InstallUtils.ExtractZip(stream, targetDirectory, progress, overwrite: true);
}
protected static void CreateDesktopShortcut(string targetDirectory)
{
InstallUtils.CreateDesktopShortcut("TR1X", Path.Combine(targetDirectory, "TR1X.exe"));
if (File.Exists(Path.Combine(targetDirectory, "data", "cat.phd")))
{
InstallUtils.CreateDesktopShortcut("TR1X - UB", Path.Combine(targetDirectory, "TR1X.exe"), new[] { "-gold" });
}
}
protected static async Task DownloadMusicFiles(string targetDirectory, IProgress<InstallProgress> progress)
{
await InstallUtils.DownloadZip($"{_resourceBaseURL}/music.zip", targetDirectory, progress);
}
protected static async Task DownloadUnfinishedBusinessFiles(string targetDirectory, UBPackType type, IProgress<InstallProgress> progress)
{
await InstallUtils.DownloadZip(
$"{_resourceBaseURL}/trub-{type.ToString().ToLower()}.zip",
targetDirectory, progress);
}
private readonly InstallSettings _settings;
}

View file

@ -1,9 +0,0 @@
namespace Installer.Installers;
public class InstallProgress
{
public int? CurrentValue { get; set; }
public string? Description { get; set; }
public bool Finished { get; set; }
public int? MaximumValue { get; set; }
}

View file

@ -1,235 +0,0 @@
using Installer.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Installer.Installers;
public static class InstallUtils
{
private static readonly string _legacyStorageKey = @"Software\Tomb1Main";
private static readonly string _registryStorageKey = @"Software\TR1X";
public static async Task CopyDirectoryTree(
string sourceDirectory,
string targetDirectory,
IProgress<InstallProgress> progress,
Func<string, bool>? filterCallback = null,
Func<string, bool>? overwriteCallback = null
)
{
try
{
progress.Report(new InstallProgress { Description = "Scanning directory" });
var files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);
var currentProgress = 0;
var maximumProgress = files.Length;
foreach (var sourcePath in files)
{
if (filterCallback is not null && !filterCallback(sourcePath))
{
continue;
}
var relPath = Path.GetRelativePath(sourceDirectory, sourcePath);
var targetPath = Path.Combine(targetDirectory, relPath);
var isSamePath = string.Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(targetPath), StringComparison.OrdinalIgnoreCase);
if (!File.Exists(targetPath) || (overwriteCallback is not null && overwriteCallback(sourcePath)) && !isSamePath)
{
progress.Report(new InstallProgress
{
CurrentValue = currentProgress,
MaximumValue = maximumProgress,
Description = $"Copying {relPath}",
});
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
await Task.Run(() => File.Copy(sourcePath, targetPath, true));
}
else
{
progress.Report(new InstallProgress
{
CurrentValue = currentProgress,
MaximumValue = maximumProgress,
Description = $"Copying {relPath} - skipped",
});
}
currentProgress++;
}
}
catch (Exception e)
{
throw new ApplicationException($"Could not extract ZIP:\n{e.Message}");
}
}
public static void CreateDesktopShortcut(string name, string targetPath, string[]? args = null)
{
var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), $"{name}.lnk");
ShortcutUtils.CreateShortcut(shortcutPath, targetPath, "Tomb Raider I: Community Edition", args);
}
public static async Task<byte[]> DownloadFile(string url, IProgress<InstallProgress> progress)
{
HttpProgressClient wc = new();
progress.Report(new InstallProgress { Description = $"Initializing download of {url}" });
wc.DownloadProgressChanged += (totalBytesToReceive, bytesReceived) =>
{
progress.Report(new InstallProgress
{
CurrentValue = (int)bytesReceived,
MaximumValue = (int)totalBytesToReceive,
Description = $"Downloading {url}",
});
};
return await wc.DownloadDataTaskAsync(new Uri(url));
}
public static async Task DownloadZip(
string url,
string targetDirectory,
IProgress<InstallProgress> progress
)
{
var response = await DownloadFile(url, progress);
using var stream = new MemoryStream(response);
await ExtractZip(stream, targetDirectory, progress);
}
public static async Task ExtractZip(
Stream stream,
string targetDirectory,
IProgress<InstallProgress> progress,
Func<string, bool>? filterCallback = null,
bool overwrite = false
)
{
try
{
using var zip = new ZipArchive(stream);
progress.Report(new InstallProgress
{
Description = "Scanning embedded ZIP",
});
var currentProgress = 0;
var maximumProgress = zip.Entries.Count;
foreach (var entry in zip.Entries)
{
if (new Regex(@"[\\/]$").IsMatch(entry.FullName))
{
continue;
}
if (filterCallback is not null && !filterCallback(entry.FullName))
{
continue;
}
var targetPath = Path.Combine(
targetDirectory,
new Regex(@"[\\/]").Replace(entry.FullName, Path.DirectorySeparatorChar.ToString()));
if (!File.Exists(targetPath) || overwrite)
{
progress.Report(new InstallProgress
{
CurrentValue = currentProgress,
MaximumValue = maximumProgress,
Description = $"Extracting {entry.FullName}",
});
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
await Task.Run(() => entry.ExtractToFile(targetPath, true));
}
else
{
progress.Report(new InstallProgress
{
CurrentValue = currentProgress,
MaximumValue = maximumProgress,
Description = $"Extracting {entry.FullName} - skipped",
});
}
currentProgress++;
}
}
catch (Exception e)
{
throw new ApplicationException($"Could not extract ZIP:\n{e.Message}");
}
}
public static IEnumerable<string> GetDesktopShortcutDirectories()
{
foreach (
var shortcutPath in Directory.EnumerateFiles(
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "*.lnk"
)
)
{
string? lnkPath;
try
{
lnkPath = ShortcutUtils.GetLnkTargetPath(shortcutPath);
}
catch (Exception)
{
continue;
}
if (lnkPath is not null)
{
var dirName = Path.GetDirectoryName(lnkPath);
if (dirName is not null)
{
yield return dirName;
}
}
}
}
public static void StoreInstallationPath(string installPath)
{
RenameLegacyStorage();
using var key = Registry.CurrentUser.CreateSubKey(_registryStorageKey);
key?.SetValue("InstallPath", installPath);
}
public static string? GetPreviousInstallationPath()
{
RenameLegacyStorage();
using var key = Registry.CurrentUser.OpenSubKey(_registryStorageKey);
return key?.GetValue("InstallPath")?.ToString();
}
private static void RenameLegacyStorage()
{
// Added in #1411 - to be removed in the future.
using var legacyKey = Registry.CurrentUser.OpenSubKey(_legacyStorageKey);
if (legacyKey is null)
{
return;
}
using var currentKey = Registry.CurrentUser.OpenSubKey(_registryStorageKey);
if (currentKey is not null)
{
return;
}
using var destinationKey = Registry.CurrentUser.CreateSubKey(_registryStorageKey);
foreach (string valueName in legacyKey.GetValueNames())
{
object? objValue = legacyKey.GetValue(valueName);
if (objValue is not null)
{
RegistryValueKind valueKind = legacyKey.GetValueKind(valueName);
destinationKey.SetValue(valueName, objValue, valueKind);
}
}
Registry.CurrentUser.DeleteSubKey(_legacyStorageKey);
}
}

View file

@ -1,33 +0,0 @@
namespace Installer.Models;
public class FinishSettings : BaseNotifyPropertyChanged
{
public bool LaunchGame
{
get => _launchGame;
set
{
if (value != _launchGame)
{
_launchGame = value;
NotifyPropertyChanged();
}
}
}
public bool OpenGameDirectory
{
get => _openGameDirectory;
set
{
if (value != _openGameDirectory)
{
_openGameDirectory = value;
NotifyPropertyChanged();
}
}
}
private bool _launchGame = false;
private bool _openGameDirectory = true;
}

View file

@ -1,14 +0,0 @@
namespace Installer.Models;
public class FinishStep : BaseNotifyPropertyChanged, IStep
{
public FinishStep(FinishSettings finishSettings)
{
FinishSettings = finishSettings;
}
public bool CanProceedToNextStep => false;
public bool CanProceedToPreviousStep => false;
public FinishSettings FinishSettings { get; }
public string SidebarImage => "pack://application:,,,/TR1X_Installer;component/Resources/side4.jpg";
}

View file

@ -1,10 +0,0 @@
using System.ComponentModel;
namespace Installer.Models;
public interface IStep : INotifyPropertyChanged
{
bool CanProceedToNextStep { get; }
bool CanProceedToPreviousStep { get; }
string SidebarImage { get; }
}

View file

@ -1,139 +0,0 @@
using Installer.Installers;
namespace Installer.Models;
public class InstallSettings : BaseNotifyPropertyChanged
{
public bool CreateDesktopShortcut
{
get => _createDesktopShortcut;
set
{
if (value != _createDesktopShortcut)
{
_createDesktopShortcut = value;
NotifyPropertyChanged();
}
}
}
public bool DownloadMusic
{
get => _downloadMusic;
set
{
if (value != _downloadMusic)
{
_downloadMusic = value;
NotifyPropertyChanged();
}
}
}
public bool DownloadUnfinishedBusiness
{
get => _downloadUnfinishedBusiness;
set
{
if (value != _downloadUnfinishedBusiness)
{
_downloadUnfinishedBusiness = value;
NotifyPropertyChanged();
}
}
}
public UBPackType UnfinishedBusinessType
{
get => _unfinishedBusinessType;
set
{
if (value != _unfinishedBusinessType)
{
_unfinishedBusinessType = value;
NotifyPropertyChanged();
}
}
}
public bool ImportSaves
{
get => _importSaves;
set
{
if (value != _importSaves)
{
_importSaves = value;
NotifyPropertyChanged();
}
}
}
public IInstallSource? InstallSource
{
get => _installSource;
set
{
if (value != _installSource)
{
_installSource = value;
DownloadMusic = SourceDirectory is not null && (_installSource?.IsDownloadingMusicNeeded(SourceDirectory) ?? false);
DownloadUnfinishedBusiness = SourceDirectory is not null && (_installSource?.IsDownloadingUnfinishedBusinessNeeded(SourceDirectory) ?? false);
ImportSaves = _installSource?.IsImportingSavesSupported ?? false;
TargetDirectory = _installSource?.SuggestedInstallationDirectory;
NotifyPropertyChanged();
}
}
}
public bool IsDownloadingMusicNeeded
{
get
{
return SourceDirectory is not null && (InstallSource?.IsDownloadingMusicNeeded(SourceDirectory) ?? false);
}
}
public bool IsDownloadingUnfinishedBusinessNeeded
{
get
{
return SourceDirectory is not null && (InstallSource?.IsDownloadingUnfinishedBusinessNeeded(SourceDirectory) ?? false);
}
}
public string? SourceDirectory
{
get => _sourceDirectory;
set
{
if (value != _sourceDirectory)
{
_sourceDirectory = value;
NotifyPropertyChanged();
}
}
}
public string? TargetDirectory
{
get => _targetDirectory;
set
{
if (value != _targetDirectory)
{
_targetDirectory = value;
NotifyPropertyChanged();
}
}
}
private bool _createDesktopShortcut = true;
private bool _downloadMusic;
private bool _downloadUnfinishedBusiness;
private UBPackType _unfinishedBusinessType;
private bool _importSaves;
private IInstallSource? _installSource;
private string? _sourceDirectory;
private string? _targetDirectory;
}

View file

@ -1,40 +0,0 @@
using Installer.Utils;
using System.Windows.Input;
namespace Installer.Models;
public class InstallSettingsStep : BaseNotifyPropertyChanged, IStep
{
public InstallSettingsStep(InstallSettings installSettings)
{
InstallSettings = installSettings;
InstallSettings.PropertyChanged += (sender, e) =>
{
NotifyPropertyChanged(nameof(CanProceedToNextStep));
};
}
public bool CanProceedToNextStep => InstallSettings.TargetDirectory != null;
public bool CanProceedToPreviousStep => true;
public ICommand ChooseLocationCommand
{
get
{
return _chooseLocationCommand ??= new RelayCommand(ChooseLocation);
}
}
public InstallSettings InstallSettings { get; }
public string SidebarImage => "pack://application:,,,/TR1X_Installer;component/Resources/side2.jpg";
private RelayCommand? _chooseLocationCommand;
private void ChooseLocation()
{
var result = FileBrowser.Browse(InstallSettings.TargetDirectory);
if (result is not null)
{
InstallSettings.TargetDirectory = result;
}
}
}

View file

@ -1,66 +0,0 @@
using Installer.Installers;
using Installer.Utils;
using System.Windows.Input;
namespace Installer.Models;
public class InstallSourceViewModel : BaseNotifyPropertyChanged
{
public InstallSourceViewModel(IInstallSource source)
{
InstallSource = source;
foreach (var directory in source.DirectoriesToTry)
{
if (InstallSource.IsGameFound(directory))
{
SourceDirectory = directory;
break;
}
}
}
public ICommand ChooseLocationCommand
{
get
{
return _chooseLocationCommand ??= new RelayCommand(ChooseLocation);
}
}
public IInstallSource InstallSource { get; private set; }
public bool IsAvailable
{
get
{
return SourceDirectory != null && InstallSource.IsGameFound(SourceDirectory);
}
}
public string? SourceDirectory
{
get => _sourceDirectory;
set
{
if (value != _sourceDirectory)
{
_sourceDirectory = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsAvailable));
}
}
}
private RelayCommand? _chooseLocationCommand;
private string? _sourceDirectory;
private void ChooseLocation()
{
var result = FileBrowser.Browse(SourceDirectory);
if (result is not null)
{
SourceDirectory = result;
}
}
}

View file

@ -1,119 +0,0 @@
using Installer.Installers;
using System;
using System.Threading.Tasks;
namespace Installer.Models;
public class InstallStep : BaseNotifyPropertyChanged, IStep
{
public InstallStep(InstallSettings installSettings)
{
Logger = new Logger();
InstallSettings = installSettings;
}
public bool CanProceedToNextStep
{
get => _canProceedToNextStep;
set
{
if (value != _canProceedToNextStep)
{
_canProceedToNextStep = value;
NotifyPropertyChanged();
}
}
}
public bool CanProceedToPreviousStep => false;
public int CurrentProgress
{
get { return _currentProgress; }
set
{
if (value != _currentProgress)
{
_currentProgress = value;
NotifyPropertyChanged();
}
}
}
public string? Description
{
get => _description;
set
{
if (value != _description)
{
_description = value;
NotifyPropertyChanged();
}
}
}
public InstallSettings InstallSettings { get; }
public Logger Logger { get; }
public int MaximumProgress
{
get { return _maximumProgress; }
set
{
if (value != _maximumProgress)
{
_maximumProgress = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(CanProceedToNextStep));
}
}
}
public string SidebarImage => "pack://application:,,,/TR1X_Installer;component/Resources/side3.jpg";
public void RunInstall()
{
var progress = new Progress<InstallProgress>();
progress.ProgressChanged += (sender, progress) =>
{
if (progress.CurrentValue is not null && progress.MaximumValue is not null)
{
CurrentProgress = progress.CurrentValue.Value;
MaximumProgress = progress.MaximumValue.Value;
}
else
{
CurrentProgress = progress.Finished ? 1 : 0;
MaximumProgress = 1;
}
Description = progress.Description;
if (progress.Description is not null)
{
Logger.RaiseLogEvent(progress.Description);
}
if (progress.Finished)
{
CanProceedToNextStep = true;
}
};
Task.Run(async () =>
{
try
{
var executor = new InstallExecutor(InstallSettings);
await executor.ExecuteInstall(progress);
}
catch (Exception ex)
{
Logger.RaiseLogEvent(ex.ToString());
}
});
}
private bool _canProceedToNextStep;
private int _currentProgress = 0;
private string? _description;
private int _maximumProgress = 1;
}

View file

@ -1,23 +0,0 @@
namespace Installer.Models;
public class LogEventArgs
{
public LogEventArgs(string message)
{
Message = message;
}
public string Message { get; }
}
public class Logger
{
public delegate void LogEventHandler(object sender, LogEventArgs e);
public event LogEventHandler? LogEvent;
public void RaiseLogEvent(string message)
{
LogEvent?.Invoke(this, new LogEventArgs(message));
}
}

View file

@ -1,167 +0,0 @@
using Installer.Utils;
using System;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Input;
namespace Installer.Models;
public class MainWindowViewModel : BaseNotifyPropertyChanged
{
public MainWindowViewModel()
{
_sourceStep = new SourceStep();
_currentStep = _sourceStep;
_installSettings = new InstallSettings();
}
public ICommand CloseWindowCommand
{
get
{
return _closeWindowCommand ??= new RelayCommand<Window>(CloseWindow);
}
}
public IStep CurrentStep
{
get { return _currentStep; }
set
{
_currentStep = value;
_goToPreviousStepCommand?.RaiseCanExecuteChanged();
_goToNextStepCommand?.RaiseCanExecuteChanged();
_currentStep.PropertyChanged += (sender, e) =>
{
_goToPreviousStepCommand?.RaiseCanExecuteChanged();
_goToNextStepCommand?.RaiseCanExecuteChanged();
};
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(IsFinalStep));
}
}
public ICommand GoToNextStepCommand
{
get
{
return _goToNextStepCommand ??= new RelayCommand(GoToNextStep, CanGoToNextStep);
}
}
public ICommand GoToPreviousStepCommand
{
get
{
return _goToPreviousStepCommand ??= new RelayCommand(GoToPreviousStep, CanGoToPreviousStep);
}
}
public bool IsFinalStep
{
get
{
return CurrentStep is FinishStep;
}
}
public bool IsSidebarVisible
{
get
{
return WindowWidth >= 500;
}
}
public int WindowWidth
{
get => _windowWidth;
set
{
if (value != _windowWidth)
{
_windowWidth = value;
NotifyPropertyChanged(nameof(IsSidebarVisible));
}
}
}
private const bool _autoFinishInstallStep = false;
private RelayCommand<Window>? _closeWindowCommand;
private IStep _currentStep;
private FinishSettings? _finishSettings;
private RelayCommand? _goToNextStepCommand;
private RelayCommand? _goToPreviousStepCommand;
private readonly InstallSettings _installSettings;
private readonly IStep _sourceStep;
private int _windowWidth;
private bool CanGoToNextStep()
{
return CurrentStep.CanProceedToNextStep;
}
private bool CanGoToPreviousStep()
{
return CurrentStep.CanProceedToPreviousStep;
}
private void CloseWindow(Window? window)
{
if (_finishSettings is not null && _finishSettings.LaunchGame)
{
if (_installSettings.TargetDirectory is null)
{
throw new NullReferenceException();
}
Process.Start(Path.Combine(_installSettings.TargetDirectory, "TR1X.exe"));
}
if (_finishSettings is not null && _finishSettings.OpenGameDirectory)
{
if (_installSettings.TargetDirectory is null)
{
throw new NullReferenceException();
}
Process.Start("explorer.exe", _installSettings.TargetDirectory);
}
window?.Close();
}
private void GoToNextStep()
{
if (CurrentStep is SourceStep sourceStep)
{
var installSource = sourceStep.SelectedInstallationSource!.InstallSource;
_installSettings.InstallSource = installSource;
_installSettings.SourceDirectory = sourceStep.SelectedInstallationSource.SourceDirectory;
CurrentStep = new InstallSettingsStep(_installSettings);
}
else if (CurrentStep is InstallSettingsStep targetStep)
{
var installStep = new InstallStep(targetStep.InstallSettings);
installStep.RunInstall();
installStep.PropertyChanged += (sender, e) =>
{
if (_autoFinishInstallStep && installStep.CanProceedToNextStep)
{
_finishSettings = new FinishSettings();
CurrentStep = new FinishStep(_finishSettings);
}
};
CurrentStep = installStep;
}
else if (CurrentStep is InstallStep)
{
_finishSettings = new FinishSettings();
CurrentStep = new FinishStep(_finishSettings);
}
}
private void GoToPreviousStep()
{
CurrentStep = _sourceStep;
}
}

View file

@ -1,70 +0,0 @@
using Installer.Installers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Installer.Models;
public class SourceStep : BaseNotifyPropertyChanged, IStep
{
public SourceStep()
{
InstallationSources = new ObservableCollection<InstallSourceViewModel>
{
// NOTE: the order also decides which installation source will be selected by default
new(new SteamInstallSource()),
new(new GOGInstallSource()),
new(new TombATIInstallSource()),
new(new TR1XInstallSource()),
new(new CDRomInstallSource()),
};
foreach (var installationSource in InstallationSources)
{
installationSource.PropertyChanged += (sender, e) =>
{
NotifyPropertyChanged(nameof(InstallationSources));
if (installationSource == selectedInstallationSource)
{
NotifyPropertyChanged(nameof(SelectedInstallationSource));
}
};
}
foreach (var source in InstallationSources)
{
if (source.IsAvailable)
{
// TR1X comes last and always trumps any other installation source
SelectedInstallationSource = source;
}
}
}
public bool CanProceedToNextStep
{
get
{
return SelectedInstallationSource != null && SelectedInstallationSource.IsAvailable;
}
}
public bool CanProceedToPreviousStep => false;
public IEnumerable<InstallSourceViewModel> InstallationSources { get; private set; }
public InstallSourceViewModel? SelectedInstallationSource
{
get => selectedInstallationSource;
set
{
if (value != selectedInstallationSource)
{
selectedInstallationSource = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(CanProceedToNextStep));
}
}
}
public string SidebarImage => "pack://application:,,,/TR1X_Installer;component/Resources/side1.jpg";
private InstallSourceViewModel? selectedInstallationSource;
}

View file

@ -1,7 +0,0 @@
namespace Installer.Models;
public enum UBPackType
{
Music,
Vanilla,
}

View file

@ -1,31 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="{x:Type Button}" x:Key="ButtonStyle">
<Setter Property="Padding" Value="10,5" />
</Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource ButtonStyle}" />
<Style x:Key="heading" TargetType="TextBlock">
<Setter Property="FontSize" Value="20" />
</Style>
<Style x:Key="subHeading" TargetType="TextBlock">
<Setter Property="FontSize" Value="15" />
</Style>
<Style x:Key="subHeadingFound" TargetType="TextBlock" BasedOn="{StaticResource subHeading}">
<Setter Property="Foreground" Value="ForestGreen" />
</Style>
<Style x:Key="subHeadingNotFound" TargetType="TextBlock" BasedOn="{StaticResource subHeading}">
<Setter Property="Foreground" Value="Firebrick" />
</Style>
<Style x:Key="normal" TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
</Style>
<Style x:Key="small">
<Setter Property="Control.FontSize" Value="10" />
<Setter Property="Run.FontSize" Value="10" />
</Style>
<Style TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality" />
</Style>
</ResourceDictionary>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View file

@ -1,14 +0,0 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Installer;
public abstract class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View file

@ -1,30 +0,0 @@
using System.IO;
using System.Text;
namespace Installer.Utils;
public static class BinaryReaderExtensions
{
public static string ReadNullTerminatedString(this BinaryReader stream)
{
string str = "";
char ch;
while ((int)(ch = stream.ReadChar()) != 0)
{
str += ch;
}
return str;
}
public static string ReadSystemCodepageString(this BinaryReader stream)
{
var length = stream.ReadUInt16();
return Encoding.Default.GetString(stream.ReadBytes(length));
}
public static string ReadUtf16String(this BinaryReader stream)
{
var length = stream.ReadUInt16();
return Encoding.Unicode.GetString(stream.ReadBytes(length * 2));
}
}

View file

@ -1,29 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Installer.Utils;
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolToVisibilityConverter : IValueConverter
{
public BoolToVisibilityConverter()
{
FalseValue = Visibility.Hidden;
TrueValue = Visibility.Visible;
}
public Visibility FalseValue { get; set; }
public Visibility TrueValue { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View file

@ -1,18 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Installer.Utils;
public class ComparisonConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? parameter : Binding.DoNothing;
}
}

View file

@ -1,27 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
namespace Installer.Utils;
public sealed class ConditionalMarkupConverter : MarkupExtension, IValueConverter
{
public object FalseValue { get; set; } = new();
public object TrueValue { get; set; } = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is true ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}

View file

@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace Installer.Utils;
public class CueFile
{
public readonly List<CueTrack> TrackList = new();
public CueFile(string cueFilePath)
{
_cueFilePath = cueFilePath;
string cueFileContent;
using (TextReader cueReader = new StreamReader(cueFilePath))
{
cueFileContent = cueReader.ReadToEnd();
}
MatchCollection fileMatches = _fileGroupRegex.Matches(cueFileContent);
if (fileMatches.Count == 0)
{
throw new ApplicationException($"Could not parse {cueFilePath}: no tracks were found");
}
foreach (Match fileMatch in fileMatches.Cast<Match>())
{
var binFilePath = GetBinFilePath(fileMatch.Groups["name"].Value.Trim('"'));
var matches = _trackRegex.Matches(fileMatch.Groups["content"].Value);
if (matches.Count == 0)
{
throw new ApplicationException($"Could not parse {cueFilePath}: no tracks were found");
}
CueTrack? track = null;
CueTrack? prevTrack = null;
foreach (Match trackMatch in matches.Cast<Match>())
{
track = new CueTrack(
binFilePath,
int.Parse(trackMatch.Groups["track"].Value),
trackMatch.Groups["mode"].Value,
trackMatch.Groups["time"].Value);
if (prevTrack != null)
{
prevTrack.Stop = track.StartPosition - 1;
prevTrack.StopSector = track.StartSector;
}
TrackList.Add(track);
prevTrack = track;
}
if (track == null)
{
return;
}
track.Stop = GetBinFileLength(binFilePath);
track.StopSector = track.Stop / CueTrack.SectorLength;
}
}
private static readonly Regex _fileGroupRegex = new(
@"^file\s+(?<name>""[^""]+""|[^""\s]+)\s+(?<mode>\w+)\s+(?<content>(.(?!^file))*)",
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline);
private static readonly Regex _trackRegex = new(@"track\s+?(?<track>\d+?)\s+?(?<mode>\S+?)[\s$]+?index\s+?\d+?\s+?(?<time>\S*)",
RegexOptions.IgnoreCase | RegexOptions.Multiline);
private readonly string _cueFilePath;
private static long GetBinFileLength(string binFilePath)
{
FileInfo fileInfo = new(binFilePath);
return fileInfo.Length;
}
private string GetBinFilePath(string name)
{
var cueDirectory = Path.GetDirectoryName(_cueFilePath)!;
string result = Path.Combine(cueDirectory, Path.GetFileName(name));
if (!File.Exists(result))
{
result = Path.Combine(cueDirectory, Path.GetFileNameWithoutExtension(_cueFilePath) + ".bin");
}
return result;
}
}

View file

@ -1,232 +0,0 @@
using Installer.Installers;
using System;
using System.IO;
namespace Installer.Utils;
public class CueTrack
{
public const int SectorLength = 2352;
public bool Audio;
public long StopSector;
public bool SwapAudioByteOrder;
public bool TruncatePsx;
public bool WavFormat;
public CueTrack(string binFilePath, int trackNumber, string mode, string time)
{
BinFilePath = binFilePath;
TrackNumber = trackNumber;
SetMode(mode);
StartSector = ToFrames(time);
}
public enum TrackExtension
{
ISO, CDR, WAV, UGH
}
public string BinFilePath { get; private set; }
public int BlockSize { get; private set; }
public int BlockStart { get; private set; }
public TrackExtension FileExtension { get; private set; }
public long StartPosition
{
get { return StartSector * SectorLength; }
}
public long StartSector { get; private set; }
public long Stop { get; set; }
public long TotalBytes
{
get { return (StopSector - StartSector + 1) * BlockSize; }
}
public int TrackNumber { get; private set; }
public void Write(string targetPath, IProgress<Installers.InstallProgress> progress)
{
using FileStream fileStream = OpenBinFile();
try
{
using Stream stream = File.OpenWrite(targetPath);
if (Audio && WavFormat)
{
byte[] header = MakeWavHeader(TotalBytes);
stream.Write(header, 0, header.Length);
}
long currentPosition = StartPosition;
long sector = StartSector;
long convertedBytes = 0;
byte[] buf = new byte[SectorLength];
while (sector <= StopSector && fileStream.Read(buf, 0, SectorLength) > 0)
{
if (Audio && SwapAudioByteOrder)
{
DoByteSwap(buf);
}
stream.Write(buf, BlockStart, BlockSize);
currentPosition += SectorLength;
convertedBytes += BlockSize;
if (currentPosition / SectorLength % 500 == 0)
{
progress.Report(new InstallProgress
{
MaximumValue = (int)TotalBytes,
CurrentValue = (int)convertedBytes,
Description = "Converting BIN to ISO"
});
}
sector++;
}
}
catch (Exception e)
{
throw new ApplicationException(string.Format(" Could not write to track file {0}: {1}", targetPath, e.Message));
}
progress.Report(new InstallProgress
{
MaximumValue = (int)TotalBytes,
CurrentValue = (int)TotalBytes,
Description = "Converting BIN to ISO",
});
}
private static byte[] MakeWavHeader(long length)
{
const int WAV_RIFF_HLEN = 12;
const int WAV_FORMAT_HLEN = 24;
const int WAV_DATA_HLEN = 8;
const int WAV_HEADER_LEN = WAV_RIFF_HLEN + WAV_FORMAT_HLEN + WAV_DATA_HLEN;
MemoryStream memoryStream = new(WAV_HEADER_LEN);
using (BinaryWriter writer = new(memoryStream))
{
// RIFF header
writer.Write("RIFF".ToCharArray());
uint dwordValue = (uint)length + WAV_DATA_HLEN + WAV_FORMAT_HLEN + 4;
writer.Write(dwordValue); // length of file, starting from WAVE
writer.Write("WAVE".ToCharArray());
// FORMAT header
writer.Write("fmt ".ToCharArray());
dwordValue = 0x10; // length of FORMAT header
writer.Write(dwordValue);
ushort wordValue = 0x01; // constant
writer.Write(wordValue);
wordValue = 0x02; // channels
writer.Write(wordValue);
dwordValue = 44100; // sample rate
writer.Write(dwordValue);
dwordValue = 44100 * 4; // bytes per second
writer.Write(dwordValue);
wordValue = 4; // bytes per sample
writer.Write(wordValue);
wordValue = 2 * 8; // bits per channel
writer.Write(wordValue);
// DATA header
writer.Write("data".ToCharArray());
dwordValue = (uint)length;
writer.Write(dwordValue);
}
return memoryStream.ToArray();
}
private static long ToFrames(string time)
{
string[] segs = time.Split(':');
int mins = int.Parse(segs[0]);
int secs = int.Parse(segs[1]);
int frames = int.Parse(segs[2]);
return (mins * 60 + secs) * 75 + frames;
}
private void DoByteSwap(byte[] buf)
{
// swap low and high bytes
int p = BlockStart;
int ep = BlockSize;
while (p < ep)
{
(buf[p + 1], buf[p]) = (buf[p], buf[p + 1]);
p += 2;
}
}
private FileStream OpenBinFile()
{
FileStream fileStream;
try
{
fileStream = File.OpenRead(BinFilePath);
}
catch (Exception e)
{
throw new ApplicationException($"Could not open BIN {BinFilePath}: {e.Message}");
}
try
{
fileStream.Seek(StartPosition, SeekOrigin.Begin);
}
catch (Exception e)
{
throw new ApplicationException(string.Format("Could not seek to track location: {0}", e.Message));
}
return fileStream;
}
private void SetMode(string mode)
{
Audio = false;
BlockStart = 0;
FileExtension = TrackExtension.ISO;
switch (mode.ToUpper())
{
case "AUDIO":
BlockSize = 2352;
Audio = true;
FileExtension = WavFormat ? TrackExtension.WAV : TrackExtension.CDR;
break;
case "MODE1/2352":
BlockStart = 16;
BlockSize = 2048;
break;
case "MODE2/2336":
// WAS 2352 in V1.361B still work? What if MODE2/2336 single track bin, still 2352 sectors?
BlockStart = 16;
BlockSize = 2336;
break;
case "MODE2/2352":
if (TruncatePsx)
{
// PSX: truncate from 2352 to 2336 byte tracks
BlockSize = 2336;
}
else
{
// Normal MODE2/2352
BlockStart = 24;
BlockSize = 2048;
}
break;
default:
BlockSize = 2352;
FileExtension = TrackExtension.UGH;
break;
}
}
}

View file

@ -1,21 +0,0 @@
using System.Windows.Forms;
namespace Installer.Utils;
public class FileBrowser
{
public static string? Browse(string? initialDirectory)
{
using var dlg = new FolderBrowserDialog()
{
Description = "Choose directory",
SelectedPath = initialDirectory,
ShowNewFolderButton = true,
};
if (dlg.ShowDialog() == DialogResult.OK)
{
return dlg.SelectedPath;
}
return null;
}
}

View file

@ -1,52 +0,0 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace Installer.Utils;
public class HttpProgressClient
{
public delegate void ProgressChangedHandler(long totalBytesToReceive, long bytesReceived);
public event ProgressChangedHandler? DownloadProgressChanged;
public async Task<byte[]> DownloadDataTaskAsync(Uri uri)
{
using HttpClient client = new();
client.DefaultRequestHeaders.CacheControl = new()
{
NoCache = true
};
HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
long totalBytes = response.Content.Headers.ContentLength ?? 0;
using Stream contentStream = await response.Content.ReadAsStreamAsync();
return await ProcessContentStream(totalBytes, contentStream);
}
private async Task<byte[]> ProcessContentStream(long totalBytes, Stream contentStream)
{
long totalBytesRead = 0;
byte[] buffer = new byte[8192];
using MemoryStream outputStream = new();
while (true)
{
int bytesRead = await contentStream.ReadAsync(buffer);
if (bytesRead == 0)
{
break;
}
await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalBytesRead += bytesRead;
DownloadProgressChanged?.Invoke(totalBytes, totalBytesRead);
}
return outputStream.ToArray();
}
}

View file

@ -1,102 +0,0 @@
using System;
using System.Windows.Input;
namespace Installer.Utils;
public class RelayCommand : ICommand
{
public RelayCommand(Action execute, Func<bool>? canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public RelayCommand(Action execute)
{
_execute = execute;
_canExecute = null;
}
public event EventHandler? CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
_canExecuteChanged += value;
}
remove
{
CommandManager.RequerySuggested -= value;
_canExecuteChanged -= value;
}
}
public bool CanExecute(object? parameter)
{
return _canExecute == null || _canExecute();
}
public void Execute(object? parameter)
{
_execute();
}
public void RaiseCanExecuteChanged()
{
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
}
private readonly Func<bool>? _canExecute;
private readonly Action _execute;
private EventHandler? _canExecuteChanged;
}
public class RelayCommand<T> : ICommand
{
public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public RelayCommand(Action<T?> execute)
{
_execute = execute;
_canExecute = null;
}
public event EventHandler? CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
_canExecuteChanged += value;
}
remove
{
CommandManager.RequerySuggested -= value;
_canExecuteChanged -= value;
}
}
public bool CanExecute(object? parameter)
{
return _canExecute == null || _canExecute((T?)parameter);
}
public void Execute(object? parameter)
{
_execute((T?)parameter);
}
public void RaiseCanExecuteChanged()
{
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
}
private readonly Func<T?, bool>? _canExecute;
private readonly Action<T?> _execute;
private EventHandler? _canExecuteChanged;
}

View file

@ -1,237 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Installer.Utils;
public static class ShortcutUtils
{
public static void CreateShortcut(string shortcutPath, string targetPath, string name, string[]? args = null)
{
var fileInfo = File.Exists(targetPath) ? new FileInfo(targetPath) : null;
using var stream = File.Open(Path.ChangeExtension(shortcutPath, "lnk"), FileMode.Create);
using var bw = new BinaryWriter(stream);
void writeShellLinkHeader()
{
// HeaderSize
bw.Write((Int32)0x4C);
// LinkCLSID
bw.Write(new byte[] { 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 });
// LinkFlags
bw.Write((Int32)(
(1 << 0) // HasLinkTargetIDList
| (1 << 2) // HasName
| (1 << 3) // HasRelativePath
| (1 << 4) // HasWorkingDir
| (1 << 5) // HasArguments
| (1 << 7) // IsUnicode
| (1 << 8) // ForceNoLinkInfo
));
if (fileInfo is not null)
{
bw.Write((Int32)fileInfo.Attributes); // FileAttributes
bw.Write((Int64)fileInfo.CreationTimeUtc.ToFileTime()); // CreationTime
bw.Write((Int64)fileInfo.LastAccessTimeUtc.ToFileTime()); // AccessTime
bw.Write((Int64)fileInfo.LastWriteTimeUtc.ToFileTime()); // WriteTime
bw.Write((Int32)fileInfo.Length); // FileSize
}
else
{
bw.Write((Int32)0); // FileAttributes
bw.Write((Int64)0); // CreationTime
bw.Write((Int64)0); // AccessTime
bw.Write((Int64)0); // WriteTime
bw.Write((Int32)0); // FileSize
}
bw.Write((Int32)0); // IconIndex
bw.Write((Int32)1); // ShowCommand - SW_SHOWNORMAL
bw.Write((Int16)0); // HotKey
bw.Write((Int16)0); // Reserved1
bw.Write((Int32)0); // Reserved2
bw.Write((Int32)0); // Reserved3
}
void writeLinkTargetIDList()
{
var idListSizePos = (int)bw.BaseStream.Position;
bw.Write((UInt16)0); // IDListSize
// CLSID for this computer
bw.Write((Int16)(0x12 + 2));
bw.Write(new byte[] { 0x1F, 0x50, 0xE0, 0x4F, 0xD0, 0x20, 0xEA, 0x3A, 0x69, 0x10, 0xA2, 0xD8, 0x08, 0x00, 0x2B, 0x30, 0x30, 0x9D });
// Root directory
var rootPrefix = "/";
var root = Path.GetPathRoot(targetPath)!;
var rootIdData = Encoding.Default.GetBytes(rootPrefix + root)
.Concat(Enumerable.Repeat((byte)0, 21).ToArray())
.Concat(new byte[] { 0x00 }).ToArray();
bw.Write((Int16)(rootIdData.Length + 2));
bw.Write(rootIdData);
var targetLeafPrefix = fileInfo is not null && (fileInfo.Attributes & FileAttributes.Directory) != 0
? new byte[] { 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
: new byte[] { 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
var targetLeaf = Path.GetRelativePath(root, targetPath);
var targetLeafIdData = targetLeafPrefix.Concat(Encoding.Default.GetBytes(targetLeaf)).Concat(new byte[] { 0x00 }).ToArray();
bw.Write((Int16)(targetLeafIdData.Length + 2));
bw.Write(targetLeafIdData);
var idListSize = (int)bw.BaseStream.Position - idListSizePos;
bw.Write((Int16)0);
// fix offsets
// IDListSize
bw.Seek(idListSizePos, SeekOrigin.Begin);
bw.Write((Int16)idListSize);
// restore pos
bw.Seek(idListSizePos + idListSize + 2, SeekOrigin.Begin);
}
void writeStringData()
{
// NAME
bw.Write((Int16)name.Length);
bw.Write(Encoding.Unicode.GetBytes(name));
// RELATIVE_PATH
var relativePath = Path.GetFileName(targetPath);
bw.Write((Int16)relativePath.Length);
bw.Write(Encoding.Unicode.GetBytes(relativePath));
// WORKING_DIR
var targetDir = Path.GetDirectoryName(targetPath)!;
bw.Write((Int16)targetDir.Length);
bw.Write(Encoding.Unicode.GetBytes(targetDir));
// ARGUMENTS
var cmdline = args is null ? "" : string.Join(
" ",
args.Select(
arg =>
{
if (string.IsNullOrEmpty(arg))
return arg;
string value = Regex.Replace(arg, @"(\\*)" + "\"", @"$1\$0");
value = Regex.Replace(value, @"^(.*\s.*?)(\\*)$", "\"$1$2$2\"");
return value;
}
).ToArray()
);
bw.Write((Int16)cmdline.Length);
bw.Write(Encoding.Unicode.GetBytes(cmdline));
}
writeShellLinkHeader();
writeLinkTargetIDList();
writeStringData();
}
/// <summary>
/// .NET Core compatible .lnk reader.
/// MS Documentation:
/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/16cb4ca1-9339-4d0c-a68d-bf1d6cc0f943?redirectedfrom=MSDN
/// </summary>
public static string? GetLnkTargetPath(string filepath)
{
using var br = new BinaryReader(File.OpenRead(filepath));
var headerSize = br.ReadUInt32();
if (headerSize != 0x4C)
{
throw new ApplicationException("Invalid LNK signature");
}
br.ReadBytes(0x10); // skip LinkCLSID
// LinkFlags
var linkFlags = br.ReadUInt32();
br.ReadBytes(4); // skip FileAttributes
br.ReadBytes(8); // skip CreationTime
br.ReadBytes(8); // skip AccessTime
br.ReadBytes(8); // skip WriteTime
br.ReadBytes(4); // skip FileSize
br.ReadBytes(4); // skip IconIndex
br.ReadBytes(4); // skip ShowCommand
br.ReadBytes(2); // skip Hotkey
br.ReadBytes(2); // skip Reserved
br.ReadBytes(4); // skip Reserved2
br.ReadBytes(4); // skip Reserved3
var hasLinkTargetIDList = (linkFlags & (1 << 0)) != 0;
var hasLinkInfo = (linkFlags & (1 << 1)) != 0;
var hasName = (linkFlags & (1 << 2)) != 0;
var hasRelativePath = (linkFlags & (1 << 3)) != 0;
var hasWorkingDir = (linkFlags & (1 << 4)) != 0;
var isUnicode = (linkFlags & (1 << 7)) != 0;
// if the HasLinkTargetIDList bit, skip LinkTargetIDList
if (hasLinkTargetIDList)
{
var skip = br.ReadUInt16();
br.ReadBytes(skip);
}
if (hasLinkInfo)
{
// get the number of bytes the path contains
var linkInfoSize = br.ReadUInt32();
br.ReadBytes(4); // skip LinkInfoHeaderSize
br.ReadBytes(4); // skip LinkInfoFlags
br.ReadBytes(4); // skip VolumeIDOffset
// Find the location of the LocalBasePath position
var localPathBaseOffset = br.ReadUInt32();
// Skip to the path position
// (subtract the length of the read (4 bytes), the length of the skip (12 bytes), and
// the length of the localPathBaseOffset read (4 bytes) from the localPathBaseOffset)
br.ReadBytes((int)localPathBaseOffset - 0x14);
var size = linkInfoSize - localPathBaseOffset - 0x02;
var bytePath = br.ReadBytes((int)size);
var path = Encoding.UTF8.GetString(bytePath, 0, bytePath.Length);
return path;
}
if (hasName)
{
var _ = isUnicode ? br.ReadSystemCodepageString() : br.ReadUtf16String(); // skip Name
}
string? relativePath = null;
if (hasRelativePath)
{
relativePath = isUnicode ? br.ReadSystemCodepageString() : br.ReadUtf16String();
}
string? workingDir = null;
if (hasWorkingDir)
{
workingDir = isUnicode ? br.ReadSystemCodepageString() : br.ReadUtf16String();
}
if (workingDir is not null && relativePath is not null)
{
return Path.Combine(workingDir, relativePath);
}
if (workingDir is not null)
{
return workingDir;
}
if (relativePath is not null)
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), relativePath);
}
throw new ApplicationException("Unable to determine link target path");
}
}

View file

@ -1,25 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32421.90
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer", "Installer\Installer.csproj", "{2BD62007-7A9F-4DFF-9045-545BA9151DC1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2BD62007-7A9F-4DFF-9045-545BA9151DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BD62007-7A9F-4DFF-9045-545BA9151DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BD62007-7A9F-4DFF-9045-545BA9151DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BD62007-7A9F-4DFF-9045-545BA9151DC1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3D36B2BD-DD06-4A3A-B36D-B56D83846FAB}
EndGlobalSection
EndGlobal