ui, and controller, ecc, ecc

This commit is contained in:
lanyi 2020-03-31 04:05:57 +02:00
parent 1d706b1adc
commit 97067a68a5
20 changed files with 862 additions and 396 deletions

162
App.xaml
View File

@ -1,9 +1,167 @@
<Application x:Class="HashCalculator.GUI.App" <Application x:Class="HashCalculator.GUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HashCalculator.GUI" xmlns:l="clr-namespace:HashCalculator.GUI"
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<c:ValueConverterAggregate x:Key="ValidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:ValueConverterAggregate x:Key="InvalidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<c:BooleanInvertConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:ValueConverterAggregate x:Key="NullToVisibilityConverter">
<c:NullToBooleanConverter />
<c:BooleanInvertConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:ValueConverterAggregate x:Key="NotNullToVisibilityConverter">
<c:NullToBooleanConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:ValueConverterAggregate x:Key="NonZeroToVisibilityConverter">
<c:IsZeroToBooleanConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:MultiValueEqualityConverter x:Key="MultiValueEqualityConverter" />
<Style x:Key="CommonStyle" TargetType="Control">
<Setter
Property="Foreground"
Value="LightGray"
/>
<Setter
Property="Background"
Value="#202020"
/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="l:MainWindow"
/>
<Style
TargetType="l:ShellLink"
>
<Setter
Property="Foreground"
Value="#30B0FF"
/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type TextBox}"
>
<Setter Property="Background" Value="#00000000"/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type ComboBox}"
>
<Setter Property="Background" Value="#00000000"/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type Button}"
x:Key="ButtonStyle"
>
<Setter Property="Background" Value="#20808080"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="ButtonBorderTemplate"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True"
>
<ContentPresenter
x:Name="ButtonContentPresenterTemplate"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
Focusable="False"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsDefaulted" Value="True">
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#80BEE6FD"/>
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FF3C7FB1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#80C4E5F6"/>
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FF2C628B"/>
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#FFBCDDEE"/>
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FF245A83"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#00000000"/>
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FFADB2B5"/>
<Setter Property="Foreground" Value="#808080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
BasedOn="{StaticResource ButtonStyle}"
TargetType="Button"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="CheckBox"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGrid}"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGridColumnHeader}"
>
<Setter Property="Padding" Value="10,5" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Background" Value="#343434"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0, 0, 1, 0"/>
<Setter Property="BorderBrush" Value="Gray" />
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGridRowHeader}"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGridRow}"
/>
<Style TargetType="{x:Type Button}" x:Key="{ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}">
<Setter Property="Background" Value="Black" />
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type ListBox}"
>
<Setter Property="Background" Value="#181818"/>
</Style>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@ -5,6 +5,7 @@ using System.Data;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using TechnologyAssembler;
namespace HashCalculator.GUI namespace HashCalculator.GUI
{ {
@ -13,5 +14,10 @@ namespace HashCalculator.GUI
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
public App()
{
var core = new TechnologyAssemblerCoreModule();
core.Initialize();
}
} }
} }

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using TechnologyAssembler.Core.Assets;
namespace HashCalculator.GUI namespace HashCalculator.GUI
{ {
@ -10,10 +12,10 @@ namespace HashCalculator.GUI
public string Type { get; } public string Type { get; }
public string Name { get; } public string Name { get; }
public IEnumerable<string> DisplayLabels { get; } public IEnumerable<string> DisplayLabels { get; }
public uint Hash => SageHash.CalculateLowercaseHash(Name); public uint InstanceId => SageHash.CalculateLowercaseHash(Name);
public string IdString => $"{Type}:{Name}"; public string NameString => $"{Type}:{Name}";
public string HashString => $"{Hash:0X} ({Hash})"; public string InstanceIdString => $"{InstanceId:X8} ({InstanceId})";
public AssetEntry(XElement element) public AssetEntry(XElement element)
{ {
@ -38,6 +40,21 @@ namespace HashCalculator.GUI
DisplayLabels = labels.Concat(transformLabels).ToArray(); DisplayLabels = labels.Concat(transformLabels).ToArray();
} }
public AssetEntry(Asset asset)
{
if(asset is null)
{
throw new ArgumentNullException($"{nameof(asset)} is null");
}
Type = asset.TypeName;
Name = asset.InstanceName;
DisplayLabels = Enumerable.Empty<string>();
if(InstanceId != asset.InstanceId || SageHash.CalculateBinaryHash(Type) != asset.TypeId)
{
throw new InvalidDataException();
}
}
public bool Equals(AssetEntry entry) public bool Equals(AssetEntry entry)
{ {
return this == entry; return this == entry;
@ -73,7 +90,7 @@ namespace HashCalculator.GUI
return true; return true;
} }
return a.Type == b.Type && a.Hash == b.Hash; return a.Type == b.Type && a.InstanceId == b.InstanceId;
} }
public static bool operator !=(AssetEntry a, AssetEntry b) public static bool operator !=(AssetEntry a, AssetEntry b)
@ -84,7 +101,34 @@ namespace HashCalculator.GUI
// override object.GetHashCode // override object.GetHashCode
public override int GetHashCode() public override int GetHashCode()
{ {
return HashCode.Combine(Type, Hash); return HashCode.Combine(Type, InstanceId);
}
}
internal class DisplayAssetEntry : IComparable<DisplayAssetEntry>
{
public string Name { get; }
public string InstanceId { get; }
public DisplayAssetEntry(AssetEntry entry)
{
Name = entry.NameString;
InstanceId = entry.InstanceIdString;
}
public int CompareTo(DisplayAssetEntry other)
{
return string.CompareOrdinal(Name, other.Name);
}
}
internal class LocalizedDisplayAssetEntry : DisplayAssetEntry
{
public string LocalizedNames { get; }
public LocalizedDisplayAssetEntry(AssetEntry entry) : base(entry)
{
LocalizedNames = entry.DisplayLabels.Aggregate((x, y) => $"{x} {y}");
} }
} }
} }

227
Controller.cs Normal file
View File

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TechnologyAssembler.Core.IO;
using TechnologyAssembler.Core.Assets;
namespace HashCalculator.GUI
{
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")]
internal class Controller
{
public const string SelectedFileSystemRootPath = "/selected";
public const string ManifestExtension = ".manifest";
private ViewModel ViewModel { get; }
private BigFileSystemProvider? _currentBig = null;
public Controller(ViewModel viewModel)
{
ViewModel = viewModel;
}
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
public async Task OnMainInputDecided(InputEntry selected)
{
_currentBig?.Dispose();
_currentBig = null;
ViewModel.Entries.Clear();
ViewModel.TraceText = string.Empty;
ViewModel.BigEntryInput.AllManifests = null;
try
{
switch (selected.Type)
{
case InputEntryType.BinaryFile:
ViewModel.StatusText = "请留意一下弹出的窗口(";
CopyableBox.ShowDialog(token => CalculateBinaryHash(selected.Value, token));
ViewModel.StatusText = ViewModel.SuggestionString(string.Empty);
return;
case InputEntryType.BigFile:
await LoadBig(selected.Value).ConfigureAwait(true);
return;
case InputEntryType.ManifestFile:
await LoadManifestFromFile(selected.Value).ConfigureAwait(true);
return;
case InputEntryType.XmlFile:
throw new NotImplementedException();
default:
throw new NotSupportedException();
}
}
catch (Exception error)
{
ViewModel.StatusText = "失败…";
CopyableBox.ShowDialog(_ =>
{
return Task.FromResult($"在尝试加载 {selected.Type} `{selected.Value}` 时发生错误:\r\n{error}");
});
ViewModel.StatusText = string.Empty;
}
}
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
private static Task<string> CalculateBinaryHash(string filePath, CancellationToken cancel)
{
return Task.Run(() =>
{
try
{
using var file = File.OpenRead(filePath);
var array = new byte[file.Length];
cancel.ThrowIfCancellationRequested();
var totalRead = 0;
const int ReadSize = 32 * 1024 * 1024;
while (totalRead < array.Length)
{
totalRead += file.Read(array, totalRead, Math.Min(array.Length - totalRead, ReadSize));
cancel.ThrowIfCancellationRequested();
}
var hashes = new Func<byte[], uint>[]
{
SageHash.CalculateBinaryHash,
SageHash.CalculateLauncherBinaryHash
}.AsParallel().Select(fn => fn(array)).ToArray();
return $"使用 SAGE FastHash 计算出的哈希值:{hashes[0]:X8} (十进制 {hashes[0]}\r\n"
+ "注意这是以大小写敏感模式计算出的哈希值与素材ID的哈希值转换成小写后计算的哈希一般是不一样的\r\n\r\n"
+ "使用 SAGE Launcher FastHash比如说 RA3.exe 用来校验更新补丁文件的哈希)计算出的哈希值:"
+ $"{hashes[1]:X8} (十进制 {hashes[1]}";
}
catch (Exception exception)
{
return exception.ToString();
}
});
}
private async Task LoadBig(string path)
{
ViewModel.StatusText = "正在尝试加载 big 文件…";
var bigViewModel = ViewModel.BigEntryInput;
bigViewModel.Text = string.Empty;
bigViewModel.AllManifests = await Task.Run(() =>
{
using var big = new BigFile(path);
_currentBig = new BigFileSystemProvider(SelectedFileSystemRootPath, big, null);
return FindManifests();
}).ConfigureAwait(true);
var first = bigViewModel.AllManifests.FirstOrDefault();
if (first != null)
{
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value);
if (name.StartsWith("mod", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("mapmetadata", StringComparison.OrdinalIgnoreCase))
{
bigViewModel.SelectedItem = first;
await bigViewModel.SelectCommand.ExecuteTask(ViewModel).ConfigureAwait(true);
ViewModel.StatusText = "big 已加载完毕,并自动选了一个看起来比较合适的 manifest 文件(";
}
}
else
{
ViewModel.StatusText = "big 文件已加载完毕,请选择 big 文件里的 manifest 文件";
}
}
private async Task LoadManifestFromFile(string path)
{
using var provider = new FileSystemProvider(SelectedFileSystemRootPath, null);
await ProcessManifest(path).ConfigureAwait(true);
}
public async Task ProcessManifest(string path)
{
ViewModel.Entries.Clear();
ViewModel.TraceText = string.Empty;
ViewModel.StatusText = "正在加载 manifest 文件…";
var entries = await Task.Run(() =>
{
if (path.EndsWith(ManifestExtension, StringComparison.OrdinalIgnoreCase))
{
path = path.Remove(path.Length - ManifestExtension.Length);
}
using var manifest = Manifest.Load(path);
var assets = from asset in manifest.Assets.Values
select new DisplayAssetEntry(new AssetEntry(asset));
return assets.ToArray();
}).ConfigureAwait(true);
await AddEntries(entries).ConfigureAwait(true);
ViewModel.StatusText = $"总共加载了{ViewModel.Entries.Count}个素材";
}
public async Task AddEntries(IEnumerable<DisplayAssetEntry> entries)
{
var target = ViewModel.Entries;
int BinarySearch(DisplayAssetEntry entry, int? hint = null)
{
var begin = hint.GetValueOrDefault(0);
var end = target.Count;
while (begin < end)
{
var middle = begin + (end - begin) / 2;
var result = entry.CompareTo(target[middle]);
if (result == 0)
{
return middle;
}
else if (result < 0)
{
end = middle;
}
else if (result > 0)
{
begin = middle + 1;
}
}
return end;
}
var ordered = await Task.Run(() =>
{
var array = entries.ToArray();
Array.Sort(array);
return array;
}).ConfigureAwait(true);
ViewModel.StatusText = $"正在处理刚刚读取出来的 {ordered.Length} 个素材";
var hint = 0;
foreach (var entry in ordered)
{
var index = hint = BinarySearch(entry, hint);
target.Insert(index, entry);
}
}
public static IEnumerable<InputEntry> FindManifests()
{
var manifests = VirtualFileSystem.ListFiles(SelectedFileSystemRootPath, "*.manifest", VirtualSearchOptionType.AllDirectories);
var modManifests = from manifest in manifests
where FileNameStartsWith(manifest, "mod")
select manifest;
var globalDataManifests = from manifest in manifests
where FileNameStartsWith(manifest, "mapmetadata")
select manifest;
var firstManifests = modManifests.Concat(globalDataManifests);
var otherManifests = from manifest in manifests
where !firstManifests.Contains(manifest)
select manifest;
return from manifest in firstManifests.Concat(otherManifests)
select new InputEntry(InputEntryType.ManifestFile, manifest, manifest);
}
private static bool FileNameStartsWith(string path, string what)
{
return VirtualFileSystem.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Windows.Data; using System.Windows.Data;
namespace HashCalculator.GUI.Converters namespace HashCalculator.GUI.Converters
{ {
[SuppressMessage("Microsoft.Performance", "CA1812")]
[ValueConversion(typeof(bool), typeof(bool))] [ValueConversion(typeof(bool), typeof(bool))]
internal class BooleanInvertConverter : IValueConverter internal class BooleanInvertConverter : IValueConverter
{ {

View File

@ -0,0 +1,20 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace HashCalculator.GUI.Converters
{
[ValueConversion(typeof(int), typeof(bool))]
public class IsZeroToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value == 0 ? false : true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
namespace HashCalculator.GUI.Converters
{
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal class MultiValueEqualityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.All(x => Equals(x, values.First()));
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Windows.Data; using System.Windows.Data;
namespace HashCalculator.GUI.Converters namespace HashCalculator.GUI.Converters
{ {
[SuppressMessage("Microsoft.Performance", "CA1812")]
[ValueConversion(typeof(InputEntry), typeof(bool))] [ValueConversion(typeof(InputEntry), typeof(bool))]
internal class ValidInputEntryTypeToBooleanConverter : IValueConverter internal class ValidInputEntryTypeToBooleanConverter : IValueConverter
{ {

View File

@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Windows.Data; using System.Windows.Data;
namespace HashCalculator.GUI.Converters namespace HashCalculator.GUI.Converters
{ {
[SuppressMessage("Microsoft.Performance", "CA1812")]
internal class ValueConverterAggregate : List<IValueConverter>, IValueConverter internal class ValueConverterAggregate : List<IValueConverter>, IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

View File

@ -128,32 +128,22 @@ namespace HashCalculator.GUI
private async Task InitializationTask(Func<CancellationToken, Task<string>> action, Dispatcher dispatcher) private async Task InitializationTask(Func<CancellationToken, Task<string>> action, Dispatcher dispatcher)
{ {
var utcBegin = DateTimeOffset.UtcNow;
var timer = new DispatcherTimer(TimeSpan.FromMilliseconds(200), DispatcherPriority.Normal, (s, e) =>
{
var timeElapsed = DateTimeOffset.UtcNow - utcBegin;
if (((DispatcherTimer)s).IsEnabled && timeElapsed.TotalSeconds > 1)
{
Text = Text = $"{InitialMessage}\r\n目前耗时{timeElapsed}稍微再等一下吧233";
}
}, dispatcher);
timer.Start();
var token = _cancellationTokenSource.Token; var token = _cancellationTokenSource.Token;
async Task<string> Action() async Task<string> Action()
{ {
try try
{ {
using var timer = new DisposableDispatcherTimer(timer =>
{
Text = $"{InitialMessage}\r\n目前耗时{timer.TimeSinceCreation}稍微再等一下吧233";
}, dispatcher, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(1));
return await action(token).ConfigureAwait(false); return await action(token).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return "操作已被取消"; return "操作已被取消";
} }
finally
{
timer.Stop();
}
} }
Text = await dispatcher.Invoke(Action).ConfigureAwait(true); Text = await dispatcher.Invoke(Action).ConfigureAwait(true);
} }

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace HashCalculator.GUI
{
internal class DisposableDispatcherTimer : IDisposable
{
public DispatcherTimer Timer { get; }
public DateTimeOffset CreationTime { get; } = DateTimeOffset.UtcNow;
public TimeSpan TimeSinceCreation => DateTimeOffset.UtcNow - CreationTime;
public DisposableDispatcherTimer(Action<DisposableDispatcherTimer> action, Dispatcher dispatcher, TimeSpan interval) :
this(action, dispatcher, interval, TimeSpan.Zero)
{
}
public DisposableDispatcherTimer(Action<DisposableDispatcherTimer> action, Dispatcher dispatcher, TimeSpan interval, TimeSpan wait)
{
Timer = new DispatcherTimer(interval, DispatcherPriority.Normal, (s, e) =>
{
if (Timer.IsEnabled && (TimeSinceCreation > wait))
{
action(this);
}
}, dispatcher);
Timer.Start();
}
public void Dispose()
{
Timer.Stop();
}
}
}

View File

@ -44,15 +44,16 @@ namespace HashCalculator.GUI
var currentFiles = new List<InputEntry>(); var currentFiles = new List<InputEntry>();
string? fileName; string? fileName;
string? currentFullPath; string? currentFullPath = null;
try try
{ {
currentFullPath = Path.GetFullPath(path); var currentFile = new FileInfo(path);
fileName = Path.GetFileName(path); fileName = currentFile.Name;
if (File.Exists(currentFullPath)) if (currentFile.Exists)
{ {
var type = CheckExtension(currentFullPath); currentFullPath = currentFile.FullName;
if(type is InputEntryType entryType) var type = CheckExtension(currentFile);
if (type is InputEntryType entryType)
{ {
currentFiles.Add(new InputEntry(entryType, path, currentFullPath)); currentFiles.Add(new InputEntry(entryType, path, currentFullPath));
} }
@ -77,7 +78,7 @@ namespace HashCalculator.GUI
select file; select file;
var supportedFiles = from file in otherFiles var supportedFiles = from file in otherFiles
let type = CheckExtension(file.Extension) let type = CheckExtension(file)
where type.HasValue where type.HasValue
select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName); select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName);
@ -91,9 +92,9 @@ namespace HashCalculator.GUI
return currentFiles.Concat(alternatives); return currentFiles.Concat(alternatives);
} }
private static InputEntryType? CheckExtension(string path) private static InputEntryType? CheckExtension(FileInfo info)
{ {
if (Mapping.TryGetValue(path, out var type)) if (Mapping.TryGetValue(info.Extension, out var type))
{ {
return type; return type;
} }

View File

@ -20,11 +20,11 @@
<PackageReference Include="Mvp.Xml" Version="2.3.0" /> <PackageReference Include="Mvp.Xml" Version="2.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TechnologyAssembler.Core\TechnologyAssembler.Core.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Windows.Presentation" /> <Reference Include="System.Windows.Presentation" />
<Reference Include="TechnologyAssembler.Core">
<HintPath>TechnologyAssembler.Core.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -8,7 +8,7 @@
xmlns:c="clr-namespace:HashCalculator.GUI.Converters" xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
x:ClassModifier="internal" x:ClassModifier="internal"
x:Name="_this" x:Name="Self"
d:DesignHeight="100" d:DesignWidth="800" d:DesignHeight="100" d:DesignWidth="800"
> >
<UserControl.Resources> <UserControl.Resources>
@ -69,12 +69,17 @@
<Grid> <Grid>
<!--TextChanged="OnTextChanged"--> <!--TextChanged="OnTextChanged"-->
<Grid> <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<TextBox <TextBox
x:Name="TextBox" x:Name="TextBox"
Text="{Binding Path=Text, Text="{Binding Path=Text,
RelativeSource={RelativeSource AncestorType=local:InputBar}, RelativeSource={RelativeSource AncestorType=local:InputBar},
UpdateSourceTrigger=PropertyChanged}" UpdateSourceTrigger=PropertyChanged}"
PreviewKeyDown="OnPreviewKeyDown" PreviewKeyDown="OnPreviewKeyDown"
Grid.Column="0"
Padding="2, 0" Padding="2, 0"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
LostFocus="OnLostFocus" LostFocus="OnLostFocus"
@ -84,9 +89,20 @@
RelativeSource={RelativeSource AncestorType=local:InputBar}}" RelativeSource={RelativeSource AncestorType=local:InputBar}}"
IsHitTestVisible="False" IsHitTestVisible="False"
Visibility="{Binding ElementName=TextBox, Path=Text, Converter={StaticResource NullToVisibilityConverter}}" Visibility="{Binding ElementName=TextBox, Path=Text, Converter={StaticResource NullToVisibilityConverter}}"
Grid.Column="0"
Padding="5, 0" Padding="5, 0"
VerticalAlignment="Center" VerticalAlignment="Center"
/> />
<Button
Click="OnToggleDropDownButtonClick"
Grid.Column="1"
Background="Transparent"
>
<Path
Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Data="M 0 2 L 1 0 L 6 4 L 11 0 L 12 2 L 6 7 Z"
/>
</Button>
</Grid> </Grid>
<Popup <Popup
x:Name="DropDown" x:Name="DropDown"
@ -101,7 +117,6 @@
ItemsSource="{Binding Path=Collection, ItemsSource="{Binding Path=Collection,
RelativeSource={RelativeSource AncestorType=local:InputBar}}" RelativeSource={RelativeSource AncestorType=local:InputBar}}"
SelectionMode="Single" SelectionMode="Single"
SelectionChanged="OnSelectionChanged"
SelectedItem="{Binding Path=SelectedItem, SelectedItem="{Binding Path=SelectedItem,
RelativeSource={RelativeSource AncestorType=local:InputBar}}" RelativeSource={RelativeSource AncestorType=local:InputBar}}"
SelectedIndex="{Binding Path=SelectedIndex, SelectedIndex="{Binding Path=SelectedIndex,

View File

@ -7,13 +7,9 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Globalization;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;
namespace HashCalculator.GUI namespace HashCalculator.GUI
{ {
@ -74,7 +70,12 @@ namespace HashCalculator.GUI
DependencyProperty.Register(nameof(SelectedItem), typeof(InputEntry), typeof(InputBar), new FrameworkPropertyMetadata DependencyProperty.Register(nameof(SelectedItem), typeof(InputEntry), typeof(InputBar), new FrameworkPropertyMetadata
{ {
BindsTwoWayByDefault = true, BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
PropertyChangedCallback = (d, e) =>
{
var self = (InputBar)d;
self.OnSelectionChanged();
}
}); });
public InputEntry? SelectedItem public InputEntry? SelectedItem
{ {
@ -127,7 +128,7 @@ namespace HashCalculator.GUI
} }
} }
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnSelectionChanged()
{ {
if (SelectedItem != null) if (SelectedItem != null)
{ {
@ -151,5 +152,14 @@ namespace HashCalculator.GUI
return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1); return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1);
} }
private void OnToggleDropDownButtonClick(object sender, RoutedEventArgs e)
{
TextBox.Focus();
TextBox.Select(Text?.Length ?? 0, 0);
if (!DropDown.IsOpen)
{
DropDown.IsOpen = true;
}
}
} }
} }

View File

@ -8,106 +8,18 @@
xmlns:l="clr-namespace:HashCalculator.GUI" xmlns:l="clr-namespace:HashCalculator.GUI"
xmlns:c="clr-namespace:HashCalculator.GUI.Converters" xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
Title="Sage FastHash 哈希计算器" Height="600" Width="600" Title="SAGE FastHash 哈希计算器"
Background="#202020" Height="600"
Foreground="LightGray" Width="600"
Style="{StaticResource CommonStyle}"
> >
<Window.Resources>
<c:ValueConverterAggregate x:Key="ValidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<c:ValueConverterAggregate x:Key="InvalidInputEntryToVisibilityConverter">
<c:ValidInputEntryTypeToBooleanConverter />
<c:BooleanInvertConverter />
<BooleanToVisibilityConverter />
</c:ValueConverterAggregate>
<Style x:Key="CommonStyle" TargetType="Control">
<Setter
Property="Foreground"
Value="{Binding Path=(TextElement.Foreground),
RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"
/>
<Setter
Property="Background"
Value="{Binding Path=(TextElement.Background),
RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"
/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type TextBox}"
>
<Setter Property="Background" Value="#00000000"/>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type Button}"
>
<Setter Property="Background" Value="#20808080"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsDefaulted" Value="True">
<Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" TargetName="border" Value="#80BEE6FD"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF3C7FB1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" TargetName="border" Value="#80C4E5F6"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/>
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Background" TargetName="border" Value="#FFBCDDEE"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF245A83"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" TargetName="border" Value="#00000000"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FFADB2B5"/>
<Setter Property="Foreground" Value="#808080"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGrid}"
/>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type DataGridColumnHeader}"
>
<Setter Property="Padding" Value="10,5" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Background" Value="#343434"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0, 0, 1, 0"/>
<Setter Property="BorderBrush" Value="Gray" />
</Style>
<Style
BasedOn="{StaticResource CommonStyle}"
TargetType="{x:Type ListBox}"
>
<Setter Property="Background" Value="#181818"/>
</Style>
</Window.Resources>
<Window.DataContext> <Window.DataContext>
<l:ViewModel/> <l:ViewModel />
</Window.DataContext> </Window.DataContext>
<Grid Margin="10,20,10,10"> <Grid Margin="10,20,10,10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition/> <RowDefinition/>
<RowDefinition Height="130" /> <RowDefinition Height="150" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DockPanel Grid.Row="0"> <DockPanel Grid.Row="0">
<Grid <Grid
@ -150,79 +62,134 @@
DockPanel.Dock="Top" DockPanel.Dock="Top"
Height="25" Height="25"
Margin="0,10,0,0" Margin="0,10,0,0"
Visibility="Visible" Visibility="{Binding BigEntryInput.AllManifests, Converter={StaticResource NotNullToVisibilityConverter}}"
> >
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="92"/> <ColumnDefinition Width="92" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<l:InputBar <l:InputBar
HintText="输入 big 文件里包含的 manifest 文件的路径" HintText="输入 big 文件里包含的 manifest 文件的路径"
Collection="{Binding Items}" Text="{Binding BigEntryInput.Text}"
SelectedItem="{Binding SelectedItem}" Collection="{Binding BigEntryInput.Items}"
SelectedItem="{Binding BigEntryInput.SelectedItem}"
SelectedIndex="{Binding BigEntryInput.SelectedIndex}"
Grid.Column="0" Grid.Column="0"
Margin="0,0,10,0" Margin="0,0,10,0"
/> />
<Button <Button
Content="确认" Content="确认"
Command="{Binding BigEntryInput.SelectCommand}"
CommandParameter="{Binding}"
Grid.Column="1" Grid.Column="1"
Margin="0" Margin="0"
Foreground="ForestGreen" >
BorderBrush="ForestGreen" Template="{DynamicResource ButtonBaseControlTemplate1}" <Button.Style>
<Style
BasedOn="{StaticResource ButtonStyle}"
TargetType="Button"
>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Value="False">
<Condition.Binding>
<MultiBinding Converter="{StaticResource MultiValueEqualityConverter}">
<Binding Path="BigEntryInput.LastProcessedManifest"></Binding>
<Binding Path="BigEntryInput.SelectedItem"></Binding>
</MultiBinding>
</Condition.Binding>
</Condition>
<Condition
Binding="{Binding RelativeSource={RelativeSource Mode=Self},
Path=IsEnabled}"
Value="True"
/> />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="#20FF30" />
<Setter Property="BorderBrush" Value="#20FF30" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid> </Grid>
<DockPanel <Grid
DockPanel.Dock="Top" DockPanel.Dock="Top"
Height="25" Height="50"
Margin="0,10" Margin="0,10"
> >
<Button <Grid.RowDefinitions>
DockPanel.Dock="Right" <RowDefinition />
Content="取消加载" <RowDefinition />
Grid.Column="1" </Grid.RowDefinitions>
Width="92" <TextBlock
/> Text="{Binding StatusText}"
<Button Grid.Row="0"
DockPanel.Dock="Right"
Content="加载 csf / mod.str"
Grid.Column="1"
Width="140"
Margin="10,0" Margin="10,0"
VerticalAlignment="Center"
/> />
<TextBlock <TextBlock
Text="正在加载……" Text="{Binding Entries.Count, StringFormat=目前加载了{0}个素材}"
Grid.Row="1"
Margin="10,5,0,5"
Width="160"
HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Grid.Column="0" Visibility="{Binding Entries.Count, Converter={StaticResource NonZeroToVisibilityConverter}}"
/> />
</DockPanel>
<l:InputBar <CheckBox
HintText="过滤Asset ID可选" Content="只显示 GameObject"
Collection="{Binding Items}" IsChecked="True"
SelectedItem="{Binding SelectedItem}" Grid.Row="1"
Margin="0,5,242,5"
Width="155"
HorizontalAlignment="Right"
VerticalAlignment="Center"
/>
<Button
Content="加载 csf/mod.str"
Grid.Row="1"
Width="140"
Margin="0,0,97,0"
HorizontalAlignment="Right"
/>
<Button
Content="取消加载"
Grid.Row="1"
Width="92"
HorizontalAlignment="Right"
/>
</Grid>
<Grid
DockPanel.Dock="Top" DockPanel.Dock="Top"
Height="25" Height="25"
>
<TextBox></TextBox>
<TextBlock
Text="过滤Asset ID可选"
Padding="5,0"
VerticalAlignment="Center"
/> />
</Grid>
<DataGrid <DataGrid
x:Name="DataGrid" ItemsSource="{Binding Entries}"
AutoGenerateColumns="False"
ScrollViewer.CanContentScroll="True" ScrollViewer.CanContentScroll="True"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
EnableRowVirtualization="True" EnableRowVirtualization="True"
VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.VirtualizationMode="Recycling"
DockPanel.Dock="Top" />
>
<DataGrid.Columns>
<DataGridTextColumn Header="Asset ID" Width="200" Binding="{Binding AssetId}"/>
<DataGridTextColumn Header="哈希" Width="*" Binding="{Binding Hash}"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel> </DockPanel>
<TextBlock <StackPanel
x:Name="textBlock"
Grid.Row="1" Grid.Row="1"
Margin="0,10,0,0" Margin="0,10,0,0"
TextWrapping="Wrap" ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
> >
<TextBlock TextWrapping="Wrap">
本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak /> 本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak />
此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的 此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
<l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink> <l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink>
@ -230,5 +197,17 @@
<LineBreak /> <LineBreak />
假如你对本工具有任何疑问或者建议的话,可以来到<l:ShellLink NavigateUri="https://tieba.baidu.com/ra3">红警3吧</l:ShellLink>发帖寻找岚依( 假如你对本工具有任何疑问或者建议的话,可以来到<l:ShellLink NavigateUri="https://tieba.baidu.com/ra3">红警3吧</l:ShellLink>发帖寻找岚依(
</TextBlock> </TextBlock>
<TextBlock />
<StackPanel
Height="25"
HorizontalAlignment="Right"
>
<Button
Content="清除输出"
Width="92"
Grid.Row="1"
/>
</StackPanel>
</StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@ -21,65 +21,37 @@ namespace HashCalculator.GUI
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
internal ViewModel ViewModel => (ViewModel)DataContext; internal ViewModel ViewModel => (ViewModel)DataContext;
private bool _autoscroll = true;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
} }
private void OnMainInputTextChanged(object sender, TextChangedEventArgs e) private void OnButtomScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
{ {
CopyableBox.ShowDialog(async token => var scrollViewer = (ScrollViewer)e.Source;
{ // User scroll event : set or unset auto-scroll mode
var init = DateTimeOffset.UtcNow; if (e.ExtentHeightChange == 0)
var i = 0; { // Content unchanged : user scroll event
while (i < 100) if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{ { // Scroll bar is in bottom
token.ThrowIfCancellationRequested(); // Set auto-scroll mode
await Task.Delay(100).ConfigureAwait(false); _autoscroll = true;
++i;
} }
MessageBox.Show("Completed!"); else
return $"Completed after exactly {DateTimeOffset.UtcNow - init}"; { // Scroll bar isn't in bottom
}); // Unset auto-scroll mode
/*var mainInput = _viewModel.MainInput; _autoscroll = false;
if (sender is InputBar)
{
var inputValue = mainInput.Text;
if(inputValue != null)
{
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
} }
}*/
} }
private void OnBigInputTextChanged(object sender, TextChangedEventArgs e) // Content scroll event : auto-scroll eventually
{ if (_autoscroll && e.ExtentHeightChange != 0)
/*var mainInput = _viewModel.MainInput; { // Content changed and auto-scroll mode set
if (sender is InputBar) // Autoscroll
{ scrollViewer.ScrollToVerticalOffset(scrollViewer.ExtentHeight);
var inputValue = mainInput.Text;
if (inputValue != null)
{
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
} }
}*/
}
private void OnAssetInputTextChanged(object sender, TextChangedEventArgs e)
{
/*var mainInput = _viewModel.MainInput;
if (sender is InputBar)
{
var inputValue = mainInput.Text;
if (inputValue != null)
{
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
}
}*/
} }
} }
} }

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TechnologyAssembler.Core.Assets;
namespace HashCalculator.GUI
{
public class ModManifest
{
IReadOnlyCollection<AssetEntry> Entries;
public ModManifest()
{
var x = Manifest.Load("");
}
}
}

View File

@ -18,6 +18,15 @@ namespace HashCalculator.GUI
return FastHash.GetHashCode(content); return FastHash.GetHashCode(content);
} }
public static uint CalculateLauncherBinaryHash(byte[] content)
{
if (content == null)
{
throw new ArgumentNullException($"{nameof(content)} is null");
}
return FastHash.GetHashCodeLauncher(0, content);
}
public static uint CalculateBinaryHash(string content) public static uint CalculateBinaryHash(string content)
{ {
if (content == null) if (content == null)

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
@ -46,74 +47,52 @@ namespace HashCalculator.GUI
[SuppressMessage("Microsoft.Performance", "CA1812")] [SuppressMessage("Microsoft.Performance", "CA1812")]
internal class ViewModel : NotifyPropertyChanged internal class ViewModel : NotifyPropertyChanged
{ {
public MainInputViewModel MainInput { get; } = new MainInputViewModel(); private static readonly Random _random = new Random();
public BigInputViewModel BigEntryInput { get; } = new BigInputViewModel();
public InputBarViewModel AssetIdInput { get; } = new InputBarViewModel();
private Visibility _bigInputVisibility = Visibility.Collapsed; public MainInputViewModel MainInput { get; }
public Visibility BigInputVisibility public BigInputViewModel BigEntryInput { get; }
private ObservableCollection<DisplayAssetEntry> _entries = new ObservableCollection<DisplayAssetEntry>();
public ObservableCollection<DisplayAssetEntry> Entries
{ {
get => _bigInputVisibility; get => _entries;
set => SetField(ref _bigInputVisibility, value); set => SetField(ref _entries, value);
} }
private InputEntry? _mainInputResult; // if not null private string _statusText = SuggestionString("不知道该显示些什么呢……");
private string? _bigManifest; public string StatusText
{
get => _statusText;
set => SetField(ref _statusText, value);
}
private string _traceText = string.Empty;
public string TraceText
{
get => _traceText;
set => SetField(ref _traceText, value);
}
public ViewModel() public ViewModel()
{ {
var controller = new Controller(this);
MainInput = new MainInputViewModel(controller);
BigEntryInput = new BigInputViewModel(controller);
} }
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")] public static string SuggestionString(string original)
public async Task OnMainInputDecided(InputEntry selected)
{ {
BigInputVisibility = Visibility.Collapsed; var generated = $"{_random.NextDouble()}";
switch (selected.Type) System.Diagnostics.Debug.WriteLine(generated);
if (generated.Contains("38") || generated.Contains("16"))
{ {
case InputEntryType.BinaryFile: return "你们都是喂鱼的马甲!(";
CopyableBox.ShowDialog(async cancel =>
{
try
{
var hash = await Task.Run(() =>
{
return SageHash.CalculateBinaryHash(File.ReadAllBytes(selected.Value));
}).ConfigureAwait(false);
return $"使用Sage Hash计算出的哈希值{hash:X8} (十进制 {hash}\r\n"
+ "注意这是以大小写敏感模式计算出的哈希值与素材ID大小写不敏感的哈希其实并不完全一样";
} }
catch (Exception exception) if (generated.IndexOf('2') == 2)
{ {
return exception.ToString(); return "本来以为两小时就能写完这个小工具没想到写了两个星期开始怀疑自己的智商orz";
} }
}); return original;
break;
case InputEntryType.BigFile:
BigInputVisibility = Visibility.Visible;
throw new NotImplementedException();
case InputEntryType.ManifestFile:
await ProcessManifest(selected).ConfigureAwait(true);
break;
case InputEntryType.XmlFile:
throw new NotImplementedException();
default:
throw new NotSupportedException();
}
}
public async Task ProcessManifest(InputEntry entry)
{
var content = await Task.Run(() =>
{
using var stream = File.OpenRead(entry.Value);
return new ManifestContent(entry.Text, stream);
}).ConfigureAwait(true);
await ProcessManifest(content).ConfigureAwait(true);
}
public async Task ProcessManifest(ManifestContent content)
{
throw new NotImplementedException();
} }
} }
@ -133,6 +112,13 @@ namespace HashCalculator.GUI
set => SetField(ref _selectedItem, value); set => SetField(ref _selectedItem, value);
} }
private int _selectedIndex;
public virtual int SelectedIndex
{
get => _selectedIndex;
set => SetField(ref _selectedIndex, value);
}
private string? _text; private string? _text;
public virtual string? Text public virtual string? Text
{ {
@ -145,10 +131,9 @@ namespace HashCalculator.GUI
{ {
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions(); private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
private int _selectedIndex; public override int SelectedIndex
public int SelectedIndex
{ {
get => _selectedIndex; get => base.SelectedIndex;
set set
{ {
if (value == 0) if (value == 0)
@ -159,10 +144,10 @@ namespace HashCalculator.GUI
} }
else else
{ {
value = _selectedIndex < value ? 1 : -1; value = base.SelectedIndex < value ? 1 : -1;
} }
} }
SetField(ref _selectedIndex, value); base.SelectedIndex = value;
} }
} }
@ -182,7 +167,7 @@ namespace HashCalculator.GUI
public Command<MainWindow> BrowseCommand { get; } public Command<MainWindow> BrowseCommand { get; }
public Command<ViewModel> SelectCommand { get; } public Command<ViewModel> SelectCommand { get; }
public MainInputViewModel() public MainInputViewModel(Controller controller)
{ {
Items = Enumerable.Empty<InputEntry>(); Items = Enumerable.Empty<InputEntry>();
BrowseCommand = new Command<MainWindow>(window => BrowseCommand = new Command<MainWindow>(window =>
@ -200,7 +185,7 @@ namespace HashCalculator.GUI
}); });
SelectCommand = new Command<ViewModel>(async viewModel => SelectCommand = new Command<ViewModel>(async viewModel =>
{ {
if(SelectedItem?.IsValid != true) if (SelectedItem?.IsValid != true)
{ {
return; return;
} }
@ -209,7 +194,7 @@ namespace HashCalculator.GUI
{ {
BrowseCommand.CanExecuteValue = false; BrowseCommand.CanExecuteValue = false;
SelectCommand.CanExecuteValue = false; SelectCommand.CanExecuteValue = false;
await viewModel.OnMainInputDecided(SelectedItem).ConfigureAwait(true); await controller.OnMainInputDecided(SelectedItem).ConfigureAwait(true);
} }
finally finally
{ {
@ -246,102 +231,105 @@ namespace HashCalculator.GUI
} }
} }
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")] internal class BigInputViewModel : InputBarViewModel
internal class BigInputViewModel : NotifyPropertyChanged
{ {
private CancellationTokenSource? _currentCancellator = null; public override IEnumerable<InputEntry>? Items
private IEnumerable<ManifestContent>? _manifests;
public IEnumerable<ManifestContent>? Manifests
{ {
get => _manifests; get => base.Items;
set => SetField(ref _manifests, value); set => base.Items = value;
} }
private ManifestContent? _selectedManifest; public override InputEntry? SelectedItem
public ManifestContent? SelectedManifest
{ {
get => _selectedManifest; get => base.SelectedItem;
set set
{ {
SetField(ref _selectedManifest, value); base.SelectedItem = value;
SelectCommand.CanExecuteValue = value != null; SelectCommand.CanExecuteValue = value != null;
} }
} }
public override string? Text
{
get => base.Text;
set
{
base.Text = value;
if (value != SelectedItem?.ToString())
{
UpdateList(value);
}
}
}
private InputEntry? _lastProcessedManifest;
public InputEntry? LastProcessedManifest
{
get => _lastProcessedManifest;
set => SetField(ref _lastProcessedManifest, value);
}
private IEnumerable<InputEntry>? _allManifests;
public IEnumerable<InputEntry>? AllManifests
{
get => _allManifests;
set
{
SetField(ref _allManifests, value);
Items = value;
LastProcessedManifest = null;
Text = null;
}
}
public Command<ViewModel> SelectCommand { get; } public Command<ViewModel> SelectCommand { get; }
public BigInputViewModel() [SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
public BigInputViewModel(Controller controller)
{ {
SelectCommand = new Command<ViewModel>(viewModel => viewModel.ProcessManifest(SelectedManifest!)); SelectCommand = new Command<ViewModel>(async viewModel =>
}
public async Task LoadBig(string path)
{ {
using var cancellator = _currentCancellator = new CancellationTokenSource(); var mainInput = viewModel.MainInput;
var saved = Interlocked.Exchange(ref _currentCancellator, cancellator);
saved?.Cancel();
var token = cancellator.Token;
try try
{ {
using var big = await Task.Run(() => new BigFile(path), token).ConfigureAwait(true); SelectCommand.CanExecuteValue = false;
var manifests = await Task.Run(() => mainInput.BrowseCommand.CanExecuteValue = false;
mainInput.SelectCommand.CanExecuteValue = false;
LastProcessedManifest = SelectedItem;
await controller.ProcessManifest(SelectedItem!.Value).ConfigureAwait(true);
}
catch (Exception exception)
{ {
var manifests = big.GetFiles(string.Empty, "*.manifest", VirtualSearchOptionType.AllDirectories); CopyableBox.ShowDialog(_ =>
var modManifests = from manifest in manifests {
where FileNameStartsWith(manifest, "mod") return Task.FromResult($"在加载 manifest 时发生错误:{exception}");
});
}
finally
{
SelectCommand.CanExecuteValue = true;
mainInput.BrowseCommand.CanExecuteValue = true;
mainInput.SelectCommand.CanExecuteValue = true;
}
})
{
CanExecuteValue = false
};
}
private void UpdateList(string? input)
{
input ??= string.Empty;
input = input.Replace(VirtualFileSystem.AltDirectorySeparatorChar, VirtualFileSystem.DirectorySeparatorChar);
var filtered = from manifest in AllManifests
where manifest.Value.IndexOf(input, StringComparison.OrdinalIgnoreCase) != -1
select manifest; select manifest;
var globalDataManifests = from manifest in manifests Items = filtered;
where FileNameStartsWith(manifest, "mapmetadata") if (Items.FirstOrDefault()?.Value.Equals(input, StringComparison.OrdinalIgnoreCase) == true)
select manifest;
var firstManifests = modManifests.Concat(globalDataManifests);
var otherManifests = from manifest in manifests
where !firstManifests.Contains(manifest)
select manifest;
var list = new List<ManifestContent>();
foreach (var path in firstManifests.Concat(otherManifests))
{ {
token.ThrowIfCancellationRequested(); SelectedIndex = 0;
using var stream = big.OpenStream(path);
list.Add(new ManifestContent(path, stream));
} }
return list;
}, token).ConfigureAwait(true);
if (!token.IsCancellationRequested)
{
Manifests = manifests;
}
}
catch (OperationCanceledException) { }
Interlocked.CompareExchange(ref _currentCancellator, null, cancellator);
}
private static bool FileNameStartsWith(string path, string what)
{
return Path.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
}
}
internal class ManifestContent
{
public string Name { get; }
public byte[] Data { get; }
public ManifestContent(string name, Stream stream)
{
Name = name;
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
Data = memoryStream.ToArray();
}
public override string ToString()
{
return Name;
} }
} }
} }