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);
}
}
}