mirror of
https://github.com/LostArtefacts/TRX.git
synced 2025-04-28 20:58:07 +03:00
tools/installer: create common installer library
This creates a generic common installer WPF library for both games.
This commit is contained in:
parent
2adaf2ac0f
commit
e16fcda94b
51 changed files with 2737 additions and 0 deletions
16
tools/installer/.gitignore
vendored
Normal file
16
tools/installer/.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
*.suo
|
||||
*.o
|
||||
*.obj
|
||||
*.pdb
|
||||
*.lib
|
||||
*.exp
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
[Oo]bj/
|
||||
*.user
|
||||
*.ipch
|
||||
.vs/
|
||||
*.vcxproj
|
||||
*.filters
|
||||
*.pubxml
|
||||
[Oo]ut/
|
25
tools/installer/TRX_InstallerLib.sln
Normal file
25
tools/installer/TRX_InstallerLib.sln
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{27F08E8C-2910-4682-B8BC-96ED4C1ECE54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BA21B1D5-1CC7-4ED8-8C79-A1A5B0ACC840}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,32 @@
|
|||
<UserControl
|
||||
x:Class="TRX_InstallerLib.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:TRX_InstallerLib.Models"
|
||||
d:DataContext="{d:DesignInstance Type=models:FinishStep}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary Source="/TRX_InstallerLib;component/Resources/styles.xaml" />
|
||||
</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>
|
|
@ -0,0 +1,11 @@
|
|||
using WC = System.Windows.Controls;
|
||||
|
||||
namespace TRX_InstallerLib.Controls;
|
||||
|
||||
public partial class FinishStepControl : WC.UserControl
|
||||
{
|
||||
public FinishStepControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
<UserControl
|
||||
x:Class="TRX_InstallerLib.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:TRX_InstallerLib.Models"
|
||||
xmlns:utils="clr-namespace:TRX_InstallerLib.Utils"
|
||||
d:DataContext="{d:DesignInstance Type=models:InstallSettingsStep}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary Source="/TRX_InstallerLib;component/Resources/styles.xaml" />
|
||||
</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}">
|
||||
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, OOG, MP3 and WAV files.
|
||||
</Run>
|
||||
</TextBlock>
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox VerticalAlignment="Center" Margin="0,0,0,6" IsChecked="{Binding InstallSettings.DownloadExpansionPack}" IsEnabled="{Binding InstallSettings.IsDownloadingExpansionNeeded}">
|
||||
<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.IsDownloadingExpansionNeeded, 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.DownloadExpansionPack}" Style="{StaticResource small}" Margin="0,0,0,6" Content="Fan-made edition (includes music triggers)"
|
||||
IsChecked="{Binding Path=InstallSettings.ExpansionPackType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static models:ExpansionPackType.Music}}"/>
|
||||
<RadioButton IsEnabled="{Binding InstallSettings.DownloadExpansionPack}" Style="{StaticResource small}" Margin="0,0,0,6" Content="Original edition (does not include music triggers)"
|
||||
IsChecked="{Binding Path=InstallSettings.ExpansionPackType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static models:ExpansionPackType.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>
|
|
@ -0,0 +1,11 @@
|
|||
using WC = System.Windows.Controls;
|
||||
|
||||
namespace TRX_InstallerLib.Controls;
|
||||
|
||||
public partial class InstallSettingsStepControl : WC.UserControl
|
||||
{
|
||||
public InstallSettingsStepControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<UserControl
|
||||
x:Class="TRX_InstallerLib.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:TRX_InstallerLib.Models"
|
||||
xmlns:utils="clr-namespace:TRX_InstallerLib.Utils"
|
||||
d:DataContext="{d:DesignInstance Type=models:InstallSourceViewModel}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/TRX_InstallerLib;component/Resources/styles.xaml" />
|
||||
<ResourceDictionary>
|
||||
<utils:BoolToVisibilityConverter
|
||||
x:Key="BoolToHiddenConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
<utils:BoolToVisibilityConverter
|
||||
x:Key="BoolToVisibleConverter"
|
||||
FalseValue="Collapsed"
|
||||
TrueValue="Visible" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</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>
|
|
@ -0,0 +1,11 @@
|
|||
using WC = System.Windows.Controls;
|
||||
|
||||
namespace TRX_InstallerLib.Controls;
|
||||
|
||||
public partial class InstallSourceControl : WC.UserControl
|
||||
{
|
||||
public InstallSourceControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<UserControl
|
||||
x:Class="TRX_InstallerLib.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:TRX_InstallerLib.Models"
|
||||
d:DataContext="{d:DesignInstance Type=models:InstallStep}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/TRX_InstallerLib;component/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>
|
|
@ -0,0 +1,41 @@
|
|||
using Installer.Models;
|
||||
using System.Windows;
|
||||
using TRX_InstallerLib.Models;
|
||||
using WC = System.Windows.Controls;
|
||||
|
||||
namespace TRX_InstallerLib.Controls;
|
||||
|
||||
public partial class InstallStepControl : WC.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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<UserControl
|
||||
x:Class="TRX_InstallerLib.Controls.SourceStepControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:controls="clr-namespace:TRX_InstallerLib.Controls"
|
||||
xmlns:models="clr-namespace:TRX_InstallerLib.Models"
|
||||
xmlns:utils="clr-namespace:TRX_InstallerLib.Utils"
|
||||
d:DataContext="{d:DesignInstance Type=models:MainWindowViewModel}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/TRX_InstallerLib;component/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>
|
|
@ -0,0 +1,11 @@
|
|||
using WC = System.Windows.Controls;
|
||||
|
||||
namespace TRX_InstallerLib.Controls;
|
||||
|
||||
public partial class SourceStepControl : WC.UserControl
|
||||
{
|
||||
public SourceStepControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
107
tools/installer/TRX_InstallerLib/Controls/TRXInstallWindow.xaml
Normal file
107
tools/installer/TRX_InstallerLib/Controls/TRXInstallWindow.xaml
Normal file
|
@ -0,0 +1,107 @@
|
|||
<Window
|
||||
x:Class="TRX_InstallerLib.Controls.TRXInstallWindow"
|
||||
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:TRX_InstallerLib.Controls"
|
||||
xmlns:models="clr-namespace:TRX_InstallerLib.Models"
|
||||
xmlns:utils="clr-namespace:TRX_InstallerLib.Utils"
|
||||
d:DataContext="{d:DesignInstance Type=models:MainWindowViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{Binding ViewText[window_title_main]}"
|
||||
MinWidth="480"
|
||||
MinHeight="360"
|
||||
Width="{Binding WindowWidth, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged, FallbackValue=640}"
|
||||
Height="500"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/TRX_InstallerLib;component/Resources/styles.xaml" />
|
||||
<ResourceDictionary>
|
||||
<utils:BoolToVisibilityConverter
|
||||
x:Key="BoolToHiddenConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
<utils:BoolToVisibilityConverter
|
||||
x:Key="BoolToVisibleConverter"
|
||||
FalseValue="Collapsed"
|
||||
TrueValue="Visible" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</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>
|
||||
|
||||
<Grid Margin="12" Grid.Column="1" Grid.IsSharedSizeScope="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="SSG"/>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="SSG"/>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="SSG"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Command="{Binding GoToPreviousStepCommand}"
|
||||
Visibility="{Binding IsFinalStep, Converter={StaticResource BoolToHiddenConverter}}"
|
||||
Content="_Back" />
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Command="{Binding GoToNextStepCommand}"
|
||||
Visibility="{Binding IsFinalStep, Converter={StaticResource BoolToHiddenConverter}}"
|
||||
Content="_Next" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Command="{Binding CloseWindowCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
Content="{Binding IsFinalStep, Converter={utils:ConditionalMarkupConverter TrueValue='_Close', FalseValue='_Cancel'}}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -0,0 +1,14 @@
|
|||
using System.Windows;
|
||||
using TRX_InstallerLib.Installers;
|
||||
using TRX_InstallerLib.Models;
|
||||
|
||||
namespace TRX_InstallerLib.Controls;
|
||||
|
||||
public partial class TRXInstallWindow : Window
|
||||
{
|
||||
public TRXInstallWindow(IEnumerable<IInstallSource> installSources)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new MainWindowViewModel(installSources);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using System.IO;
|
||||
using TRX_InstallerLib.Models;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Installers;
|
||||
|
||||
public abstract class BaseInstallSource : IInstallSource
|
||||
{
|
||||
public abstract IEnumerable<string> DirectoriesToTry { get; }
|
||||
|
||||
public virtual string ImageSource
|
||||
{
|
||||
get => AssemblyUtils.GetEmbeddedResourcePath($"{SourceName}.png");
|
||||
}
|
||||
|
||||
public abstract bool IsImportingSavesSupported { get; }
|
||||
public abstract string SourceName { get; }
|
||||
|
||||
public virtual string SuggestedInstallationDirectory
|
||||
{
|
||||
get => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
TRXConstants.Instance.Game!);
|
||||
}
|
||||
|
||||
public abstract Task CopyOriginalGameFiles(
|
||||
string sourceDirectory,
|
||||
string targetDirectory,
|
||||
IProgress<InstallProgress> progress,
|
||||
bool importSaves
|
||||
);
|
||||
|
||||
public abstract bool IsDownloadingMusicNeeded(string sourceDirectory);
|
||||
|
||||
public abstract bool IsDownloadingExpansionNeeded(string sourceDirectory);
|
||||
|
||||
public abstract bool IsGameFound(string sourceDirectory);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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 IsDownloadingExpansionNeeded(string sourceDirectory);
|
||||
|
||||
public bool IsGameFound(string sourceDirectory);
|
||||
|
||||
bool IsImportingSavesSupported { get; }
|
||||
}
|
103
tools/installer/TRX_InstallerLib/Installers/InstallExecutor.cs
Normal file
103
tools/installer/TRX_InstallerLib/Installers/InstallExecutor.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
using System.IO;
|
||||
using TRX_InstallerLib.Models;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Installers;
|
||||
|
||||
public class InstallExecutor
|
||||
{
|
||||
private static readonly string _resourceBaseURL;
|
||||
|
||||
static InstallExecutor()
|
||||
{
|
||||
_resourceBaseURL = $"https://lostartefacts.dev/aux/{TRXConstants.Instance.Game!.ToLower()}";
|
||||
}
|
||||
|
||||
private readonly InstallSettings _settings;
|
||||
|
||||
public InstallExecutor(InstallSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public IInstallSource? InstallSource
|
||||
{
|
||||
get => _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 CopyTRXFiles(_settings.TargetDirectory, progress);
|
||||
if (_settings.DownloadMusic)
|
||||
{
|
||||
await DownloadMusicFiles(_settings.TargetDirectory, progress);
|
||||
}
|
||||
|
||||
if (_settings.DownloadExpansionPack)
|
||||
{
|
||||
await DownloadExpansionFiles(_settings.TargetDirectory, _settings.ExpansionPackType, 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 CopyTRXFiles(string targetDirectory, IProgress<InstallProgress> progress)
|
||||
{
|
||||
InstallUtils.StoreInstallationPath(targetDirectory);
|
||||
|
||||
progress.Report(new InstallProgress
|
||||
{
|
||||
CurrentValue = 0,
|
||||
MaximumValue = 1,
|
||||
Description = "Opening embedded ZIP",
|
||||
});
|
||||
|
||||
using var stream = AssemblyUtils.GetResourceStream("Resources.release.zip", false)
|
||||
?? 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 DownloadExpansionFiles(string targetDirectory, ExpansionPackType type, IProgress<InstallProgress> progress)
|
||||
{
|
||||
await InstallUtils.DownloadZip(
|
||||
$"{_resourceBaseURL}/trub-{type.ToString().ToLower()}.zip",
|
||||
targetDirectory, progress);
|
||||
}
|
||||
}
|
207
tools/installer/TRX_InstallerLib/Installers/InstallUtils.cs
Normal file
207
tools/installer/TRX_InstallerLib/Installers/InstallUtils.cs
Normal file
|
@ -0,0 +1,207 @@
|
|||
using Microsoft.Win32;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
using TRX_InstallerLib.Models;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Installers;
|
||||
|
||||
public static class InstallUtils
|
||||
{
|
||||
private static readonly string _registryStorageKey;
|
||||
|
||||
static InstallUtils()
|
||||
{
|
||||
_registryStorageKey = $@"Software\{TRXConstants.Instance.Game}";
|
||||
}
|
||||
|
||||
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");
|
||||
// TODO: pass extra arg for this
|
||||
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 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)
|
||||
{
|
||||
using var key = Registry.CurrentUser.CreateSubKey(_registryStorageKey);
|
||||
key?.SetValue("InstallPath", installPath);
|
||||
}
|
||||
|
||||
public static string? GetPreviousInstallationPath()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(_registryStorageKey);
|
||||
return key?.GetValue("InstallPath")?.ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public class BaseLanguageViewModel : BaseNotifyPropertyChanged
|
||||
{
|
||||
public static Dictionary<string, string> ViewText
|
||||
{
|
||||
get => Language.Instance.Controls ?? new();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public enum ExpansionPackType
|
||||
{
|
||||
Music,
|
||||
Vanilla,
|
||||
}
|
35
tools/installer/TRX_InstallerLib/Models/FinishSettings.cs
Normal file
35
tools/installer/TRX_InstallerLib/Models/FinishSettings.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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;
|
||||
}
|
16
tools/installer/TRX_InstallerLib/Models/FinishStep.cs
Normal file
16
tools/installer/TRX_InstallerLib/Models/FinishStep.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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 => AssemblyUtils.GetEmbeddedResourcePath("side4.jpg");
|
||||
}
|
10
tools/installer/TRX_InstallerLib/Models/IStep.cs
Normal file
10
tools/installer/TRX_InstallerLib/Models/IStep.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public interface IStep : INotifyPropertyChanged
|
||||
{
|
||||
bool CanProceedToNextStep { get; }
|
||||
bool CanProceedToPreviousStep { get; }
|
||||
string SidebarImage { get; }
|
||||
}
|
155
tools/installer/TRX_InstallerLib/Models/InstallSettings.cs
Normal file
155
tools/installer/TRX_InstallerLib/Models/InstallSettings.cs
Normal file
|
@ -0,0 +1,155 @@
|
|||
using TRX_InstallerLib.Installers;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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 DownloadExpansionPack
|
||||
{
|
||||
get => _downloadExpansionPack;
|
||||
set
|
||||
{
|
||||
if (value != _downloadExpansionPack)
|
||||
{
|
||||
_downloadExpansionPack = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowExpansionTypeSelection
|
||||
{
|
||||
get => _allowExpansionPackSelection;
|
||||
private set
|
||||
{
|
||||
if (value != _allowExpansionPackSelection)
|
||||
{
|
||||
_allowExpansionPackSelection = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ExpansionPackType ExpansionPackType
|
||||
{
|
||||
get => _expansionPackType;
|
||||
set
|
||||
{
|
||||
if (value != _expansionPackType)
|
||||
{
|
||||
_expansionPackType = 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);
|
||||
AllowExpansionTypeSelection = TRXConstants.Instance.AllowExpansionTypeSelection ?? false;
|
||||
DownloadExpansionPack = SourceDirectory is not null && (_installSource?.IsDownloadingExpansionNeeded(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 IsDownloadingExpansionNeeded
|
||||
{
|
||||
get
|
||||
{
|
||||
return SourceDirectory is not null && (InstallSource?.IsDownloadingExpansionNeeded(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 _downloadExpansionPack;
|
||||
private bool _allowExpansionPackSelection;
|
||||
private ExpansionPackType _expansionPackType;
|
||||
private bool _importSaves;
|
||||
private IInstallSource? _installSource;
|
||||
private string? _sourceDirectory;
|
||||
private string? _targetDirectory;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System.Windows.Input;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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 InstallSettings InstallSettings { get; }
|
||||
public string SidebarImage => AssemblyUtils.GetEmbeddedResourcePath("side2.jpg");
|
||||
|
||||
|
||||
private RelayCommand? _chooseLocationCommand;
|
||||
public ICommand ChooseLocationCommand
|
||||
{
|
||||
get => _chooseLocationCommand ??= new RelayCommand(ChooseLocation);
|
||||
}
|
||||
|
||||
private void ChooseLocation()
|
||||
{
|
||||
var result = FileBrowser.Browse(InstallSettings.TargetDirectory);
|
||||
if (result is not null)
|
||||
{
|
||||
InstallSettings.TargetDirectory = result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
using System.Windows.Input;
|
||||
using TRX_InstallerLib.Installers;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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;
|
||||
}
|
||||
}
|
||||
}
|
119
tools/installer/TRX_InstallerLib/Models/InstallStep.cs
Normal file
119
tools/installer/TRX_InstallerLib/Models/InstallStep.cs
Normal file
|
@ -0,0 +1,119 @@
|
|||
using Installer.Models;
|
||||
using TRX_InstallerLib.Installers;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.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 => AssemblyUtils.GetEmbeddedResourcePath("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;
|
||||
}
|
46
tools/installer/TRX_InstallerLib/Models/Language.cs
Normal file
46
tools/installer/TRX_InstallerLib/Models/Language.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Globalization;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public class Language
|
||||
{
|
||||
private static readonly string _langPathFormat = "Resources.Lang.{0}.json";
|
||||
private static readonly string _defaultCulture = "en-US";
|
||||
|
||||
public static Language Instance { get; private set; }
|
||||
|
||||
public Dictionary<string, string>? Controls { get; set; }
|
||||
|
||||
static Language()
|
||||
{
|
||||
CultureInfo defaultCulture = CultureInfo.GetCultureInfo(_defaultCulture);
|
||||
JObject defaultData = ReadLanguage(defaultCulture.TwoLetterISOLanguageName);
|
||||
|
||||
if (CultureInfo.CurrentCulture != defaultCulture)
|
||||
{
|
||||
// Merge the main language first if it exists, and then the country specific if that exists.
|
||||
// e.g. fr.json would load first, then fr-BE.json.
|
||||
MergeLanguage(defaultData, CultureInfo.CurrentCulture.TwoLetterISOLanguageName);
|
||||
MergeLanguage(defaultData, CultureInfo.CurrentCulture.Name);
|
||||
}
|
||||
|
||||
Instance = JsonConvert.DeserializeObject<Language>(defaultData.ToString())!;
|
||||
}
|
||||
|
||||
private static JObject ReadLanguage(string tag)
|
||||
{
|
||||
return JsonUtils.LoadEmbeddedResource(string.Format(_langPathFormat, tag)) ?? new();
|
||||
}
|
||||
|
||||
private static void MergeLanguage(JObject data, string tag)
|
||||
{
|
||||
JObject cultureData = ReadLanguage(tag);
|
||||
if (cultureData != null)
|
||||
{
|
||||
data.Merge(cultureData);
|
||||
}
|
||||
}
|
||||
}
|
23
tools/installer/TRX_InstallerLib/Models/Logger.cs
Normal file
23
tools/installer/TRX_InstallerLib/Models/Logger.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
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));
|
||||
}
|
||||
}
|
152
tools/installer/TRX_InstallerLib/Models/MainWindowViewModel.cs
Normal file
152
tools/installer/TRX_InstallerLib/Models/MainWindowViewModel.cs
Normal file
|
@ -0,0 +1,152 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using TRX_InstallerLib.Installers;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public class MainWindowViewModel : BaseLanguageViewModel
|
||||
{
|
||||
public MainWindowViewModel(IEnumerable<IInstallSource> installSources)
|
||||
{
|
||||
_sourceStep = new SourceStep(installSources);
|
||||
_currentStep = _sourceStep;
|
||||
_installSettings = new InstallSettings();
|
||||
}
|
||||
|
||||
public ICommand CloseWindowCommand
|
||||
{
|
||||
get => _closeWindowCommand ??= new RelayCommand<Window>(CloseWindow);
|
||||
}
|
||||
|
||||
public IStep CurrentStep
|
||||
{
|
||||
get => _currentStep;
|
||||
set
|
||||
{
|
||||
_currentStep = value;
|
||||
_goToPreviousStepCommand?.RaiseCanExecuteChanged();
|
||||
_goToNextStepCommand?.RaiseCanExecuteChanged();
|
||||
_currentStep.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
_goToPreviousStepCommand?.RaiseCanExecuteChanged();
|
||||
_goToNextStepCommand?.RaiseCanExecuteChanged();
|
||||
};
|
||||
NotifyPropertyChanged();
|
||||
NotifyPropertyChanged(nameof(IsFinalStep));
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand GoToNextStepCommand
|
||||
{
|
||||
get => _goToNextStepCommand ??= new RelayCommand(GoToNextStep, CanGoToNextStep);
|
||||
}
|
||||
|
||||
public ICommand GoToPreviousStepCommand
|
||||
{
|
||||
get => _goToPreviousStepCommand ??= new RelayCommand(GoToPreviousStep, CanGoToPreviousStep);
|
||||
}
|
||||
|
||||
public bool IsFinalStep
|
||||
{
|
||||
get => CurrentStep is FinishStep;
|
||||
}
|
||||
|
||||
public bool IsSidebarVisible
|
||||
{
|
||||
get => 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;
|
||||
}
|
||||
}
|
60
tools/installer/TRX_InstallerLib/Models/SourceStep.cs
Normal file
60
tools/installer/TRX_InstallerLib/Models/SourceStep.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using TRX_InstallerLib.Installers;
|
||||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public class SourceStep : BaseNotifyPropertyChanged, IStep
|
||||
{
|
||||
public SourceStep(IEnumerable<IInstallSource> installSources)
|
||||
{
|
||||
// NOTE: the order also decides which installation source will be selected by default
|
||||
InstallationSources = new ObservableCollection<InstallSourceViewModel>
|
||||
(installSources.Select(i => new InstallSourceViewModel(i)));
|
||||
|
||||
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)
|
||||
{
|
||||
SelectedInstallationSource = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanProceedToNextStep
|
||||
{
|
||||
get => 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 => AssemblyUtils.GetEmbeddedResourcePath("side1.jpg");
|
||||
private InstallSourceViewModel? selectedInstallationSource;
|
||||
}
|
19
tools/installer/TRX_InstallerLib/Models/TRXConstants.cs
Normal file
19
tools/installer/TRX_InstallerLib/Models/TRXConstants.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using TRX_InstallerLib.Utils;
|
||||
|
||||
namespace TRX_InstallerLib.Models;
|
||||
|
||||
public class TRXConstants
|
||||
{
|
||||
private static readonly string _constConfigPath = "Resources.const.json";
|
||||
|
||||
public static TRXConstants Instance { get; private set; }
|
||||
|
||||
static TRXConstants()
|
||||
{
|
||||
Instance = JsonUtils.LoadEmbeddedResource(_constConfigPath)?.ToObject<TRXConstants>() ?? new();
|
||||
}
|
||||
|
||||
public string? Game { get; set; }
|
||||
public bool? AllowExpansionTypeSelection { get; set; }
|
||||
|
||||
}
|
5
tools/installer/TRX_InstallerLib/Resources/Lang/en.json
Normal file
5
tools/installer/TRX_InstallerLib/Resources/Lang/en.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Controls": {
|
||||
|
||||
}
|
||||
}
|
4
tools/installer/TRX_InstallerLib/Resources/const.json
Normal file
4
tools/installer/TRX_InstallerLib/Resources/const.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"Game": "TRX",
|
||||
"AllowExpansionTypeSelection": false
|
||||
}
|
32
tools/installer/TRX_InstallerLib/Resources/styles.xaml
Normal file
32
tools/installer/TRX_InstallerLib/Resources/styles.xaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<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" />
|
||||
<Setter Property="MinWidth" Value="70" />
|
||||
</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>
|
24
tools/installer/TRX_InstallerLib/TRX_InstallerLib.csproj
Normal file
24
tools/installer/TRX_InstallerLib/TRX_InstallerLib.csproj
Normal file
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\const.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\const.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\styles.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Resource>
|
||||
</ItemGroup>
|
||||
</Project>
|
35
tools/installer/TRX_InstallerLib/Utils/AssemblyUtils.cs
Normal file
35
tools/installer/TRX_InstallerLib/Utils/AssemblyUtils.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TRX_InstallerLib.Utils;
|
||||
|
||||
public static class AssemblyUtils
|
||||
{
|
||||
public static readonly string _resourcePathFormat = "pack://application:,,,/{0};component/Resources/{1}";
|
||||
|
||||
private static Assembly? GetReferencedAssembly(bool local)
|
||||
{
|
||||
return local ? Assembly.GetExecutingAssembly() : Assembly.GetEntryAssembly();
|
||||
}
|
||||
|
||||
public static Stream GetResourceStream(string relativePath, bool local)
|
||||
{
|
||||
return GetReferencedAssembly(local)?.GetManifestResourceStream(GetAbsolutePath(relativePath, local))!;
|
||||
}
|
||||
|
||||
public static bool ResourceExists(string relativePath, bool local)
|
||||
{
|
||||
return GetReferencedAssembly(local)?.GetManifestResourceNames()
|
||||
.Contains(GetAbsolutePath(relativePath, local)) ?? false;
|
||||
}
|
||||
|
||||
public static string GetAbsolutePath(string relativePath, bool local)
|
||||
{
|
||||
return $"{GetReferencedAssembly(local)!.GetName().Name}.{relativePath}";
|
||||
}
|
||||
|
||||
public static string GetEmbeddedResourcePath(string resource)
|
||||
{
|
||||
return string.Format(_resourcePathFormat, Assembly.GetEntryAssembly()!.GetName().Name, resource);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace TRX_InstallerLib.Utils;
|
||||
|
||||
public abstract class BaseNotifyPropertyChanged : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace TRX_InstallerLib.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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace TRX_InstallerLib.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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System.Globalization;
|
||||
using WD = System.Windows.Data;
|
||||
|
||||
namespace TRX_InstallerLib.Utils;
|
||||
|
||||
public class ComparisonConverter : WD.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 : WD.Binding.DoNothing;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace TRX_InstallerLib.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;
|
||||
}
|
||||
}
|
89
tools/installer/TRX_InstallerLib/Utils/CueFile.cs
Normal file
89
tools/installer/TRX_InstallerLib/Utils/CueFile.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TRX_InstallerLib.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;
|
||||
}
|
||||
}
|
230
tools/installer/TRX_InstallerLib/Utils/CueTrack.cs
Normal file
230
tools/installer/TRX_InstallerLib/Utils/CueTrack.cs
Normal file
|
@ -0,0 +1,230 @@
|
|||
using System.IO;
|
||||
|
||||
namespace TRX_InstallerLib.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<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;
|
||||
}
|
||||
}
|
||||
}
|
19
tools/installer/TRX_InstallerLib/Utils/FileBrowser.cs
Normal file
19
tools/installer/TRX_InstallerLib/Utils/FileBrowser.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace TRX_InstallerLib.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;
|
||||
}
|
||||
}
|
50
tools/installer/TRX_InstallerLib/Utils/HttpProgressClient.cs
Normal file
50
tools/installer/TRX_InstallerLib/Utils/HttpProgressClient.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.IO;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace TRX_InstallerLib.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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace TRX_InstallerLib.Utils;
|
||||
|
||||
public class InstallProgress
|
||||
{
|
||||
public int? CurrentValue { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Finished { get; set; }
|
||||
public int? MaximumValue { get; set; }
|
||||
}
|
31
tools/installer/TRX_InstallerLib/Utils/JsonUtils.cs
Normal file
31
tools/installer/TRX_InstallerLib/Utils/JsonUtils.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using System.IO;
|
||||
|
||||
namespace TRX_InstallerLib.Utils;
|
||||
|
||||
public static class JsonUtils
|
||||
{
|
||||
public static JObject? LoadEmbeddedResource(string path)
|
||||
{
|
||||
// Try to locate the data in this assembly first, then merge it
|
||||
// with the same in the entry assembly if relevant.
|
||||
JObject? data = null;
|
||||
|
||||
if (AssemblyUtils.ResourceExists(path, true))
|
||||
{
|
||||
using Stream stream = AssemblyUtils.GetResourceStream(path, true);
|
||||
using StreamReader reader = new(stream);
|
||||
data = JObject.Parse(reader.ReadToEnd());
|
||||
}
|
||||
|
||||
if (AssemblyUtils.ResourceExists(path, false))
|
||||
{
|
||||
data ??= new();
|
||||
using Stream stream = AssemblyUtils.GetResourceStream(path, false);
|
||||
using StreamReader reader = new(stream);
|
||||
data.Merge(JObject.Parse(reader.ReadToEnd()));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
20
tools/installer/TRX_InstallerLib/Utils/ProcessUtils.cs
Normal file
20
tools/installer/TRX_InstallerLib/Utils/ProcessUtils.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace TRX_InstallerLib.Utils;
|
||||
|
||||
public static class ProcessUtils
|
||||
{
|
||||
public static void Start(string fileName, string? arguments = null)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = new Uri(fileName).IsFile
|
||||
? Path.GetDirectoryName(fileName)
|
||||
: null
|
||||
});
|
||||
}
|
||||
}
|
101
tools/installer/TRX_InstallerLib/Utils/RelayCommand.cs
Normal file
101
tools/installer/TRX_InstallerLib/Utils/RelayCommand.cs
Normal file
|
@ -0,0 +1,101 @@
|
|||
using System.Windows.Input;
|
||||
|
||||
namespace TRX_InstallerLib.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;
|
||||
}
|
235
tools/installer/TRX_InstallerLib/Utils/ShortcutUtils.cs
Normal file
235
tools/installer/TRX_InstallerLib/Utils/ShortcutUtils.cs
Normal file
|
@ -0,0 +1,235 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TRX_InstallerLib.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(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(
|
||||
(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((int)fileInfo.Attributes); // FileAttributes
|
||||
bw.Write(fileInfo.CreationTimeUtc.ToFileTime()); // CreationTime
|
||||
bw.Write(fileInfo.LastAccessTimeUtc.ToFileTime()); // AccessTime
|
||||
bw.Write(fileInfo.LastWriteTimeUtc.ToFileTime()); // WriteTime
|
||||
bw.Write((int)fileInfo.Length); // FileSize
|
||||
}
|
||||
else
|
||||
{
|
||||
bw.Write(0); // FileAttributes
|
||||
bw.Write((long)0); // CreationTime
|
||||
bw.Write((long)0); // AccessTime
|
||||
bw.Write((long)0); // WriteTime
|
||||
bw.Write(0); // FileSize
|
||||
}
|
||||
bw.Write(0); // IconIndex
|
||||
bw.Write(1); // ShowCommand - SW_SHOWNORMAL
|
||||
bw.Write((short)0); // HotKey
|
||||
bw.Write((short)0); // Reserved1
|
||||
bw.Write(0); // Reserved2
|
||||
bw.Write(0); // Reserved3
|
||||
}
|
||||
|
||||
void writeLinkTargetIDList()
|
||||
{
|
||||
var idListSizePos = (int)bw.BaseStream.Position;
|
||||
bw.Write((ushort)0); // IDListSize
|
||||
|
||||
// CLSID for this computer
|
||||
bw.Write((short)(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((short)(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((short)(targetLeafIdData.Length + 2));
|
||||
bw.Write(targetLeafIdData);
|
||||
|
||||
var idListSize = (int)bw.BaseStream.Position - idListSizePos;
|
||||
|
||||
bw.Write((short)0);
|
||||
|
||||
// fix offsets
|
||||
// IDListSize
|
||||
bw.Seek(idListSizePos, SeekOrigin.Begin);
|
||||
bw.Write((short)idListSize);
|
||||
|
||||
// restore pos
|
||||
bw.Seek(idListSizePos + idListSize + 2, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
void writeStringData()
|
||||
{
|
||||
// NAME
|
||||
bw.Write((short)name.Length);
|
||||
bw.Write(Encoding.Unicode.GetBytes(name));
|
||||
|
||||
// RELATIVE_PATH
|
||||
var relativePath = Path.GetFileName(targetPath);
|
||||
bw.Write((short)relativePath.Length);
|
||||
bw.Write(Encoding.Unicode.GetBytes(relativePath));
|
||||
|
||||
// WORKING_DIR
|
||||
var targetDir = Path.GetDirectoryName(targetPath)!;
|
||||
bw.Write((short)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((short)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");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue