Files
rqoverlay/src/RQOverlay/SessionViewModel.cs

190 lines
6.8 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using RQOverlay.Core;
using RQOverlay.Plugins;
namespace RQOverlay;
/// <summary>Состояние текущей сессии фарма: время, показатели включённых плагинов, итог.</summary>
public sealed class SessionViewModel : Observable
{
readonly PriceBook _prices;
readonly PluginManager _plugins;
DateTime _start = DateTime.Now;
public SessionViewModel(PriceBook prices, PluginManager plugins)
{
_prices = prices;
_plugins = plugins;
_plugins.Changed += Rebuild;
Rebuild();
}
/// <summary>
/// Ручная корректировка заработка в золоте (например, минус потраченное на зелья).
/// Показывается в оверлее отдельной строкой, только когда не ноль.
/// </summary>
public Metric Adjustment { get; } = new("Корректировка", "#9AA3B5", isGold: true);
public string AdjustmentText
{
get => Adjustment.Value == 0 ? "" : Adjustment.Value.ToString("0.##", CultureInfo.CurrentCulture);
set
{
string s = (value ?? "").Trim().Replace(" ", "").Replace(" ", "").Replace(',', '.');
decimal v;
if (s.Length == 0 || s == "-") v = 0;
else if (!decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out v)) return;
bool wasShown = Adjustment.Value != 0;
Adjustment.Value = v;
Raise();
if (wasShown != (v != 0)) Rebuild();
else RaiseTotals();
}
}
/// <summary>Показатели всех включённых плагинов, по порядку.</summary>
public ObservableCollection<Metric> Metrics { get; } = new();
/// <summary>Золотые показатели — для полоски долей.</summary>
public ObservableCollection<Metric> GoldMetrics { get; } = new();
/// <summary>Дополнительные блоки плагинов в оверлее (список добычи и т.п.).</summary>
public ObservableCollection<FrameworkElement> OverlayViews { get; } = new();
decimal TotalGold => GoldMetrics.Sum(m => m.Value);
double Hours => (DateTime.Now - _start).TotalHours;
public bool HasGold => GoldMetrics.Count > 0;
public bool HasIncome => TotalGold > 0;
public string Total => Fmt.Gold(TotalGold);
public string PerHour =>
// Первую минуту темп скачет слишком сильно, чтобы что-то значить.
Hours < 1.0 / 60 ? "— / ч" : Fmt.Gold(TotalGold / (decimal)Hours) + " / ч";
public string Elapsed
{
get
{
var t = DateTime.Now - _start;
return $"{(int)t.TotalHours}:{t.Minutes:00}:{t.Seconds:00}";
}
}
/// <summary>Оверлей пропускает клики мыши в игру.</summary>
bool _clickThrough;
public bool ClickThrough { get => _clickThrough; set => Set(ref _clickThrough, value); }
bool _listExpanded = true;
public bool ListExpanded { get => _listExpanded; set => Set(ref _listExpanded, value); }
bool _connected;
public bool Connected { get => _connected; set => Set(ref _connected, value); }
string _status = "Запуск…";
public string Status { get => _status; set => Set(ref _status, value); }
/// <summary>Раз в секунду: таймер и темп.</summary>
public void Tick()
{
double hours = Hours;
foreach (var m in Metrics) m.UpdateRate(hours);
Raise(nameof(Elapsed));
Raise(nameof(PerHour));
}
public void Reset()
{
_start = DateTime.Now;
_plugins.ResetSession();
AdjustmentText = "";
Tick();
}
/// <param name="initial">Первый снимок — история до запуска оверлея.</param>
/// <param name="countHistory">Засчитывать ли историю в сессию (флаг --history).</param>
public void Apply(IEnumerable<LogEntry> entries, bool initial, bool countHistory)
{
foreach (var entry in entries)
{
var events = GameEvents.Parse(entry);
if (events.Count == 0) continue;
// Цены учим из любых продаж, в том числе из истории лога.
foreach (var s in events.OfType<ItemSold>())
_prices.LearnFromSale(s.Name, s.Qty, s.Total);
if (!initial)
_plugins.Dispatch(entry, events, isLive: true);
else if (countHistory)
_plugins.Dispatch(entry, events, isLive: false);
}
}
/// <summary>Набор включённых плагинов изменился — пересобираем оверлей.</summary>
void Rebuild()
{
foreach (var m in Metrics) m.PropertyChanged -= OnMetricChanged;
Metrics.Clear();
GoldMetrics.Clear();
OverlayViews.Clear();
foreach (var plugin in _plugins.Enabled)
{
foreach (var m in plugin.Metrics)
{
Metrics.Add(m);
if (m.IsGold) GoldMetrics.Add(m);
m.PropertyChanged += OnMetricChanged;
}
try
{
if (plugin.CreateOverlayView() is { } view) OverlayViews.Add(view);
}
catch { /* сломанный блок плагина просто не показываем */ }
}
if (Adjustment.Value != 0)
{
Metrics.Add(Adjustment);
GoldMetrics.Add(Adjustment);
Adjustment.PropertyChanged += OnMetricChanged;
}
Tick();
RaiseTotals();
Raise(nameof(HasGold));
}
void OnMetricChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Metric.Value) && sender is Metric { IsGold: true })
RaiseTotals();
}
void RaiseTotals()
{
Raise(nameof(Total)); Raise(nameof(PerHour)); Raise(nameof(HasIncome));
}
}
public static class Fmt
{
static readonly NumberFormatInfo Nfi = new() { NumberGroupSeparator = " ", NumberDecimalSeparator = "," };
public static string Gold(decimal v) => Math.Round(v).ToString("#,0", Nfi);
public static string Gold(long v) => v.ToString("#,0", Nfi);
public static string Plural(int n, string one, string few, string many)
{
int m10 = n % 10, m100 = n % 100;
if (m10 == 1 && m100 != 11) return one;
if (m10 is >= 2 and <= 4 && m100 is < 12 or > 14) return few;
return many;
}
}