using AnotherReplayReader.Apm; using AnotherReplayReader.ReplayFile; using AnotherReplayReader.Utils; using OxyPlot; using OxyPlot.Annotations; using OxyPlot.Axes; using OxyPlot.Legends; using OxyPlot.Series; using System; using System.ComponentModel; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; namespace AnotherReplayReader { /// /// APM.xaml 的交互逻辑 /// internal partial class ApmWindow : Window { public static readonly TimeSpan DefaultResolution = TimeSpan.FromSeconds(15); private readonly Regex _resolutionRegex = new(@"[^0-9]+"); private readonly PlayerIdentity _identity; private readonly Replay _replay; private readonly Task _plotter; private readonly ApmWindowPlotController _plotController; public ApmWindowPlotViewModel PlotModel { get; } = new(); public TimeSpan PlotResolution { get; private set; } = DefaultResolution; public bool SkipUnknowns { get; private set; } = true; public bool SkipAutos { get; private set; } = true; public bool SkipClicks { get; private set; } = false; public ApmWindow(Replay replay, PlayerIdentity identity) { _identity = identity; _replay = replay; _plotter = Task.Run(() => new ApmPlotter(replay)); _plotController = new(this); InitializeComponent(); } public async void FilterData(TimeSpan begin, TimeSpan end, bool updatePlot) { await FilterDataAsync(begin, end, updatePlot); } private async void OnApmWindowLoaded(object sender, RoutedEventArgs e) { _plot.Controller = _plotController; _resolution.Text = PlotResolution.TotalSeconds.ToString(); _skipUnknowns.IsChecked = SkipUnknowns; _skipAutos.IsChecked = SkipAutos; _skipClicks.IsChecked = SkipClicks; await InitializeApmWindowData(); } private async Task InitializeApmWindowData() { if (_identity.IsUsable) { _setPlayerButton.IsEnabled = true; _setPlayerButton.Visibility = Visibility.Visible; } else { _setPlayerButton.IsEnabled = false; _setPlayerButton.Visibility = Visibility.Hidden; } await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true); _label.Content = "选择表格之后按 Ctrl+C 可以复制内容"; } private async Task FilterDataAsync(TimeSpan begin, TimeSpan end, bool updatePlot) { try { await FilterDataAsyncThrowable(begin, end, updatePlot); } catch (Exception e) { MessageBox.Show($"加载录像信息失败:\r\n{e}"); } } private async Task FilterDataAsyncThrowable(TimeSpan begin, TimeSpan end, bool updatePlot) { var resolution = PlotResolution; var options = new ApmPlotterFilterOptions(!SkipUnknowns, !SkipAutos, !SkipClicks); var (dataList, plotData, replayLength, isPartial) = await Task.Run(async () => { var plotter = await _plotter.ConfigureAwait(false); var list = DataRow.GetList(plotter, options, _identity, begin, end, out var apmIndex); // avg apm var avg = plotter.CalculateAverageApm(options); // data for plotting and partial apm var data = plotter.GetPoints(resolution, options); // partial apm var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength; if (isPartial) { var instantApms = ApmPlotter.CalculateInstantApm(data, begin, end); string PartialApmToString(double v, int i) { if (plotter.Players[i].IsComputer) { return "这是 AI"; } return plotter.PlayerLifes[i] < begin ? "玩家已战败" : $"{v:#.##}"; } list.Insert(apmIndex, new(DataRow.PartialApmRow, instantApms.Select(PartialApmToString))); } list.Insert(apmIndex, new(DataRow.AverageApmRow, avg.Select(v => $"{v:#.##}"))); return (list, data, plotter.ReplayLength, isPartial); }); if (updatePlot) { BuildPlot(plotData, replayLength); } _table.Items.Clear(); foreach (var row in dataList) { _table.Items.Add(row); } _table.Columns[1].Header = dataList[0].Player1Value; _table.Columns[2].Header = dataList[0].Player2Value; _table.Columns[3].Header = dataList[0].Player3Value; _table.Columns[4].Header = dataList[0].Player4Value; _table.Columns[5].Header = dataList[0].Player5Value; _table.Columns[6].Header = dataList[0].Player6Value; if (isPartial) { _label.Content = $"{(ShortTimeSpan)begin} 至 {(ShortTimeSpan)end} 之间的 APM 数据"; _label.FontWeight = System.Windows.FontWeights.Bold; } else { _label.Content = "以下是整局游戏的 APM 数据:"; _label.FontWeight = System.Windows.FontWeights.Normal; } } private void BuildPlot(DataPoint[][] data, TimeSpan replayLength) { var model = new PlotModel(); model.Annotations.Add(new ApmWindowPlotTextAnnotation { Text = "鼠标左键拖拽选择,滚轮缩放,右键拖拽移动", }); var maxApm = data.SelectMany(x => x).Max(x => x.Y); var yaxis = new LinearAxis { Position = AxisPosition.Right, IsZoomEnabled = false, AbsoluteMinimum = -maxApm / 10, AbsoluteMaximum = maxApm, MajorStep = 50, }; var lengthInSeconds = replayLength.TotalSeconds; var limit = lengthInSeconds / 10; var xaxis = new TimeSpanAxis { Position = AxisPosition.Bottom, AbsoluteMinimum = -limit, AbsoluteMaximum = lengthInSeconds + limit, }; model.Axes.Add(yaxis); model.Axes.Add(xaxis); foreach (var (points, i) in data.Select((v, i) => (v, i))) { var series = new LineSeries { Title = _replay.Players[i].PlayerName, }; series.Points.AddRange(points); model.Series.Add(series); model.Legends.Add(new Legend()); } PlotModel.Model = model; } private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e) { var window1 = new Window1(_identity); window1.ShowDialog(); } private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e) { _table.SelectAll(); } private int NormalizeResolutionInput(string text) { if(!int.TryParse(text, out var value)) { value = (int)PlotResolution.TotalSeconds; } return Math.Min(Math.Max(1, value), 3600); } private void OnResolutionPreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = _resolutionRegex.IsMatch(e.Text); } private async void OnResolutionTextChanged(object sender, EventArgs e) { var seconds = NormalizeResolutionInput(_resolution.Text); if (Math.Abs(seconds - PlotResolution.TotalSeconds) >= 0.5) { PlotResolution = TimeSpan.FromSeconds(seconds); _resolution.Text = seconds.ToString(); _plotController.DiscardRectangle(); await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true); } } private async void OnSkipUnknownsCheckedChanged(object sender, RoutedEventArgs e) { if (_skipUnknowns.IsChecked != SkipUnknowns) { SkipUnknowns = _skipUnknowns.IsChecked is true; _plotController.DiscardRectangle(); await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true); } } private async void OnSkipAutosCheckedChanged(object sender, RoutedEventArgs e) { if (_skipAutos.IsChecked != SkipAutos) { SkipAutos = _skipAutos.IsChecked is true; _plotController.DiscardRectangle(); await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true); } } private async void OnSkipClicksCheckedChanged(object sender, RoutedEventArgs e) { if (_skipClicks.IsChecked != SkipClicks) { SkipClicks = _skipClicks.IsChecked is true; _plotController.DiscardRectangle(); await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true); } } } internal class ApmWindowPlotViewModel : INotifyPropertyChanged { private PlotModel _model = new(); public event PropertyChangedEventHandler? PropertyChanged; public PlotModel Model { get => _model; set { _model = value; PropertyChanged?.Invoke(this, new(nameof(Model))); } } } internal class ApmWindowPlotController : PlotController { private readonly ApmWindow _window; private RectangleAnnotation? _current; private PlotModel Plot => _window.PlotModel.Model; private double Resolution => _window.PlotResolution.TotalSeconds; // private readonly Func public ApmWindowPlotController(ApmWindow window) { _window = window; } public void DiscardRectangle() { _current = null; RemoveSelections(); Plot.InvalidatePlot(false); } public override bool HandleMouseDown(IView view, OxyMouseDownEventArgs args) { TryBeginDragRectagle(args); return base.HandleMouseDown(view, args); } public override bool HandleMouseMove(IView view, OxyMouseEventArgs args) { TryDragRectangle(args); return base.HandleMouseMove(view, args); } public override bool HandleMouseUp(IView view, OxyMouseEventArgs args) { TryEndDragRectangle(); return base.HandleMouseUp(view, args); } private void RemoveSelections() { var list = Plot.Annotations .Select((x, i) => (Item: x, Index: i)) .Where(t => t.Item is RectangleAnnotation) .Reverse() .ToArray(); foreach (var (_, index) in list) { Plot.Annotations.RemoveAt(index); } } private void TryBeginDragRectagle(OxyMouseDownEventArgs args) { if (args.ChangedButton == OxyMouseButton.Left) { var x = Plot.Axes[1].InverseTransform(args.Position.X); x = Math.Round(x / Resolution) * Resolution; RemoveSelections(); _current = new() { ClipByYAxis = true, Fill = OxyColor.FromArgb(128, 0, 128, 255), MinimumY = double.NegativeInfinity, MaximumY = double.PositiveInfinity, MinimumX = x, MaximumX = x }; Plot.Annotations.Add(_current); Plot.InvalidatePlot(false); } } private void TryDragRectangle(OxyMouseEventArgs args) { if (_current is not null) { var x = Plot.Axes[1].InverseTransform(args.Position.X); var offsetMultiplier = Math.Round((x - _current.MinimumX) / Resolution); x = offsetMultiplier * Resolution; _current.MaximumX = _current.MinimumX + x; if (_current.MaximumX < _current.MinimumX) { var temp = _current.MinimumX; _current.MinimumX = _current.MaximumX; _current.MaximumX = temp; } Plot.InvalidatePlot(false); } } private void TryEndDragRectangle() { if (_current is not null) { if (Math.Abs(_current.MaximumX - _current.MinimumX) < Resolution) { RemoveSelections(); Plot.InvalidatePlot(false); _window.FilterData(TimeSpan.MinValue, TimeSpan.MaxValue, false); } else { _window.FilterData(TimeSpan.FromSeconds(_current.MinimumX), TimeSpan.FromSeconds(_current.MaximumX), false); } _current = null; } } } public class ApmWindowPlotTextAnnotation : Annotation { public string Text { get; set; } = string.Empty; public double X { get; set; } = 8; public double Y { get; set; } = 8; public override void Render(IRenderContext rc) { base.Render(rc); double pX = PlotModel.PlotArea.Left + X; double pY = PlotModel.PlotArea.Top + Y; rc.DrawMultilineText(new(pX, pY), Text, PlotModel.TextColor, PlotModel.DefaultFont, PlotModel.DefaultFontSize, PlotModel.SubtitleFontWeight); } } }