JD2022-TU1/main/tools/modules/JD.Guinness.WPF.Module/ViewModel/GameViewModel.cs

595 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using JD.Guinness.WPF.Module.Configuration;
using JD.Guinness.WPF.Module.Extension;
using JD.Guinness.WPF.Module.Model;
using JD.Guinness.WPF.Module.Model.Ps4;
using JD.Guinness.WPF.Module.Model.Ps5;
using JD.Guinness.WPF.Module.Model.Stadia;
using JD.Guinness.WPF.Module.Model.XBoxOne;
using JD.Guinness.WPF.Module.Model.XBoxSeriesX;
using JD.Guinness.WPF.Module.View;
using JD.Oasis.Service.API;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Plume.Core.Logging;
using Plume.Interfaces.Settings;
using Plume.Interfaces.WPF.Notifications;
using Plume.Interfaces.WPF.StatusBar;
using Plume.Perforce.Service.Api;
using Plume.WPF.Mvvm;
namespace JD.Guinness.WPF.Module.ViewModel
{
public sealed class GameViewModel : BindableBase
{
#region Fields
private readonly IPerforceService _perforceService;
private readonly IOasisService _oasisService;
private readonly ISettingsManager _settingsManager;
private readonly IStatusBarManager _statusBarManager;
private readonly INotificationManager _notificationManager;
private readonly ILogger _logger;
private readonly ContentControl _tryLockElement;
private IEnumerable<LocalizedLine> _oasisLines;
private bool _isAvailable;
private bool _isWorking;
#endregion
#region Properties
public event Func<IEnumerable<Type>, Task> GeneratePlatfromsAsked;
public event Action<GuinnessSettings> SaveAsked;
public DelegateCommand<LanguageInfo> DuplicateLanguageCommand { get; }
public DelegateCommand<LanguageInfo> RemoveLanguageCommand { get; }
public AsyncDelegateCommand<bool> SetWorkspaceCommand { get; }
public AsyncDelegateCommand LoadOasisDataCommand { get; }
public DelegateCommand<bool> SaveCommand { get; }
public bool HasWorkspace => !string.IsNullOrEmpty(Settings.WorkspacePath)
&& !string.IsNullOrEmpty(Settings.GameSaveFilePath)
&& File.Exists(Path.Combine(Settings.WorkspacePath, Settings.GameSaveFilePath));
public ItemFocuser ItemFocuser { get; }
public GuinnessSettings Settings { get; }
public DispatcherObservableCollection<LanguageInfo> Languages { get; }
public Ps4Configuration Ps4Configuration { get; }
public Ps5Configuration Ps5Configuration { get; }
public XBoxOneConfiguration XBoxOneConfiguration { get; }
public XBoxSeriesXConfiguration XBoxSeriesXConfiguration { get; }
public StadiaConfiguration StadiaConfiguration { get; }
public DispatcherObservableCollection<Challenge> Challenges { get; }
public IEnumerable<LocalizedLine> OasisLines
{
get => _oasisLines;
private set => SetProperty(ref _oasisLines, value);
}
public bool IsAvailable
{
get => _isAvailable && !IsWorking;
private set => SetProperty(ref _isAvailable, value);
}
public bool IsWorking
{
get => _isWorking;
private set
{
if (SetProperty(ref _isWorking, value))
{
OnPropertyChanged(nameof(IsAvailable));
}
}
}
#endregion
#region Initialisation
public GameViewModel(
IPerforceService perforceService,
IOasisService oasisService,
ISettingsManager settingsManager,
IStatusBarManager statusBarManager,
INotificationManager notificationManager,
ILogger logger,
ItemFocuser itemFocuser)
{
_perforceService = perforceService;
_oasisService = oasisService ?? throw new ArgumentNullException(nameof(oasisService));
_settingsManager = settingsManager ?? throw new ArgumentNullException(nameof(settingsManager));
_statusBarManager = statusBarManager ?? throw new ArgumentNullException(nameof(statusBarManager));
_notificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
ItemFocuser = itemFocuser ?? throw new ArgumentNullException(nameof(itemFocuser));
Settings = _settingsManager.Get<GuinnessSettings>();
Languages = new DispatcherObservableCollection<LanguageInfo>();
Ps4Configuration = new Ps4Configuration();
Ps5Configuration = new Ps5Configuration();
XBoxOneConfiguration = new XBoxOneConfiguration();
XBoxSeriesXConfiguration = new XBoxSeriesXConfiguration();
StadiaConfiguration = new StadiaConfiguration();
Challenges = new DispatcherObservableCollection<Challenge>();
OasisLines = Array.Empty<LocalizedLine>();
DuplicateLanguageCommand = new DelegateCommand<LanguageInfo>(OnDuplicateLanguage, _ => !IsWorking);
RemoveLanguageCommand = new DelegateCommand<LanguageInfo>(OnRemoveLanguage, i => !IsWorking && i.Id < 0);
SetWorkspaceCommand = new AsyncDelegateCommand<bool>(OnSetWorkspace, _ => !IsWorking);
LoadOasisDataCommand = new AsyncDelegateCommand(OnLoadOasisData, () => !IsWorking);
SaveCommand = new DelegateCommand<bool>(OnSave, _ => IsAvailable && HasWorkspace);
_tryLockElement = new Button { Command = SetWorkspaceCommand, CommandParameter = false };
IsWorking = false;
IsAvailable = false;
}
#endregion
#region Callbacks
private void OnDuplicateLanguage(LanguageInfo info)
{
LanguageInfo newInfo = new LanguageInfo(Math.Min(0, Languages.Min(i => i.Id)) - 1)
{
Language = info.Language,
Ps4Id = info.Ps4Id,
Ps5Id = info.Ps5Id,
MicrosoftId = info.MicrosoftId,
GoogleId = info.GoogleId
};
Languages.Add(newInfo);
CheckLanguage(newInfo);
}
private void OnRemoveLanguage(LanguageInfo info)
{
void Remove(ICollection<Selectable<LanguageInfo>> infos)
{
Selectable<LanguageInfo> selectable = infos.FirstOrDefault(s => s.Value == info);
if (selectable != null)
{
selectable.IsSelected = false;
infos.Remove(selectable);
}
}
Languages.Remove(info);
foreach (Ps4Zone zone in Ps4Configuration.Zones)
{
Remove(zone.Languages);
}
Remove(Ps5Configuration.Languages);
Remove(XBoxOneConfiguration.Languages);
Remove(XBoxSeriesXConfiguration.Languages);
Remove(StadiaConfiguration.Languages);
}
private async Task OnSetWorkspace(bool select)
{
try
{
if (SaveCommand.CanExecute(true))
{
SaveCommand.Execute(true);
}
if ((!select && HasWorkspace) || WorkspaceSelectionBox.Show(_perforceService, Settings))
{
_settingsManager.Save();
FileInfo gameSave = new FileInfo(Path.Combine(Settings.WorkspacePath, Settings.GameSaveFilePath));
await StartProcess(
LoadWorkspace(gameSave),
$"Loading workspace from file \"{gameSave.FullName}\"",
$"Loaded workspace from file \"{gameSave.FullName}\"",
$"Failed to load workspace from file \"{gameSave.FullName}\"").ConfigureAwait(true);
}
}
catch (Exception exception)
{
_logger.Error("Failed to set workspace", exception);
_notificationManager.ShowError("Failed to set workspace");
}
}
private async Task OnLoadOasisData() => await StartProcess(
Task.Run(async () =>
{
OasisLines = await _oasisService.GetLocalizedLinesAsync(null).ConfigureAwait(false);
ApplyOasisLanguages();
ApplyOasisLines(Challenges.ToList());
}),
"Loading oasis data",
"Loaded oasis data",
"Failed to load oasis data").ConfigureAwait(true);
private void OnSave(bool isExiting)
{
if (!HasWorkspace)
{
return;
}
try
{
SaveAsked?.Invoke(Settings);
_settingsManager.Save();
string data = JsonConvert.SerializeObject(
new Game
{
Save = JObject.FromObject(new Save
{
Languages = Languages.Select(l => l.ToSave()).ToArray(),
Ps4 = Ps4Configuration.ToSave(Settings.WorkspacePath),
Ps5 = Ps5Configuration.ToSave(Settings.WorkspacePath),
XBoxOne = XBoxOneConfiguration.ToSave(Settings.WorkspacePath),
XBoxSeriesX = XBoxSeriesXConfiguration.ToSave(Settings.WorkspacePath),
Stadia = StadiaConfiguration.ToSave(Settings.WorkspacePath),
Challenges = Challenges.Select(c => c.ToSave(Settings.WorkspacePath)).ToArray()
})
},
Formatting.Indented);
FileInfo gameSave = new FileInfo(Path.Combine(Settings.WorkspacePath, Settings.GameSaveFilePath));
if (data != File.ReadAllText(gameSave.FullName)
&& (!isExiting || MessageBox.Show("Do you want to save the changes you made?", "Guinness", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes))
{
File.WriteAllText(gameSave.FullName, data);
_notificationManager.ShowSuccess($"Saved to {gameSave.FullName}");
}
if (isExiting)
{
if (_perforceService.Connect(Settings.PerforceServer, Environment.UserName, Settings.WorkspaceName))
{
try
{
if (_perforceService.Connection.IsSubmittedInDepot(gameSave.FullName))
{
_perforceService.Connection.Revert(true, gameSave.FullName);
}
}
finally
{
_perforceService.Disconnect();
}
}
else
{
throw new Exception($"Unable to connect on the server '{Settings.PerforceServer}' with user '{Environment.UserName}' on client '{Settings.WorkspaceName}'.");
}
}
}
catch (Exception exception)
{
string error = $"Could not write game save to file '{Path.Combine(Settings.WorkspacePath, Settings.GameSaveFilePath)}'";
_logger.Error(error, exception);
_notificationManager.ShowError(error + Environment.NewLine + exception.Message);
}
}
#endregion
#region Methods
private void CheckLanguage(LanguageInfo info)
{
void CheckLanguage(ICollection<Selectable<LanguageInfo>> infos)
{
Selectable<LanguageInfo> selectable = infos.FirstOrDefault(l => l.Value.Id == info.Id);
if (selectable is null)
{
infos.Add(new Selectable<LanguageInfo> { Value = info });
}
else
{
selectable.Value = info;
}
}
foreach (Ps4Zone zone in Ps4Configuration.Zones)
{
CheckLanguage(zone.Languages);
}
CheckLanguage(Ps5Configuration.Languages);
CheckLanguage(XBoxOneConfiguration.Languages);
CheckLanguage(XBoxSeriesXConfiguration.Languages);
CheckLanguage(StadiaConfiguration.Languages);
}
private void ApplyOasisLanguages()
{
foreach (Language language in OasisLines.SelectMany(l => l.Translations.Keys).Distinct().Where(l => !Settings.IgnoredLanguages.Contains(l.Id)))
{
bool hasAny = false;
foreach (LanguageInfo info in Languages.Where(i => i.Language.Id == language.Id))
{
hasAny = true;
info.Language = language;
CheckLanguage(info);
}
if (!hasAny)
{
LanguageInfo info = new LanguageInfo(language.Id) { Language = language, Ps4Id = -1, MicrosoftId = string.Empty, GoogleId = string.Empty };
Languages.Add(info);
CheckLanguage(info);
}
}
}
private void ApplyOasisLines(IEnumerable<Challenge> challenges)
{
Dictionary<int, LocalizedLine> lines = OasisLines.ToDictionary(l => l.Id);
Ps4Configuration.TitleName = Ps4Configuration.TitleName.GetLineFrom(lines);
Ps4Configuration.TitleDescription = Ps4Configuration.TitleDescription.GetLineFrom(lines);
Ps5Configuration.TitleName = Ps5Configuration.TitleName.GetLineFrom(lines);
foreach (Challenge challenge in challenges)
{
challenge.Name = challenge.Name.GetLineFrom(lines);
challenge.Objective = challenge.Objective.GetLineFrom(lines);
XBoxOneAchievement xBoxOne = challenge.GetPlatformChallenge<XBoxOneAchievement>();
if (xBoxOne != null)
{
xBoxOne.LockedObjective = xBoxOne.LockedObjective.GetLineFrom(lines);
xBoxOne.UnlockedObjective = xBoxOne.UnlockedObjective.GetLineFrom(lines);
}
XBoxSeriesXAchievement xBoxSeriesX = challenge.GetPlatformChallenge<XBoxSeriesXAchievement>();
if (xBoxSeriesX != null)
{
xBoxSeriesX.LockedObjective = xBoxSeriesX.LockedObjective.GetLineFrom(lines);
xBoxSeriesX.UnlockedObjective = xBoxSeriesX.UnlockedObjective.GetLineFrom(lines);
}
StadiaAchievement stadia = challenge.GetPlatformChallenge<StadiaAchievement>();
if (stadia != null)
{
stadia.LockedName = stadia.LockedName.GetLineFrom(lines);
stadia.LockedObjective = stadia.LockedObjective.GetLineFrom(lines);
stadia.UnlockedName = stadia.UnlockedName.GetLineFrom(lines);
stadia.UnlockedObjective = stadia.UnlockedObjective.GetLineFrom(lines);
}
}
}
private Task LoadWorkspace(FileInfo gameSave) => Task.Run(() =>
{
IsAvailable = false;
_statusBarManager.ResetDefaultBackgroundColor();
_statusBarManager.RemoveStatusControl(_tryLockElement);
bool hasLock;
if (!_perforceService.Connect(Settings.PerforceServer, Environment.UserName, Settings.WorkspaceName))
{
throw new Exception($"Unable to connect on the server '{Settings.PerforceServer}' with user '{Environment.UserName}' on client '{Settings.WorkspaceName}'.");
}
try
{
if (_perforceService.Connection.IsSubmittedInDepot(gameSave.FullName))
{
_perforceService.Connection.Sync(gameSave.FullName);
_perforceService.Connection.OpenForEdit(gameSave.FullName);
}
else
{
if (!gameSave.Exists)
{
gameSave.Directory.Create();
File.WriteAllText(gameSave.FullName, JsonConvert.SerializeObject(new Game()));
}
_perforceService.Connection.OpenForAdd(gameSave.FullName);
}
hasLock = _perforceService.Connection.TryLock(gameSave.FullName, out string lockedBy);
if (!hasLock)
{
Warn($"Workspace file \"{gameSave.FullName}\" already locked by {lockedBy}");
_statusBarManager.SetBackgroundColor(Colors.DarkGoldenrod);
_tryLockElement.Dispatcher.BeginInvoke(new Action(() => _tryLockElement.Content = $"locked by {lockedBy}, click to try to take ownership"));
_statusBarManager.AddStatusControl(_tryLockElement);
}
}
finally
{
_perforceService.Disconnect();
}
Languages.Clear();
Challenges.Clear();
Game game = JsonConvert.DeserializeObject<Game>(File.ReadAllText(gameSave.FullName));
foreach (LanguageInfo language in game.GetLanguages())
{
LanguageInfo info = Languages.FirstOrDefault(l => l.Id == language.Id);
if (info is null)
{
Languages.Add(language);
}
else
{
info.Language = language.Language;
info.Ps4Id = language.Ps4Id;
info.Ps5Id = language.Ps5Id;
info.MicrosoftId = language.MicrosoftId;
info.GoogleId = language.GoogleId;
}
}
game.SetPs4Configuration(Settings.WorkspacePath, Ps4Configuration);
game.SetPs5Configuration(Settings.WorkspacePath, Ps5Configuration);
game.SetXBoxOneConfiguration(Settings.WorkspacePath, XBoxOneConfiguration);
game.SetXBoxSeriesXConfiguration(Settings.WorkspacePath, XBoxSeriesXConfiguration);
game.SetStadiaConfiguration(Settings.WorkspacePath, StadiaConfiguration);
foreach (Challenge challenge in game.GetChallenges(Settings.WorkspacePath))
{
Challenges.Add(challenge);
}
ApplyOasisLanguages();
ApplyOasisLines(Challenges.ToList());
IsAvailable = hasLock;
});
public Task OpenForEditOrAdd(params string[] files) => Task.Run(() =>
{
if (!_perforceService.Connect(Settings.PerforceServer, Environment.UserName, Settings.WorkspaceName))
{
throw new Exception($"Unable to connect on the server '{Settings.PerforceServer}' with user '{Environment.UserName}' on client '{Settings.WorkspaceName}'.");
}
try
{
foreach (FileInfo file in files.Where(f => !string.IsNullOrEmpty(f)).Select(f => new FileInfo(f)))
{
// because @ is a wildcard
if (_perforceService.Connection.IsSubmittedInDepot(file.FullName.Replace("@", "%40")))
{
_perforceService.Connection.Sync(file.FullName.Replace("@", "%40"));
_perforceService.Connection.OpenForEdit(file.FullName.Replace("@", "%40"));
}
else if (file.FullName.StartsWith(Settings.WorkspacePath) && !file.FullName.Contains("\\..\\"))
{
if (!file.Exists)
{
file.Directory.Create();
}
// because some image can contains @ we need to force
_perforceService.Connection.Run("add", "-d", "-f", file.FullName);
}
}
}
finally
{
_perforceService.Disconnect();
}
});
public Task Revert(string path) => Task.Run(() =>
{
if (path.StartsWith(Settings.WorkspacePath) && !path.Contains("\\..\\"))
{
if (!_perforceService.Connect(Settings.PerforceServer, Environment.UserName, Settings.WorkspaceName))
{
throw new Exception($"Unable to connect on the server '{Settings.PerforceServer}' with user '{Environment.UserName}' on client '{Settings.WorkspaceName}'.");
}
try
{
_perforceService.Connection.Revert(new[] { path }, false, true);
}
finally
{
_perforceService.Disconnect();
}
}
});
public Task OpenForDeleteIfMissing(params string[] files) => Task.Run(() =>
{
if (!_perforceService.Connect(Settings.PerforceServer, Environment.UserName, Settings.WorkspaceName))
{
throw new Exception($"Unable to connect on the server '{Settings.PerforceServer}' with user '{Environment.UserName}' on client '{Settings.WorkspaceName}'.");
}
try
{
foreach (FileInfo file in files.Where(f => !string.IsNullOrEmpty(f)).Select(f => new FileInfo(f.Replace("@", "%40")))) // because @ is a wildcard
{
if (!file.Exists && _perforceService.Connection.IsSubmittedInDepot(file.FullName))
{
_perforceService.Connection.OpenForDelete(file.FullName);
}
}
}
finally
{
_perforceService.Disconnect();
}
});
public async Task StartProcess(Task task, string message, string success, string error)
{
IsWorking = true;
_logger.Info(message);
_statusBarManager.SetMessage(message);
_statusBarManager.SetProgressIndeterminate(true);
_statusBarManager.SetProgressVisible(true);
try
{
await task.ConfigureAwait(true);
_logger.Info(success);
_notificationManager.ShowSuccess(success);
}
catch (Exception exception)
{
_logger.Error(error, exception);
_notificationManager.ShowError(error + Environment.NewLine + exception.Message);
}
finally
{
_statusBarManager.SetMessage(Path.Combine(Settings.WorkspacePath, Settings.GameSaveFilePath));
_statusBarManager.SetProgressVisible(false);
IsWorking = false;
}
}
public void Warn(string message)
{
_logger.Warn(message);
_notificationManager.ShowWarning(message);
}
public async Task GeneratePlatforms(IEnumerable<Type> challengeTypes)
{
foreach (Func<IEnumerable<Type>, Task> generatePlatform in GeneratePlatfromsAsked.GetInvocationList())
{
await generatePlatform(challengeTypes).ConfigureAwait(true);
}
}
#endregion
}
}