major refactoring, maybe some bugfixes, idk
This commit is contained in:
@@ -11,22 +11,25 @@ namespace MareSynchronos
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 0;
|
||||
|
||||
public string CacheFolder { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> ClientSecret { get; set; } = new();
|
||||
public Dictionary<string, string> UidComments { get; set; } = new();
|
||||
private string _apiUri = string.Empty;
|
||||
private int _maxParallelScan = 10;
|
||||
[NonSerialized]
|
||||
private DalamudPluginInterface? _pluginInterface;
|
||||
|
||||
public bool AcceptedAgreement { get; set; } = false;
|
||||
public string ApiUri
|
||||
{
|
||||
get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri;
|
||||
set => _apiUri = value;
|
||||
}
|
||||
|
||||
public bool UseCustomService { get; set; } = false;
|
||||
public string CacheFolder { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> ClientSecret { get; set; } = new();
|
||||
[JsonIgnore]
|
||||
public bool HasValidSetup => AcceptedAgreement && InitialScanComplete && !string.IsNullOrEmpty(CacheFolder) &&
|
||||
Directory.Exists(CacheFolder) && ClientSecret.ContainsKey(ApiUri);
|
||||
|
||||
public bool InitialScanComplete { get; set; } = false;
|
||||
public bool AcceptedAgreement { get; set; } = false;
|
||||
private int _maxParallelScan = 10;
|
||||
public int MaxParallelScan
|
||||
{
|
||||
get => _maxParallelScan;
|
||||
@@ -41,23 +44,18 @@ namespace MareSynchronos
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasValidSetup => AcceptedAgreement && InitialScanComplete && !string.IsNullOrEmpty(CacheFolder) &&
|
||||
Directory.Exists(CacheFolder) && ClientSecret.ContainsKey(ApiUri);
|
||||
|
||||
public Dictionary<string, string> UidComments { get; set; } = new();
|
||||
public bool UseCustomService { get; set; } = false;
|
||||
public int Version { get; set; } = 0;
|
||||
// the below exist just to make saving less cumbersome
|
||||
|
||||
[NonSerialized]
|
||||
private DalamudPluginInterface? _pluginInterface;
|
||||
|
||||
public void Initialize(DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
this._pluginInterface = pluginInterface;
|
||||
_pluginInterface = pluginInterface;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
this._pluginInterface!.SavePluginConfig(this);
|
||||
_pluginInterface!.SavePluginConfig(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using MareSynchronos.Managers;
|
||||
using MareSynchronos.Models;
|
||||
using MareSynchronos.Utils;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
||||
namespace MareSynchronos.Factories
|
||||
{
|
||||
public class CharacterCacheFactory
|
||||
public class CharacterDataFactory
|
||||
{
|
||||
private readonly ClientState _clientState;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly FileReplacementFactory _factory;
|
||||
|
||||
public CharacterCacheFactory(ClientState clientState, IpcManager ipcManager, FileReplacementFactory factory)
|
||||
public CharacterDataFactory(ClientState clientState, IpcManager ipcManager, FileReplacementFactory factory)
|
||||
{
|
||||
Logger.Debug("Creating " + nameof(CharacterDataFactory));
|
||||
|
||||
_clientState = clientState;
|
||||
_ipcManager = ipcManager;
|
||||
_factory = factory;
|
||||
@@ -34,13 +32,14 @@ namespace MareSynchronos.Factories
|
||||
return _clientState.LocalPlayer!.Name.ToString();
|
||||
}
|
||||
|
||||
public unsafe CharacterCache BuildCharacterCache()
|
||||
public unsafe CharacterData BuildCharacterData()
|
||||
{
|
||||
var cache = new CharacterCache();
|
||||
Stopwatch st = Stopwatch.StartNew();
|
||||
var cache = new CharacterData();
|
||||
|
||||
while (_clientState.LocalPlayer == null)
|
||||
{
|
||||
PluginLog.Debug("Character is null but it shouldn't be, waiting");
|
||||
Logger.Debug("Character is null but it shouldn't be, waiting");
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
var model = (CharacterBase*)((Character*)_clientState.LocalPlayer!.Address)->GameObject.GetDrawObject();
|
||||
@@ -100,6 +99,13 @@ namespace MareSynchronos.Factories
|
||||
}
|
||||
}
|
||||
|
||||
cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization()!;
|
||||
cache.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(_clientState.LocalPlayer!.Name.ToString());
|
||||
cache.JobId = _clientState.LocalPlayer!.ClassJob.Id;
|
||||
|
||||
st.Stop();
|
||||
Logger.Debug("Building Character Data took " + st.Elapsed);
|
||||
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace MareSynchronos.Factories
|
||||
public FileCache Create(string file)
|
||||
{
|
||||
FileInfo fileInfo = new(file);
|
||||
string sha1Hash = Crypto.GetFileHash(fileInfo.FullName);
|
||||
var sha1Hash = Crypto.GetFileHash(fileInfo.FullName);
|
||||
return new FileCache()
|
||||
{
|
||||
Filepath = fileInfo.FullName,
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
using Dalamud.Game.ClientState;
|
||||
using MareSynchronos.Managers;
|
||||
using MareSynchronos.Managers;
|
||||
using MareSynchronos.Models;
|
||||
using MareSynchronos.Utils;
|
||||
|
||||
namespace MareSynchronos.Factories
|
||||
{
|
||||
public class FileReplacementFactory
|
||||
{
|
||||
private readonly IpcManager ipcManager;
|
||||
private readonly IpcManager _ipcManager;
|
||||
|
||||
public FileReplacementFactory(IpcManager ipcManager)
|
||||
{
|
||||
this.ipcManager = ipcManager;
|
||||
Logger.Debug("Creating " + nameof(FileReplacementFactory));
|
||||
|
||||
this._ipcManager = ipcManager;
|
||||
}
|
||||
|
||||
public FileReplacement Create()
|
||||
{
|
||||
if (!ipcManager.CheckPenumbraApi())
|
||||
if (!_ipcManager.CheckPenumbraApi())
|
||||
{
|
||||
throw new System.Exception();
|
||||
}
|
||||
|
||||
return new FileReplacement(ipcManager.PenumbraModDirectory()!);
|
||||
return new FileReplacement(_ipcManager.PenumbraModDirectory()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable disable
|
||||
#nullable disable
|
||||
|
||||
namespace MareSynchronos.FileCacheDB
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
250
MareSynchronos/Managers/CharacterCacheManager.cs
Normal file
250
MareSynchronos/Managers/CharacterCacheManager.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Logging;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
using MareSynchronos.Models;
|
||||
using MareSynchronos.Utils;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronos.Managers;
|
||||
|
||||
public class CharacterCacheManager : IDisposable
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ClientState _clientState;
|
||||
private readonly DalamudUtil _dalamudUtil;
|
||||
private readonly Framework _framework;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ObjectTable _objectTable;
|
||||
private readonly List<CachedPlayer> _onlineCachedPlayers = new();
|
||||
private readonly List<string> _localVisiblePlayers = new();
|
||||
private DateTime _lastPlayerObjectCheck = DateTime.Now;
|
||||
|
||||
public CharacterCacheManager(ClientState clientState, Framework framework, ObjectTable objectTable, ApiController apiController, DalamudUtil dalamudUtil, IpcManager ipcManager)
|
||||
{
|
||||
Logger.Debug("Creating " + nameof(CharacterCacheManager));
|
||||
|
||||
_clientState = clientState;
|
||||
_framework = framework;
|
||||
_objectTable = objectTable;
|
||||
_apiController = apiController;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_ipcManager = ipcManager;
|
||||
}
|
||||
|
||||
public void AddInitialPairs(List<string> apiTaskResult)
|
||||
{
|
||||
_onlineCachedPlayers.AddRange(apiTaskResult.Select(a => new CachedPlayer(a)));
|
||||
Logger.Debug("Online and paired users: " + string.Join(",", _onlineCachedPlayers));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Debug("Disposing " + nameof(CharacterCacheManager));
|
||||
|
||||
_apiController.CharacterReceived -= ApiControllerOnCharacterReceived;
|
||||
_apiController.PairedClientOnline -= ApiControllerOnPairedClientOnline;
|
||||
_apiController.PairedClientOffline -= ApiControllerOnPairedClientOffline;
|
||||
_apiController.PairedWithOther -= ApiControllerOnPairedWithOther;
|
||||
_apiController.UnpairedFromOther -= ApiControllerOnUnpairedFromOther;
|
||||
_framework.Update -= FrameworkOnUpdate;
|
||||
|
||||
foreach (var character in _onlineCachedPlayers.ToList())
|
||||
{
|
||||
RestoreCharacter(character);
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_apiController.CharacterReceived += ApiControllerOnCharacterReceived;
|
||||
_apiController.PairedClientOnline += ApiControllerOnPairedClientOnline;
|
||||
_apiController.PairedClientOffline += ApiControllerOnPairedClientOffline;
|
||||
_apiController.PairedWithOther += ApiControllerOnPairedWithOther;
|
||||
_apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther;
|
||||
_framework.Update += FrameworkOnUpdate;
|
||||
}
|
||||
public async Task UpdatePlayersFromService(Dictionary<string, int> playerJobIds)
|
||||
{
|
||||
await _apiController.GetCharacterData(playerJobIds);
|
||||
}
|
||||
|
||||
private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e)
|
||||
{
|
||||
Logger.Debug("Received hash for " + e.CharacterNameHash);
|
||||
string otherPlayerName;
|
||||
|
||||
var localPlayers = _dalamudUtil.GetLocalPlayers();
|
||||
if (localPlayers.ContainsKey(e.CharacterNameHash))
|
||||
{
|
||||
_onlineCachedPlayers.Single(p => p.PlayerNameHash == e.CharacterNameHash).PlayerName = localPlayers[e.CharacterNameHash].Name.ToString();
|
||||
otherPlayerName = localPlayers[e.CharacterNameHash].Name.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Debug("Found no local player for " + e.CharacterNameHash);
|
||||
return;
|
||||
}
|
||||
|
||||
_onlineCachedPlayers.Single(p => p.PlayerNameHash == e.CharacterNameHash)
|
||||
.CharacterCache[e.CharacterData.JobId] = e.CharacterData;
|
||||
|
||||
List<FileReplacementDto> toDownloadReplacements;
|
||||
using (var db = new FileCacheContext())
|
||||
{
|
||||
Logger.Debug("Checking for files to download for player " + otherPlayerName);
|
||||
Logger.Debug("Received total " + e.CharacterData.FileReplacements.Count + " file replacement data");
|
||||
Logger.Debug("Hash for data is " + e.CharacterData.Hash);
|
||||
toDownloadReplacements =
|
||||
e.CharacterData.FileReplacements.Where(f => !db.FileCaches.Any(c => c.Hash == f.Hash))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
Logger.Debug("Downloading missing files for player " + otherPlayerName);
|
||||
// todo: make this cancellable
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await _apiController.DownloadFiles(toDownloadReplacements);
|
||||
|
||||
Logger.Debug("Assigned hash to visible player: " + otherPlayerName);
|
||||
_ipcManager.PenumbraRemoveTemporaryCollection(otherPlayerName);
|
||||
var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(otherPlayerName);
|
||||
Dictionary<string, string> moddedPaths = new();
|
||||
try
|
||||
{
|
||||
using var db = new FileCacheContext();
|
||||
foreach (var item in e.CharacterData.FileReplacements)
|
||||
{
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash);
|
||||
if (fileCache != null)
|
||||
{
|
||||
moddedPaths.Add(gamePath, fileCache.Filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Error(ex, "Something went wrong during calculation replacements");
|
||||
}
|
||||
|
||||
_dalamudUtil.WaitWhileCharacterIsDrawing(localPlayers[e.CharacterNameHash].Address);
|
||||
|
||||
_ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, e.CharacterData.ManipulationData);
|
||||
_ipcManager.GlamourerApplyCharacterCustomization(e.CharacterData.GlamourerData, otherPlayerName);
|
||||
});
|
||||
}
|
||||
|
||||
private void ApiControllerOnPairedClientOffline(object? sender, EventArgs e)
|
||||
{
|
||||
Logger.Debug("Player offline: " + sender!);
|
||||
_onlineCachedPlayers.RemoveAll(p => p.PlayerNameHash == ((string)sender!));
|
||||
}
|
||||
|
||||
private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e)
|
||||
{
|
||||
Logger.Debug("Player online: " + sender!);
|
||||
_onlineCachedPlayers.Add(new CachedPlayer((string)sender!));
|
||||
}
|
||||
|
||||
private void ApiControllerOnPairedWithOther(object? sender, EventArgs e)
|
||||
{
|
||||
var characterHash = (string?)sender;
|
||||
if (string.IsNullOrEmpty(characterHash)) return;
|
||||
var players = _dalamudUtil.GetLocalPlayers();
|
||||
if (!players.ContainsKey(characterHash)) return;
|
||||
Logger.Debug("Getting data for " + characterHash);
|
||||
_ = _apiController.GetCharacterData(new Dictionary<string, int> { { characterHash, (int)players[characterHash].ClassJob.Id } });
|
||||
}
|
||||
|
||||
private void ApiControllerOnUnpairedFromOther(object? sender, EventArgs e)
|
||||
{
|
||||
var characterHash = (string?)sender;
|
||||
if (string.IsNullOrEmpty(characterHash)) return;
|
||||
RestoreCharacter(_onlineCachedPlayers.Single(p => p.PlayerNameHash == (string)sender!));
|
||||
}
|
||||
|
||||
private void FrameworkOnUpdate(Framework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_clientState.LocalPlayer == null) return;
|
||||
|
||||
if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(2)) return;
|
||||
|
||||
_localVisiblePlayers.Clear();
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||
var playerName = obj.Name.ToString();
|
||||
if (playerName == _dalamudUtil.PlayerName) continue;
|
||||
var pObj = (PlayerCharacter)obj;
|
||||
_localVisiblePlayers.Add(pObj.Name.ToString());
|
||||
if (_onlineCachedPlayers.Any(p => p.PlayerName == pObj.Name.ToString()))
|
||||
{
|
||||
_onlineCachedPlayers.Single(p => p.PlayerName == pObj.Name.ToString()).IsVisible = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString());
|
||||
|
||||
if (_onlineCachedPlayers.All(p => p.PlayerNameHash != hashedName)) continue;
|
||||
|
||||
var cachedPlayer = _onlineCachedPlayers.Single(p => p.PlayerNameHash == hashedName);
|
||||
if (string.IsNullOrEmpty(cachedPlayer.PlayerName))
|
||||
{
|
||||
cachedPlayer.PlayerName = pObj.Name.ToString();
|
||||
}
|
||||
cachedPlayer.PlayerCharacter = pObj;
|
||||
cachedPlayer.IsVisible = true;
|
||||
}
|
||||
|
||||
foreach (var item in _onlineCachedPlayers.Where(p => !string.IsNullOrEmpty(p.PlayerName) && !_localVisiblePlayers.Contains(p.PlayerName!)))
|
||||
{
|
||||
item.IsVisible = false;
|
||||
}
|
||||
|
||||
foreach (var item in _onlineCachedPlayers.Where(p => !string.IsNullOrEmpty(p.PlayerName) && !p.IsVisible && p.WasVisible))
|
||||
{
|
||||
Logger.Debug("Player not visible anymore: " + item.PlayerName);
|
||||
RestoreCharacter(item);
|
||||
}
|
||||
|
||||
var newVisiblePlayers = _onlineCachedPlayers.Where(p => p.IsVisible && !p.WasVisible).ToList();
|
||||
if (newVisiblePlayers.Any())
|
||||
{
|
||||
Logger.Debug("Getting data for new players: " + string.Join(Environment.NewLine, newVisiblePlayers));
|
||||
Task.Run(async () => await UpdatePlayersFromService(newVisiblePlayers
|
||||
.ToDictionary(k => k.PlayerNameHash, k => (int)k.PlayerCharacter!.ClassJob.Id)));
|
||||
}
|
||||
|
||||
_lastPlayerObjectCheck = DateTime.Now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Error(ex, "error");
|
||||
}
|
||||
}
|
||||
private void RestoreCharacter(CachedPlayer character)
|
||||
{
|
||||
if (string.IsNullOrEmpty(character.PlayerName)) return;
|
||||
|
||||
Logger.Debug("Restoring state for " + character.PlayerName);
|
||||
_ipcManager.PenumbraRemoveTemporaryCollection(character.PlayerName);
|
||||
_ipcManager.GlamourerRevertCharacterCustomization(character.PlayerName);
|
||||
|
||||
character.Reset();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using MareSynchronos.Factories;
|
||||
using MareSynchronos.Models;
|
||||
using MareSynchronos.Utils;
|
||||
@@ -10,293 +7,91 @@ using MareSynchronos.WebAPI;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.PlayerWatch;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
|
||||
namespace MareSynchronos.Managers
|
||||
{
|
||||
public class CachedPlayer
|
||||
{
|
||||
public string? PlayerName { get; set; }
|
||||
public string? PlayerNameHash { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public Dictionary<int, CharacterCacheDto>? CharacterCache { get; set; }
|
||||
public PlayerCharacter? PlayerCharacter { get; set; }
|
||||
}
|
||||
|
||||
public class CharacterManager : IDisposable
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
readonly Dictionary<string, string> _cachedLocalPlayers = new();
|
||||
private readonly Dictionary<(string, int), CharacterCacheDto> _characterCache = new();
|
||||
private readonly ClientState _clientState;
|
||||
private readonly Framework _framework;
|
||||
private readonly CharacterCacheManager _characterCacheManager;
|
||||
private readonly CharacterDataFactory _characterDataFactory;
|
||||
private readonly DalamudUtil _dalamudUtil;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ObjectTable _objectTable;
|
||||
private readonly Configuration _pluginConfiguration;
|
||||
private readonly CharacterCacheFactory _characterCacheFactory;
|
||||
private readonly IPlayerWatcher _watcher;
|
||||
private DateTime _lastPlayerObjectCheck = DateTime.Now;
|
||||
private string _lastSentHash = string.Empty;
|
||||
private Task? _playerChangedTask = null;
|
||||
private Task? _playerChangedTask;
|
||||
|
||||
private List<CachedPlayer> _onlineCachedPlayers = new();
|
||||
|
||||
private Dictionary<string, string> _onlinePairedUsers = new();
|
||||
|
||||
public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager,
|
||||
Configuration pluginConfiguration, CharacterCacheFactory characterCacheFactory)
|
||||
public CharacterManager(ApiController apiController, ObjectTable objectTable, IpcManager ipcManager,
|
||||
CharacterDataFactory characterDataFactory, CharacterCacheManager characterCacheManager, DalamudUtil dalamudUtil, IPlayerWatcher watcher)
|
||||
{
|
||||
this._clientState = clientState;
|
||||
this._framework = framework;
|
||||
this._apiController = apiController;
|
||||
this._objectTable = objectTable;
|
||||
this._ipcManager = ipcManager;
|
||||
_pluginConfiguration = pluginConfiguration;
|
||||
_characterCacheFactory = characterCacheFactory;
|
||||
_watcher = PlayerWatchFactory.Create(framework, clientState, objectTable);
|
||||
Logger.Debug("Creating " + nameof(CharacterManager));
|
||||
|
||||
_apiController = apiController;
|
||||
_objectTable = objectTable;
|
||||
_ipcManager = ipcManager;
|
||||
_characterDataFactory = characterDataFactory;
|
||||
_characterCacheManager = characterCacheManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_watcher = watcher;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Debug("Disposing " + nameof(CharacterManager));
|
||||
|
||||
_ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent;
|
||||
_framework.Update -= Framework_Update;
|
||||
_clientState.TerritoryChanged -= ClientState_TerritoryChanged;
|
||||
_apiController.Connected -= ApiController_Connected;
|
||||
_apiController.Disconnected -= ApiController_Disconnected;
|
||||
_apiController.CharacterReceived -= ApiControllerOnCharacterReceived;
|
||||
_apiController.UnpairedFromOther -= ApiControllerOnUnpairedFromOther;
|
||||
_apiController.PairedWithOther -= ApiControllerOnPairedWithOther;
|
||||
_apiController.PairedClientOffline -= ApiControllerOnPairedClientOffline;
|
||||
_watcher.Disable();
|
||||
_watcher.PlayerChanged -= Watcher_PlayerChanged;
|
||||
_watcher?.Dispose();
|
||||
|
||||
foreach (var character in _onlinePairedUsers)
|
||||
{
|
||||
RestoreCharacter(character);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePlayersFromService(Dictionary<string, PlayerCharacter> currentLocalPlayers)
|
||||
{
|
||||
PluginLog.Debug("Updating local players from service");
|
||||
currentLocalPlayers = currentLocalPlayers.Where(k => _onlinePairedUsers.ContainsKey(k.Key))
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
await _apiController.GetCharacterData(currentLocalPlayers
|
||||
.ToDictionary(
|
||||
k => k.Key,
|
||||
k => (int)k.Value.ClassJob.Id));
|
||||
}
|
||||
|
||||
internal void StartWatchingPlayer()
|
||||
{
|
||||
_watcher.AddPlayerToWatch(GetPlayerName());
|
||||
_watcher.AddPlayerToWatch(_dalamudUtil.PlayerName);
|
||||
_watcher.PlayerChanged += Watcher_PlayerChanged;
|
||||
_watcher.Enable();
|
||||
_apiController.Connected += ApiController_Connected;
|
||||
_apiController.Disconnected += ApiController_Disconnected;
|
||||
_apiController.CharacterReceived += ApiControllerOnCharacterReceived;
|
||||
_apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther;
|
||||
_apiController.PairedWithOther += ApiControllerOnPairedWithOther;
|
||||
_apiController.PairedClientOffline += ApiControllerOnPairedClientOffline;
|
||||
_apiController.PairedClientOnline += ApiControllerOnPairedClientOnline;
|
||||
|
||||
PluginLog.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected);
|
||||
Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected);
|
||||
if (_apiController.IsConnected)
|
||||
{
|
||||
ApiController_Connected(null, EventArgs.Empty);
|
||||
}
|
||||
|
||||
_ipcManager.PenumbraRedraw(_dalamudUtil.PlayerName);
|
||||
}
|
||||
|
||||
private void ApiController_Connected(object? sender, EventArgs args)
|
||||
{
|
||||
PluginLog.Debug(nameof(ApiController_Connected));
|
||||
PluginLog.Debug("MyHashedName:" + Crypto.GetHash256(GetPlayerName() + _clientState.LocalPlayer!.HomeWorld.Id));
|
||||
var apiTask = _apiController.SendCharacterName(_dalamudUtil.PlayerNameHashed);
|
||||
_lastSentHash = string.Empty;
|
||||
var apiTask = _apiController.SendCharacterName(Crypto.GetHash256(GetPlayerName() + _clientState.LocalPlayer!.HomeWorld.Id));
|
||||
_characterCacheManager.Initialize();
|
||||
|
||||
Task.WaitAll(apiTask);
|
||||
|
||||
_onlinePairedUsers = apiTask.Result.ToDictionary(k => k, k => string.Empty);
|
||||
var assignTask = AssignLocalPlayersData();
|
||||
Task.WaitAll(assignTask);
|
||||
PluginLog.Debug("Online and paired users: " + string.Join(",", _onlinePairedUsers));
|
||||
|
||||
_framework.Update += Framework_Update;
|
||||
_characterCacheManager.AddInitialPairs(apiTask.Result);
|
||||
|
||||
_ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent;
|
||||
_clientState.TerritoryChanged += ClientState_TerritoryChanged;
|
||||
}
|
||||
|
||||
private void ApiController_Disconnected(object? sender, EventArgs args)
|
||||
{
|
||||
PluginLog.Debug(nameof(ApiController_Disconnected));
|
||||
_framework.Update -= Framework_Update;
|
||||
_characterCacheManager.Dispose();
|
||||
|
||||
Logger.Debug(nameof(ApiController_Disconnected));
|
||||
|
||||
_ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent;
|
||||
_clientState.TerritoryChanged -= ClientState_TerritoryChanged;
|
||||
foreach (var character in _onlinePairedUsers)
|
||||
{
|
||||
RestoreCharacter(character);
|
||||
}
|
||||
_onlinePairedUsers.Clear();
|
||||
|
||||
_lastSentHash = string.Empty;
|
||||
}
|
||||
|
||||
private void ApiControllerOnPairedWithOther(object? sender, EventArgs e)
|
||||
private async Task<CharacterData> CreateFullCharacterCache()
|
||||
{
|
||||
var characterHash = (string?)sender;
|
||||
if (string.IsNullOrEmpty(characterHash)) return;
|
||||
var players = GetLocalPlayers();
|
||||
if (players.ContainsKey(characterHash))
|
||||
{
|
||||
PluginLog.Debug("Removed pairing, restoring data for " + characterHash);
|
||||
_ = _apiController.GetCharacterData(new Dictionary<string, int> { { characterHash, (int)players[characterHash].ClassJob.Id } });
|
||||
}
|
||||
}
|
||||
var cache = _characterDataFactory.BuildCharacterData();
|
||||
|
||||
private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e)
|
||||
{
|
||||
PluginLog.Debug("Received hash for " + e.CharacterNameHash);
|
||||
string otherPlayerName;
|
||||
|
||||
var localPlayers = GetLocalPlayers();
|
||||
if (localPlayers.ContainsKey(e.CharacterNameHash))
|
||||
{
|
||||
_onlinePairedUsers[e.CharacterNameHash] = localPlayers[e.CharacterNameHash].Name.ToString();
|
||||
otherPlayerName = _onlinePairedUsers[e.CharacterNameHash];
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Debug("Found no local player for " + e.CharacterNameHash);
|
||||
return;
|
||||
}
|
||||
|
||||
_characterCache[(e.CharacterNameHash, e.CharacterData.JobId)] = e.CharacterData;
|
||||
|
||||
List<FileReplacementDto> toDownloadReplacements;
|
||||
using (var db = new FileCacheContext())
|
||||
{
|
||||
PluginLog.Debug("Checking for files to download for player " + otherPlayerName);
|
||||
PluginLog.Debug("Received total " + e.CharacterData.FileReplacements.Count + " file replacement data");
|
||||
PluginLog.Debug("Hash for data is " + e.CharacterData.Hash);
|
||||
toDownloadReplacements =
|
||||
e.CharacterData.FileReplacements.Where(f => !db.FileCaches.Any(c => c.Hash == f.Hash))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
PluginLog.Debug("Downloading missing files for player " + otherPlayerName);
|
||||
// todo: make this cancellable
|
||||
var downloadTask = _apiController.DownloadFiles(toDownloadReplacements, _pluginConfiguration.CacheFolder);
|
||||
while (!downloadTask.IsCompleted)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
PluginLog.Debug("Assigned hash to visible player: " + otherPlayerName);
|
||||
_ipcManager.PenumbraRemoveTemporaryCollection(otherPlayerName);
|
||||
var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(otherPlayerName);
|
||||
Dictionary<string, string> moddedPaths = new();
|
||||
try
|
||||
{
|
||||
using var db = new FileCacheContext();
|
||||
foreach (var item in e.CharacterData.FileReplacements)
|
||||
{
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash);
|
||||
if (fileCache != null)
|
||||
{
|
||||
moddedPaths.Add(gamePath, fileCache.Filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Error(ex, "Something went wrong during calculation replacements");
|
||||
}
|
||||
|
||||
WaitWhileCharacterIsDrawing(localPlayers[e.CharacterNameHash].Address);
|
||||
|
||||
_ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, e.CharacterData.ManipulationData);
|
||||
_ipcManager.GlamourerApplyCharacterCustomization(e.CharacterData.GlamourerData, otherPlayerName);
|
||||
}
|
||||
|
||||
private void ApiControllerOnUnpairedFromOther(object? sender, EventArgs e)
|
||||
{
|
||||
var characterHash = (string?)sender;
|
||||
if (string.IsNullOrEmpty(characterHash)) return;
|
||||
RestoreCharacter(new KeyValuePair<string, string>(characterHash, _onlinePairedUsers[characterHash]));
|
||||
}
|
||||
|
||||
private void RestoreCharacter(KeyValuePair<string, string> character)
|
||||
{
|
||||
if (string.IsNullOrEmpty(character.Value)) return;
|
||||
|
||||
foreach (var entry in _characterCache.Where(c => c.Key.Item1 == character.Key))
|
||||
{
|
||||
_characterCache.Remove(entry.Key);
|
||||
}
|
||||
|
||||
RestorePreviousCharacter(character.Value);
|
||||
PluginLog.Debug("Removed from pairing, restoring state for " + character.Value);
|
||||
_ipcManager.PenumbraRemoveTemporaryCollection(character.Value);
|
||||
_ipcManager.GlamourerRevertCharacterCustomization(character.Value);
|
||||
}
|
||||
|
||||
private void ApiControllerOnPairedClientOffline(object? sender, EventArgs e)
|
||||
{
|
||||
PluginLog.Debug("Player offline: " + sender!);
|
||||
_onlinePairedUsers.Remove((string)sender!);
|
||||
}
|
||||
|
||||
private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e)
|
||||
{
|
||||
PluginLog.Debug("Player online: " + sender!);
|
||||
_onlinePairedUsers.Add((string)sender!, string.Empty);
|
||||
}
|
||||
|
||||
private async Task AssignLocalPlayersData()
|
||||
{
|
||||
PluginLog.Debug("Temp assigning local players from cache");
|
||||
var currentLocalPlayers = GetLocalPlayers();
|
||||
foreach (var player in _characterCache)
|
||||
{
|
||||
if (currentLocalPlayers.ContainsKey(player.Key.Item1))
|
||||
{
|
||||
await Task.Run(() => ApiControllerOnCharacterReceived(null, new CharacterReceivedEventArgs(player.Key.Item1, player.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
await UpdatePlayersFromService(currentLocalPlayers);
|
||||
}
|
||||
|
||||
private void ClientState_TerritoryChanged(object? sender, ushort e)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (_clientState.LocalPlayer == null)
|
||||
{
|
||||
await Task.Delay(250);
|
||||
}
|
||||
|
||||
await AssignLocalPlayersData();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<CharacterCache> CreateFullCharacterCache()
|
||||
{
|
||||
var cache = _characterCacheFactory.BuildCharacterCache();
|
||||
cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization()!;
|
||||
cache.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(_clientState.LocalPlayer!.Name.ToString());
|
||||
cache.JobId = _clientState.LocalPlayer!.ClassJob.Id;
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
while (!cache.IsReady)
|
||||
@@ -312,163 +107,75 @@ namespace MareSynchronos.Managers
|
||||
return cache;
|
||||
}
|
||||
|
||||
private unsafe void Framework_Update(Framework framework)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_clientState.LocalPlayer == null) return;
|
||||
|
||||
if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(2)) return;
|
||||
|
||||
List<string> localPlayersList = new();
|
||||
Dictionary<string, PlayerCharacter> newPlayers = new();
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||
string playerName = obj.Name.ToString();
|
||||
if (playerName == GetPlayerName()) continue;
|
||||
var pObj = (PlayerCharacter)obj;
|
||||
var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString());
|
||||
|
||||
if (!_onlinePairedUsers.ContainsKey(hashedName)) continue;
|
||||
|
||||
_onlinePairedUsers[hashedName] = pObj.Name.ToString();
|
||||
localPlayersList.Add(hashedName);
|
||||
if (!_cachedLocalPlayers.ContainsKey(hashedName)) newPlayers[hashedName] = pObj;
|
||||
_cachedLocalPlayers[hashedName] = pObj.Name.ToString();
|
||||
}
|
||||
|
||||
foreach (var item in _cachedLocalPlayers.ToList().Where(item => !localPlayersList.Contains(item.Key)))
|
||||
{
|
||||
foreach (var cachedPlayerNameJobId in _characterCache.Keys.ToList().Where(cachedPlayerNameJobId => cachedPlayerNameJobId.Item1 == item.Key))
|
||||
{
|
||||
PluginLog.Debug("Player not visible anymore: " + cachedPlayerNameJobId.Item1);
|
||||
RestorePreviousCharacter(_cachedLocalPlayers[cachedPlayerNameJobId.Item1]);
|
||||
_characterCache.Remove(cachedPlayerNameJobId);
|
||||
}
|
||||
|
||||
_cachedLocalPlayers.Remove(item.Key);
|
||||
}
|
||||
|
||||
if (newPlayers.Any())
|
||||
{
|
||||
PluginLog.Debug("Getting data for new players: " + string.Join(Environment.NewLine, newPlayers));
|
||||
_ = UpdatePlayersFromService(newPlayers);
|
||||
}
|
||||
|
||||
_lastPlayerObjectCheck = DateTime.Now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Error(ex, "error");
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, PlayerCharacter> GetLocalPlayers()
|
||||
{
|
||||
Dictionary<string, PlayerCharacter> allLocalPlayers = new();
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||
string playerName = obj.Name.ToString();
|
||||
if (playerName == GetPlayerName()) continue;
|
||||
var playerObject = (PlayerCharacter)obj;
|
||||
allLocalPlayers[Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString())] = playerObject;
|
||||
}
|
||||
|
||||
return allLocalPlayers;
|
||||
}
|
||||
|
||||
private string GetPlayerName()
|
||||
{
|
||||
return _clientState.LocalPlayer!.Name.ToString();
|
||||
}
|
||||
|
||||
private void IpcManager_PenumbraRedrawEvent(object? objectTableIndex, EventArgs e)
|
||||
{
|
||||
var objTableObj = _objectTable[(int)objectTableIndex!];
|
||||
if (objTableObj!.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player)
|
||||
{
|
||||
if (objTableObj.Name.ToString() == GetPlayerName())
|
||||
{
|
||||
PluginLog.Debug("Penumbra Redraw Event");
|
||||
PlayerChanged(GetPlayerName());
|
||||
}
|
||||
}
|
||||
if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return;
|
||||
if (objTableObj.Name.ToString() != _dalamudUtil.PlayerName) return;
|
||||
Logger.Debug("Penumbra Redraw Event");
|
||||
PlayerChanged(_dalamudUtil.PlayerName);
|
||||
}
|
||||
|
||||
private unsafe void PlayerChanged(string name)
|
||||
private void PlayerChanged(string name)
|
||||
{
|
||||
//if (sender == null) return;
|
||||
PluginLog.Debug("Player changed: " + name);
|
||||
Logger.Debug("Player changed: " + name);
|
||||
if (_playerChangedTask is { IsCompleted: false })
|
||||
{
|
||||
PluginLog.Warning("PlayerChanged Task still running");
|
||||
return;
|
||||
}
|
||||
|
||||
_playerChangedTask = Task.Run(() =>
|
||||
_playerChangedTask = Task.Run(async () =>
|
||||
{
|
||||
WaitWhileCharacterIsDrawing(_clientState.LocalPlayer!.Address);
|
||||
Stopwatch st = Stopwatch.StartNew();
|
||||
_dalamudUtil.WaitWhileSelfIsDrawing();
|
||||
|
||||
var characterCacheTask = CreateFullCharacterCache();
|
||||
Task.WaitAll(characterCacheTask);
|
||||
var characterCacheTask = await CreateFullCharacterCache();
|
||||
|
||||
var cacheDto = characterCacheTask.Result.ToCharacterCacheDto();
|
||||
var cacheDto = characterCacheTask.ToCharacterCacheDto();
|
||||
|
||||
st.Stop();
|
||||
Logger.Debug("Elapsed time PlayerChangedTask: " + st.Elapsed);
|
||||
if (cacheDto.Hash == _lastSentHash)
|
||||
{
|
||||
PluginLog.Debug("Not sending data, already sent");
|
||||
Logger.Debug("Not sending data, already sent");
|
||||
return;
|
||||
}
|
||||
Task.WaitAll(_apiController.SendCharacterData(cacheDto, GetLocalPlayers().Select(d => d.Key).ToList()));
|
||||
await _apiController.SendCharacterData(cacheDto, _dalamudUtil.GetLocalPlayers().Select(d => d.Key).ToList());
|
||||
_lastSentHash = cacheDto.Hash;
|
||||
});
|
||||
}
|
||||
|
||||
public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress)
|
||||
{
|
||||
var obj = (GameObject*)characterAddress;
|
||||
|
||||
while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something
|
||||
{
|
||||
//PluginLog.Debug("Waiting for character to finish drawing");
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// wait half a second just in case
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
private void RestorePreviousCharacter(string playerName)
|
||||
{
|
||||
PluginLog.Debug("Restoring state for " + playerName);
|
||||
_ipcManager.PenumbraRemoveTemporaryCollection(playerName);
|
||||
_ipcManager.GlamourerRevertCharacterCustomization(playerName);
|
||||
}
|
||||
|
||||
private void Watcher_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor)
|
||||
{
|
||||
try
|
||||
Logger.Debug("Watcher Player Changed");
|
||||
Task.Run(() =>
|
||||
{
|
||||
// fix for redraw from anamnesis
|
||||
while (_clientState.LocalPlayer == null)
|
||||
try
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
// fix for redraw from anamnesis
|
||||
while (!_dalamudUtil.IsPlayerPresent)
|
||||
{
|
||||
Logger.Debug("Waiting Until Player is Present");
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
if (actor.Name.ToString() == _dalamudUtil.PlayerName)
|
||||
{
|
||||
Logger.Debug("Watcher: PlayerChanged");
|
||||
PlayerChanged(actor.Name.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Debug("PlayerChanged: " + actor.Name.ToString());
|
||||
}
|
||||
}
|
||||
if (actor.Name.ToString() == _clientState.LocalPlayer!.Name.ToString())
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Debug("Watcher: PlayerChanged");
|
||||
PlayerChanged(actor.Name.ToString());
|
||||
PluginLog.Error(ex, "Actor was null or broken " + actor);
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Debug("PlayerChanged: " + actor.Name.ToString());
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
PluginLog.Error(ex, "Actor was null or broken " + actor);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Logging;
|
||||
using MareSynchronos.Factories;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
using MareSynchronos.Utils;
|
||||
|
||||
namespace MareSynchronos.Managers
|
||||
{
|
||||
@@ -25,6 +25,8 @@ namespace MareSynchronos.Managers
|
||||
private Stopwatch? _timerStopWatch;
|
||||
public FileCacheManager(FileCacheFactory fileCacheFactory, IpcManager ipcManager, Configuration pluginConfiguration)
|
||||
{
|
||||
Logger.Debug("Creating " + nameof(FileCacheManager));
|
||||
|
||||
_fileCacheFactory = fileCacheFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_pluginConfiguration = pluginConfiguration;
|
||||
@@ -47,7 +49,8 @@ namespace MareSynchronos.Managers
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PluginLog.Debug("Disposing File Cache Manager");
|
||||
Logger.Debug("Disposing " + nameof(FileCacheManager));
|
||||
|
||||
_scanScheduler?.Stop();
|
||||
_scanCancellationTokenSource?.Cancel();
|
||||
}
|
||||
@@ -62,7 +65,7 @@ namespace MareSynchronos.Managers
|
||||
{
|
||||
_scanCancellationTokenSource = new CancellationTokenSource();
|
||||
var penumbraDir = _ipcManager.PenumbraModDirectory()!;
|
||||
PluginLog.Debug("Getting files from " + penumbraDir);
|
||||
Logger.Debug("Getting files from " + penumbraDir);
|
||||
var scannedFiles = new ConcurrentDictionary<string, bool>(
|
||||
Directory.EnumerateFiles(penumbraDir, "*.*", SearchOption.AllDirectories)
|
||||
.Select(s => s.ToLowerInvariant())
|
||||
@@ -80,7 +83,7 @@ namespace MareSynchronos.Managers
|
||||
var fileCachesToDelete = new ConcurrentBag<FileCache>();
|
||||
var fileCachesToAdd = new ConcurrentBag<FileCache>();
|
||||
|
||||
PluginLog.Debug("Getting file list from Database");
|
||||
Logger.Debug("Getting file list from Database");
|
||||
// scan files from database
|
||||
Parallel.ForEach(fileCaches, new ParallelOptions()
|
||||
{
|
||||
@@ -140,7 +143,7 @@ namespace MareSynchronos.Managers
|
||||
}
|
||||
}
|
||||
|
||||
PluginLog.Debug("Scan complete");
|
||||
Logger.Debug("Scan complete");
|
||||
TotalFiles = 0;
|
||||
CurrentFileProgress = 0;
|
||||
|
||||
@@ -160,7 +163,7 @@ namespace MareSynchronos.Managers
|
||||
|
||||
private void StartScheduler()
|
||||
{
|
||||
PluginLog.Debug("Scheduling next scan for in " + MinutesForScan + " minutes");
|
||||
Logger.Debug("Scheduling next scan for in " + MinutesForScan + " minutes");
|
||||
_scanScheduler = new System.Timers.Timer(TimeSpan.FromMinutes(MinutesForScan).TotalMilliseconds)
|
||||
{
|
||||
AutoReset = false,
|
||||
@@ -176,7 +179,7 @@ namespace MareSynchronos.Managers
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLog.Debug("Initiating periodic scan for mod changes");
|
||||
Logger.Debug("Initiating periodic scan for mod changes");
|
||||
Task.Run(() => _scanTask = StartFileScan(_scanCancellationTokenSource!.Token));
|
||||
_timerStopWatch = Stopwatch.StartNew();
|
||||
};
|
||||
|
||||
@@ -2,75 +2,78 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using System;
|
||||
using System.Buffers.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using MareSynchronos.Utils;
|
||||
|
||||
namespace MareSynchronos.Managers
|
||||
{
|
||||
public class IpcManager : IDisposable
|
||||
{
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly ICallGateSubscriber<object> _penumbraInit;
|
||||
private readonly ICallGateSubscriber<string, string, string>? _penumbraResolvePath;
|
||||
private readonly ICallGateSubscriber<string>? _penumbraResolveModDir;
|
||||
private readonly ICallGateSubscriber<string>? _glamourerGetCharacterCustomization;
|
||||
private readonly ICallGateSubscriber<string, string, object>? _glamourerApplyCharacterCustomization;
|
||||
private readonly ICallGateSubscriber<int> _penumbraApiVersion;
|
||||
private readonly ICallGateSubscriber<int> _glamourerApiVersion;
|
||||
private readonly ICallGateSubscriber<string, string, object>? _glamourerApplyCharacterCustomization;
|
||||
private readonly ICallGateSubscriber<string>? _glamourerGetCharacterCustomization;
|
||||
private readonly ICallGateSubscriber<string, object> _glamourerRevertCustomization;
|
||||
private readonly ICallGateSubscriber<int> _penumbraApiVersion;
|
||||
private readonly ICallGateSubscriber<string, string, bool, (int, string)> _penumbraCreateTemporaryCollection;
|
||||
private readonly ICallGateSubscriber<string, string> _penumbraGetMetaManipulations;
|
||||
private readonly ICallGateSubscriber<object> _penumbraInit;
|
||||
private readonly ICallGateSubscriber<IntPtr, int, object?> _penumbraObjectIsRedrawn;
|
||||
private readonly ICallGateSubscriber<string, int, object>? _penumbraRedraw;
|
||||
private readonly ICallGateSubscriber<string, int> _penumbraRemoveTemporaryCollection;
|
||||
private readonly ICallGateSubscriber<string>? _penumbraResolveModDir;
|
||||
private readonly ICallGateSubscriber<string, string, string>? _penumbraResolvePath;
|
||||
private readonly ICallGateSubscriber<string, string, string[]>? _penumbraReverseResolvePath;
|
||||
private readonly ICallGateSubscriber<string, object> _glamourerRevertCustomization;
|
||||
private readonly ICallGateSubscriber<string, string> _penumbraGetMetaManipulations;
|
||||
private readonly ICallGateSubscriber<string, string, Dictionary<string, string>, string, int, int>
|
||||
_penumbraSetTemporaryMod;
|
||||
private readonly ICallGateSubscriber<string, string, bool, (int, string)> _penumbraCreateTemporaryCollection;
|
||||
private readonly ICallGateSubscriber<string, int> _penumbraRemoveTemporaryCollection;
|
||||
|
||||
public bool Initialized { get; private set; } = false;
|
||||
|
||||
public event EventHandler? PenumbraRedrawEvent;
|
||||
|
||||
public IpcManager(DalamudPluginInterface pi)
|
||||
{
|
||||
_pluginInterface = pi;
|
||||
Logger.Debug("Creating " + nameof(IpcManager));
|
||||
|
||||
_penumbraInit = _pluginInterface.GetIpcSubscriber<object>("Penumbra.Initialized");
|
||||
_penumbraResolvePath = _pluginInterface.GetIpcSubscriber<string, string, string>("Penumbra.ResolveCharacterPath");
|
||||
_penumbraResolveModDir = _pluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory");
|
||||
_penumbraRedraw = _pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
|
||||
_glamourerGetCharacterCustomization = _pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization");
|
||||
_glamourerApplyCharacterCustomization = _pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization");
|
||||
_penumbraReverseResolvePath = _pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath");
|
||||
_penumbraApiVersion = _pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
|
||||
_glamourerApiVersion = _pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion");
|
||||
_glamourerRevertCustomization = _pluginInterface.GetIpcSubscriber<string, object>("Glamourer.RevertCharacterCustomization");
|
||||
_penumbraObjectIsRedrawn = _pluginInterface.GetIpcSubscriber<IntPtr, int, object?>("Penumbra.GameObjectRedrawn");
|
||||
_penumbraInit = pi.GetIpcSubscriber<object>("Penumbra.Initialized");
|
||||
_penumbraResolvePath = pi.GetIpcSubscriber<string, string, string>("Penumbra.ResolveCharacterPath");
|
||||
_penumbraResolveModDir = pi.GetIpcSubscriber<string>("Penumbra.GetModDirectory");
|
||||
_penumbraRedraw = pi.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
|
||||
_glamourerGetCharacterCustomization = pi.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization");
|
||||
_glamourerApplyCharacterCustomization = pi.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization");
|
||||
_penumbraReverseResolvePath = pi.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath");
|
||||
_penumbraApiVersion = pi.GetIpcSubscriber<int>("Penumbra.ApiVersion");
|
||||
_glamourerApiVersion = pi.GetIpcSubscriber<int>("Glamourer.ApiVersion");
|
||||
_glamourerRevertCustomization = pi.GetIpcSubscriber<string, object>("Glamourer.RevertCharacterCustomization");
|
||||
_penumbraObjectIsRedrawn = pi.GetIpcSubscriber<IntPtr, int, object?>("Penumbra.GameObjectRedrawn");
|
||||
_penumbraGetMetaManipulations =
|
||||
_pluginInterface.GetIpcSubscriber<string, string>("Penumbra.GetMetaManipulations");
|
||||
pi.GetIpcSubscriber<string, string>("Penumbra.GetMetaManipulations");
|
||||
|
||||
_penumbraObjectIsRedrawn.Subscribe(RedrawEvent);
|
||||
_penumbraInit.Subscribe(RedrawSelf);
|
||||
|
||||
_penumbraSetTemporaryMod =
|
||||
_pluginInterface
|
||||
pi
|
||||
.GetIpcSubscriber<string, string, Dictionary<string, string>, string, int,
|
||||
int>("Penumbra.AddTemporaryMod");
|
||||
|
||||
_penumbraCreateTemporaryCollection =
|
||||
_pluginInterface.GetIpcSubscriber<string, string, bool, (int, string)>("Penumbra.CreateTemporaryCollection");
|
||||
pi.GetIpcSubscriber<string, string, bool, (int, string)>("Penumbra.CreateTemporaryCollection");
|
||||
_penumbraRemoveTemporaryCollection =
|
||||
_pluginInterface.GetIpcSubscriber<string, int>("Penumbra.RemoveTemporaryCollection");
|
||||
pi.GetIpcSubscriber<string, int>("Penumbra.RemoveTemporaryCollection");
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public event EventHandler? PenumbraRedrawEvent;
|
||||
|
||||
public bool Initialized { get; private set; } = false;
|
||||
public bool CheckGlamourerApi()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _glamourerApiVersion.InvokeFunc() >= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckPenumbraApi()
|
||||
{
|
||||
try
|
||||
@@ -82,17 +85,88 @@ namespace MareSynchronos.Managers
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckGlamourerApi()
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _glamourerApiVersion.InvokeFunc() >= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Logger.Debug("Disposing " + nameof(IpcManager));
|
||||
|
||||
Uninitialize();
|
||||
}
|
||||
|
||||
public void GlamourerApplyCharacterCustomization(string customization, string characterName)
|
||||
{
|
||||
if (!CheckGlamourerApi()) return;
|
||||
Logger.Debug("GlamourerString: " + customization);
|
||||
_glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName);
|
||||
}
|
||||
|
||||
public string? GlamourerGetCharacterCustomization()
|
||||
{
|
||||
if (!CheckGlamourerApi()) return null;
|
||||
return _glamourerGetCharacterCustomization!.InvokeFunc();
|
||||
}
|
||||
|
||||
public void GlamourerRevertCharacterCustomization(string characterName)
|
||||
{
|
||||
if (!CheckGlamourerApi()) return;
|
||||
_glamourerRevertCustomization!.InvokeAction(characterName);
|
||||
}
|
||||
|
||||
public string PenumbraCreateTemporaryCollection(string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return string.Empty;
|
||||
Logger.Debug("Creating temp collection for " + characterName);
|
||||
return _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true).Item2;
|
||||
}
|
||||
|
||||
public string PenumbraGetMetaManipulations(string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return string.Empty;
|
||||
return _penumbraGetMetaManipulations.InvokeFunc(characterName);
|
||||
}
|
||||
|
||||
public string? PenumbraModDirectory()
|
||||
{
|
||||
if (!CheckPenumbraApi()) return null;
|
||||
return _penumbraResolveModDir!.InvokeFunc();
|
||||
}
|
||||
|
||||
public void PenumbraRedraw(string actorName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return;
|
||||
_penumbraRedraw!.InvokeAction(actorName, 0);
|
||||
}
|
||||
|
||||
public void PenumbraRemoveTemporaryCollection(string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return;
|
||||
Logger.Debug("Removing temp collection for " + characterName);
|
||||
_penumbraRemoveTemporaryCollection.InvokeFunc(characterName);
|
||||
}
|
||||
|
||||
public string? PenumbraResolvePath(string path, string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return null;
|
||||
var resolvedPath = _penumbraResolvePath!.InvokeFunc(path, characterName);
|
||||
PluginLog.Verbose("Resolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPath));
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
public string[] PenumbraReverseResolvePath(string path, string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return new[] { path };
|
||||
var resolvedPaths = _penumbraReverseResolvePath!.InvokeFunc(path, characterName);
|
||||
PluginLog.Verbose("ReverseResolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPaths));
|
||||
return resolvedPaths;
|
||||
}
|
||||
|
||||
public void PenumbraSetTemporaryMods(string collectionName, Dictionary<string, string> modPaths, string manipulationData)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return;
|
||||
|
||||
Logger.Debug("Assigning temp mods for " + collectionName);
|
||||
Logger.Debug("ManipulationString: " + manipulationData);
|
||||
var ret = _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", collectionName, modPaths, manipulationData, 0);
|
||||
Logger.Debug("Penumbra Ret: " + ret.ToString());
|
||||
}
|
||||
|
||||
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||
@@ -110,89 +184,7 @@ namespace MareSynchronos.Managers
|
||||
_penumbraInit.Unsubscribe(RedrawSelf);
|
||||
_penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent);
|
||||
Initialized = false;
|
||||
PluginLog.Debug("IPC Manager disposed");
|
||||
}
|
||||
|
||||
public string[] PenumbraReverseResolvePath(string path, string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return new[] { path };
|
||||
var resolvedPaths = _penumbraReverseResolvePath!.InvokeFunc(path, characterName);
|
||||
PluginLog.Verbose("ReverseResolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPaths));
|
||||
return resolvedPaths;
|
||||
}
|
||||
|
||||
public string? PenumbraResolvePath(string path, string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return null;
|
||||
var resolvedPath = _penumbraResolvePath!.InvokeFunc(path, characterName);
|
||||
PluginLog.Verbose("Resolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPath));
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
public string? PenumbraModDirectory()
|
||||
{
|
||||
if (!CheckPenumbraApi()) return null;
|
||||
return _penumbraResolveModDir!.InvokeFunc();
|
||||
}
|
||||
|
||||
public string? GlamourerGetCharacterCustomization()
|
||||
{
|
||||
if (!CheckGlamourerApi()) return null;
|
||||
return _glamourerGetCharacterCustomization!.InvokeFunc();
|
||||
}
|
||||
|
||||
public void GlamourerApplyCharacterCustomization(string customization, string characterName)
|
||||
{
|
||||
if (!CheckGlamourerApi()) return;
|
||||
PluginLog.Debug("GlamourerString: " + customization);
|
||||
_glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName);
|
||||
}
|
||||
|
||||
public void GlamourerRevertCharacterCustomization(string characterName)
|
||||
{
|
||||
if (!CheckGlamourerApi()) return;
|
||||
_glamourerRevertCustomization!.InvokeAction(characterName);
|
||||
}
|
||||
|
||||
public void PenumbraRedraw(string actorName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return;
|
||||
_penumbraRedraw!.InvokeAction(actorName, 0);
|
||||
}
|
||||
|
||||
public string PenumbraCreateTemporaryCollection(string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return string.Empty;
|
||||
PluginLog.Debug("Creating temp collection for " + characterName);
|
||||
return _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true).Item2;
|
||||
}
|
||||
|
||||
public void PenumbraRemoveTemporaryCollection(string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return;
|
||||
PluginLog.Debug("Removing temp collection for " + characterName);
|
||||
_penumbraRemoveTemporaryCollection.InvokeFunc(characterName);
|
||||
}
|
||||
|
||||
public void PenumbraSetTemporaryMods(string collectionName, Dictionary<string, string> modPaths, string manipulationData)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return;
|
||||
|
||||
PluginLog.Debug("Assigning temp mods for " + collectionName);
|
||||
PluginLog.Debug("ManipulationString: " + manipulationData);
|
||||
var ret = _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", collectionName, modPaths, manipulationData, 0);
|
||||
PluginLog.Debug("Penumbra Ret: " + ret.ToString());
|
||||
}
|
||||
|
||||
public string PenumbraGetMetaManipulations(string characterName)
|
||||
{
|
||||
if (!CheckPenumbraApi()) return string.Empty;
|
||||
return _penumbraGetMetaManipulations.InvokeFunc(characterName);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Uninitialize();
|
||||
Logger.Debug("IPC Manager disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
MareSynchronos/Models/CachedPlayer.cs
Normal file
43
MareSynchronos/Models/CachedPlayer.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using MareSynchronos.API;
|
||||
|
||||
namespace MareSynchronos.Models;
|
||||
|
||||
public class CachedPlayer
|
||||
{
|
||||
private bool _isVisible = false;
|
||||
|
||||
public CachedPlayer(string nameHash)
|
||||
{
|
||||
PlayerNameHash = nameHash;
|
||||
}
|
||||
|
||||
public Dictionary<int, CharacterCacheDto> CharacterCache { get; set; } = new();
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set
|
||||
{
|
||||
WasVisible = _isVisible;
|
||||
_isVisible = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int? JobId { get; set; }
|
||||
public PlayerCharacter? PlayerCharacter { get; set; }
|
||||
public string? PlayerName { get; set; }
|
||||
public string PlayerNameHash { get; }
|
||||
public bool WasVisible { get; private set; }
|
||||
public void Reset()
|
||||
{
|
||||
PlayerName = string.Empty;
|
||||
JobId = null;
|
||||
PlayerCharacter = null;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return PlayerNameHash + " : " + PlayerName + " : HasChar " + (PlayerCharacter != null);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,16 @@
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronos.Utils;
|
||||
|
||||
namespace MareSynchronos.Models
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class CharacterCache
|
||||
public class CharacterData
|
||||
{
|
||||
public CharacterCacheDto ToCharacterCacheDto()
|
||||
{
|
||||
return new CharacterCacheDto()
|
||||
{
|
||||
FileReplacements = AllReplacements.Select(f => f.ToFileReplacementDto()).ToList(),
|
||||
GlamourerData = GlamourerString,
|
||||
Hash = CacheHash,
|
||||
JobId = (int)JobId,
|
||||
ManipulationData = ManipulationString
|
||||
};
|
||||
}
|
||||
|
||||
[JsonProperty]
|
||||
public List<FileReplacement> AllReplacements =>
|
||||
FileReplacements.Where(f => f.HasFileReplacement)
|
||||
@@ -32,6 +19,9 @@ namespace MareSynchronos.Models
|
||||
.Distinct().OrderBy(f => f.GamePaths[0])
|
||||
.ToList();
|
||||
|
||||
[JsonProperty]
|
||||
public string CacheHash { get; set; } = string.Empty;
|
||||
|
||||
public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
|
||||
|
||||
[JsonProperty]
|
||||
@@ -40,12 +30,10 @@ namespace MareSynchronos.Models
|
||||
public bool IsReady => FileReplacements.All(f => f.Computed);
|
||||
|
||||
[JsonProperty]
|
||||
public string CacheHash { get; set; } = string.Empty;
|
||||
public uint JobId { get; set; } = 0;
|
||||
|
||||
public string ManipulationString { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty]
|
||||
public uint JobId { get; set; } = 0;
|
||||
public void AddAssociatedResource(FileReplacement resource, FileReplacement? mdlParent, FileReplacement? mtrlParent)
|
||||
{
|
||||
try
|
||||
@@ -71,7 +59,7 @@ namespace MareSynchronos.Models
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Debug(ex.Message);
|
||||
Logger.Debug(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +80,21 @@ namespace MareSynchronos.Models
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Debug(ex.Message);
|
||||
Logger.Debug(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public CharacterCacheDto ToCharacterCacheDto()
|
||||
{
|
||||
return new CharacterCacheDto()
|
||||
{
|
||||
FileReplacements = AllReplacements.Select(f => f.ToFileReplacementDto()).ToList(),
|
||||
GlamourerData = GlamourerString,
|
||||
Hash = CacheHash,
|
||||
JobId = (int)JobId,
|
||||
ManipulationData = ManipulationString
|
||||
};
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder stringBuilder = new();
|
||||
@@ -15,36 +15,35 @@ namespace MareSynchronos.Models
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class FileReplacement
|
||||
{
|
||||
public FileReplacementDto ToFileReplacementDto()
|
||||
{
|
||||
return new FileReplacementDto
|
||||
{
|
||||
GamePaths = GamePaths,
|
||||
Hash = Hash,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly string penumbraDirectory;
|
||||
|
||||
[JsonProperty]
|
||||
public string[] GamePaths { get; set; } = Array.Empty<string>();
|
||||
[JsonProperty]
|
||||
public string ResolvedPath { get; set; } = string.Empty;
|
||||
[JsonProperty]
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
public bool IsInUse { get; set; } = false;
|
||||
public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>();
|
||||
[JsonProperty]
|
||||
public string ImcData { get; set; } = string.Empty;
|
||||
public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath;
|
||||
|
||||
public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed);
|
||||
private Task? computationTask = null;
|
||||
|
||||
public FileReplacement(string penumbraDirectory)
|
||||
{
|
||||
this.penumbraDirectory = penumbraDirectory;
|
||||
}
|
||||
|
||||
public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>();
|
||||
|
||||
public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed);
|
||||
|
||||
[JsonProperty]
|
||||
public string[] GamePaths { get; set; } = Array.Empty<string>();
|
||||
|
||||
public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath;
|
||||
|
||||
[JsonProperty]
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty]
|
||||
public string ImcData { get; set; } = string.Empty;
|
||||
|
||||
public bool IsInUse { get; set; } = false;
|
||||
|
||||
[JsonProperty]
|
||||
public string ResolvedPath { get; set; } = string.Empty;
|
||||
|
||||
public void AddAssociated(FileReplacement fileReplacement)
|
||||
{
|
||||
fileReplacement.IsInUse = true;
|
||||
@@ -52,6 +51,27 @@ namespace MareSynchronos.Models
|
||||
Associated.Add(fileReplacement);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null) return true;
|
||||
if (obj.GetType() == typeof(FileReplacement))
|
||||
{
|
||||
return Hash == ((FileReplacement)obj).Hash;
|
||||
}
|
||||
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int result = 13;
|
||||
result *= 397;
|
||||
result += Hash.GetHashCode();
|
||||
result += ResolvedPath.GetHashCode();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SetResolvedPath(string path)
|
||||
{
|
||||
ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/');
|
||||
@@ -89,6 +109,29 @@ namespace MareSynchronos.Models
|
||||
});
|
||||
}
|
||||
|
||||
public FileReplacementDto ToFileReplacementDto()
|
||||
{
|
||||
return new FileReplacementDto
|
||||
{
|
||||
GamePaths = GamePaths,
|
||||
Hash = Hash,
|
||||
};
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}");
|
||||
foreach (var l1 in Associated)
|
||||
{
|
||||
builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {string.Join(",", l1.GamePaths)} => {l1.ResolvedPath}");
|
||||
foreach (var l2 in l1.Associated)
|
||||
{
|
||||
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}");
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string ComputeHash(FileInfo fi)
|
||||
{
|
||||
// compute hash if hash is not present
|
||||
@@ -115,41 +158,5 @@ namespace MareSynchronos.Models
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}");
|
||||
foreach (var l1 in Associated)
|
||||
{
|
||||
builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {string.Join(",", l1.GamePaths)} => {l1.ResolvedPath}");
|
||||
foreach (var l2 in l1.Associated)
|
||||
{
|
||||
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}");
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null) return true;
|
||||
if (obj.GetType() == typeof(FileReplacement))
|
||||
{
|
||||
return Hash == ((FileReplacement)obj).Hash;
|
||||
}
|
||||
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int result = 13;
|
||||
result *= 397;
|
||||
result += Hash.GetHashCode();
|
||||
result += ResolvedPath.GetHashCode();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronos.PenumbraMod
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptOut)]
|
||||
internal class DefaultMod
|
||||
{
|
||||
public string Name { get; set; } = "Default";
|
||||
public int Priority { get; set; } = 0;
|
||||
public Dictionary<string, string> Files { get; set; } = new();
|
||||
public Dictionary<string, string> FileSwaps { get; set; } = new();
|
||||
public List<string> Manipulations { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronos.PenumbraMod
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptOut)]
|
||||
internal class Meta
|
||||
{
|
||||
public int FileVersion { get; set; } = 1;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = "0";
|
||||
public string Website { get; set; } = string.Empty;
|
||||
public long ImportDate { get; set; } = DateTime.Now.Ticks;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
using MareSynchronos.Factories;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState;
|
||||
using System;
|
||||
using MareSynchronos.Models;
|
||||
using MareSynchronos.PenumbraMod;
|
||||
using Newtonsoft.Json;
|
||||
using MareSynchronos.Managers;
|
||||
using LZ4;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using MareSynchronos.UI;
|
||||
using MareSynchronos.Utils;
|
||||
using Penumbra.PlayerWatch;
|
||||
|
||||
namespace MareSynchronos
|
||||
{
|
||||
@@ -31,7 +24,6 @@ namespace MareSynchronos
|
||||
private readonly CommandManager _commandManager;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly Framework _framework;
|
||||
private readonly IntroUI _introUi;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ObjectTable _objectTable;
|
||||
@@ -39,13 +31,16 @@ namespace MareSynchronos
|
||||
private readonly PluginUi _pluginUi;
|
||||
private readonly WindowSystem _windowSystem;
|
||||
private CharacterManager? _characterManager;
|
||||
private readonly DalamudUtil _dalamudUtil;
|
||||
private readonly CharacterCacheManager _characterCacheManager;
|
||||
private readonly IPlayerWatcher _playerWatcher;
|
||||
|
||||
public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager,
|
||||
Framework framework, ObjectTable objectTable, ClientState clientState)
|
||||
{
|
||||
Logger.Debug("Launching " + Name);
|
||||
_pluginInterface = pluginInterface;
|
||||
_commandManager = commandManager;
|
||||
_framework = framework;
|
||||
_objectTable = objectTable;
|
||||
_clientState = clientState;
|
||||
_configuration = _pluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
@@ -56,6 +51,11 @@ namespace MareSynchronos
|
||||
_apiController = new ApiController(_configuration);
|
||||
_ipcManager = new IpcManager(_pluginInterface);
|
||||
_fileCacheManager = new FileCacheManager(new FileCacheFactory(), _ipcManager, _configuration);
|
||||
_dalamudUtil = new DalamudUtil(_clientState, _objectTable);
|
||||
_characterCacheManager = new CharacterCacheManager(_clientState, framework, _objectTable, _apiController,
|
||||
_dalamudUtil, _ipcManager);
|
||||
_playerWatcher = PlayerWatchFactory.Create(framework, _clientState, _objectTable);
|
||||
_playerWatcher.Enable();
|
||||
|
||||
var uiSharedComponent =
|
||||
new UIShared(_ipcManager, _apiController, _fileCacheManager, _configuration);
|
||||
@@ -83,6 +83,8 @@ namespace MareSynchronos
|
||||
public string Name => "Mare Synchronos";
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Debug("Disposing " + Name);
|
||||
|
||||
_commandManager.RemoveHandler(CommandName);
|
||||
_clientState.Login -= ClientState_Login;
|
||||
_clientState.Logout -= ClientState_Logout;
|
||||
@@ -93,13 +95,16 @@ namespace MareSynchronos
|
||||
_fileCacheManager?.Dispose();
|
||||
_ipcManager?.Dispose();
|
||||
_characterManager?.Dispose();
|
||||
_characterCacheManager.Dispose();
|
||||
_apiController?.Dispose();
|
||||
_playerWatcher.Disable();
|
||||
_playerWatcher.Dispose();
|
||||
}
|
||||
|
||||
|
||||
private void ClientState_Login(object? sender, EventArgs e)
|
||||
{
|
||||
PluginLog.Debug("Client login");
|
||||
Logger.Debug("Client login");
|
||||
|
||||
_pluginInterface.UiBuilder.Draw += Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||
@@ -119,7 +124,7 @@ namespace MareSynchronos
|
||||
|
||||
private void ClientState_Logout(object? sender, EventArgs e)
|
||||
{
|
||||
PluginLog.Debug("Client logout");
|
||||
Logger.Debug("Client logout");
|
||||
_characterManager?.Dispose();
|
||||
_pluginInterface.UiBuilder.Draw -= Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
|
||||
@@ -132,17 +137,23 @@ namespace MareSynchronos
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (_clientState.LocalPlayer == null)
|
||||
while (!_dalamudUtil.IsPlayerPresent)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
var characterCacheFactory =
|
||||
new CharacterCacheFactory(_clientState, _ipcManager, new FileReplacementFactory(_ipcManager));
|
||||
_characterManager = new CharacterManager(
|
||||
_clientState, _framework, _apiController, _objectTable, _ipcManager, _configuration, characterCacheFactory);
|
||||
_characterManager.StartWatchingPlayer();
|
||||
_ipcManager.PenumbraRedraw(_clientState.LocalPlayer!.Name.ToString());
|
||||
try
|
||||
{
|
||||
var characterCacheFactory =
|
||||
new CharacterDataFactory(_clientState, _ipcManager, new FileReplacementFactory(_ipcManager));
|
||||
_characterManager = new CharacterManager(_apiController, _objectTable, _ipcManager,
|
||||
characterCacheFactory, _characterCacheManager, _dalamudUtil, _playerWatcher);
|
||||
_characterManager.StartWatchingPlayer();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Debug(ex.Message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using ImGuiNET;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
using MareSynchronos.Managers;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.Utils;
|
||||
|
||||
namespace MareSynchronos.UI
|
||||
{
|
||||
@@ -25,6 +19,8 @@ namespace MareSynchronos.UI
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Debug("Disposing " + nameof(IntroUI));
|
||||
|
||||
_windowSystem.RemoveWindow(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ using ImGuiNET;
|
||||
using MareSynchronos.WebAPI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MareSynchronos.Managers;
|
||||
using MareSynchronos.Utils;
|
||||
|
||||
namespace MareSynchronos.UI
|
||||
{
|
||||
@@ -24,15 +26,17 @@ namespace MareSynchronos.UI
|
||||
MaximumSize = new(800, 2000),
|
||||
};
|
||||
|
||||
this._configuration = configuration;
|
||||
this._windowSystem = windowSystem;
|
||||
this._apiController = apiController;
|
||||
_configuration = configuration;
|
||||
_windowSystem = windowSystem;
|
||||
_apiController = apiController;
|
||||
_uiShared = uiShared;
|
||||
windowSystem.AddWindow(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Debug("Disposing " + nameof(PluginUi));
|
||||
|
||||
_windowSystem.RemoveWindow(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Colors;
|
||||
using ImGuiNET;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
using MareSynchronos.Managers;
|
||||
using MareSynchronos.WebAPI;
|
||||
|
||||
@@ -167,7 +162,7 @@ namespace MareSynchronos.UI
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
UIShared.TextWrapped("The folder you selected does not exist. Please provide a valid path.");
|
||||
TextWrapped("The folder you selected does not exist. Please provide a valid path.");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
59
MareSynchronos/Utils/DalamudUtil.cs
Normal file
59
MareSynchronos/Utils/DalamudUtil.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
|
||||
namespace MareSynchronos.Utils
|
||||
{
|
||||
public class DalamudUtil
|
||||
{
|
||||
private readonly ClientState _clientState;
|
||||
private readonly ObjectTable _objectTable;
|
||||
|
||||
public DalamudUtil(ClientState clientState, ObjectTable objectTable)
|
||||
{
|
||||
_clientState = clientState;
|
||||
_objectTable = objectTable;
|
||||
}
|
||||
|
||||
public bool IsPlayerPresent => _clientState.LocalPlayer != null;
|
||||
|
||||
public string PlayerName => _clientState.LocalPlayer!.Name.ToString();
|
||||
|
||||
public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id);
|
||||
|
||||
public Dictionary<string, PlayerCharacter> GetLocalPlayers()
|
||||
{
|
||||
Dictionary<string, PlayerCharacter> allLocalPlayers = new();
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||
string playerName = obj.Name.ToString();
|
||||
if (playerName == PlayerName) continue;
|
||||
var playerObject = (PlayerCharacter)obj;
|
||||
allLocalPlayers[Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString())] = playerObject;
|
||||
}
|
||||
|
||||
return allLocalPlayers;
|
||||
}
|
||||
|
||||
public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress)
|
||||
{
|
||||
var obj = (GameObject*)characterAddress;
|
||||
|
||||
while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something
|
||||
{
|
||||
Logger.Debug("Waiting for character to finish drawing");
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
// wait half a second just in case
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
public void WaitWhileSelfIsDrawing() => WaitWhileCharacterIsDrawing(_clientState.LocalPlayer!.Address);
|
||||
}
|
||||
}
|
||||
14
MareSynchronos/Utils/Logger.cs
Normal file
14
MareSynchronos/Utils/Logger.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Diagnostics;
|
||||
using Dalamud.Logging;
|
||||
|
||||
namespace MareSynchronos.Utils
|
||||
{
|
||||
internal class Logger
|
||||
{
|
||||
public static void Debug(string debug)
|
||||
{
|
||||
var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown";
|
||||
PluginLog.Debug($"[{caller}] {debug}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,35 +11,57 @@ using System.Threading.Tasks;
|
||||
using LZ4;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronos.FileCacheDB;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace MareSynchronos.WebAPI
|
||||
{
|
||||
public class CharacterReceivedEventArgs : EventArgs
|
||||
{
|
||||
public CharacterReceivedEventArgs(string characterNameHash, CharacterCacheDto characterData)
|
||||
{
|
||||
CharacterData = characterData;
|
||||
CharacterNameHash = characterNameHash;
|
||||
}
|
||||
|
||||
public CharacterCacheDto CharacterData { get; set; }
|
||||
public string CharacterNameHash { get; set; }
|
||||
}
|
||||
|
||||
public class ApiController : IDisposable
|
||||
{
|
||||
public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)";
|
||||
|
||||
private readonly Configuration _pluginConfiguration;
|
||||
public const string MainServiceUri = "https://darkarchon.internet-box.ch:5001";
|
||||
public string UID { get; private set; } = string.Empty;
|
||||
public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : "-";
|
||||
private string CacheFolder => _pluginConfiguration.CacheFolder;
|
||||
public ConcurrentDictionary<string, (long, long)> CurrentUploads { get; } = new();
|
||||
readonly CancellationTokenSource _cts;
|
||||
private readonly Configuration _pluginConfiguration;
|
||||
private HubConnection? _fileHub;
|
||||
private HubConnection? _heartbeatHub;
|
||||
private CancellationTokenSource? _uploadCancellationTokenSource;
|
||||
private HubConnection? _userHub;
|
||||
public ApiController(Configuration pluginConfiguration)
|
||||
{
|
||||
Logger.Debug("Creating " + nameof(ApiController));
|
||||
|
||||
_pluginConfiguration = pluginConfiguration;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
_ = Heartbeat();
|
||||
}
|
||||
|
||||
public event EventHandler<CharacterReceivedEventArgs>? CharacterReceived;
|
||||
|
||||
public event EventHandler? Connected;
|
||||
|
||||
public event EventHandler? Disconnected;
|
||||
|
||||
public event EventHandler? PairedClientOffline;
|
||||
|
||||
public event EventHandler? PairedClientOnline;
|
||||
|
||||
public event EventHandler? PairedWithOther;
|
||||
|
||||
public event EventHandler? UnpairedFromOther;
|
||||
|
||||
public ConcurrentDictionary<string, (long, long)> CurrentDownloads { get; } = new();
|
||||
public ConcurrentDictionary<string, (long, long)> CurrentUploads { get; } = new();
|
||||
public bool IsConnected => !string.IsNullOrEmpty(UID);
|
||||
public bool IsDownloading { get; private set; } = false;
|
||||
public bool IsUploading { get; private set; } = false;
|
||||
public List<ClientPairDto> PairedClients { get; set; } = new();
|
||||
public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : "-";
|
||||
public bool ServerAlive =>
|
||||
(_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected;
|
||||
|
||||
public string UID { get; private set; } = string.Empty;
|
||||
public bool UseCustomService
|
||||
{
|
||||
get => _pluginConfiguration.UseCustomService;
|
||||
@@ -49,43 +71,91 @@ namespace MareSynchronos.WebAPI
|
||||
_pluginConfiguration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private string ApiUri => UseCustomService ? _pluginConfiguration.ApiUri : MainServiceUri;
|
||||
|
||||
public bool ServerAlive =>
|
||||
(_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected;
|
||||
public bool IsConnected => !string.IsNullOrEmpty(UID);
|
||||
|
||||
public event EventHandler? Connected;
|
||||
public event EventHandler? Disconnected;
|
||||
public event EventHandler<CharacterReceivedEventArgs>? CharacterReceived;
|
||||
public event EventHandler? UnpairedFromOther;
|
||||
public event EventHandler? PairedWithOther;
|
||||
public event EventHandler? PairedClientOnline;
|
||||
public event EventHandler? PairedClientOffline;
|
||||
|
||||
public List<ClientPairDto> PairedClients { get; set; } = new();
|
||||
|
||||
readonly CancellationTokenSource cts;
|
||||
private HubConnection? _heartbeatHub;
|
||||
private HubConnection? _fileHub;
|
||||
private HubConnection? _userHub;
|
||||
private CancellationTokenSource? uploadCancellationTokenSource;
|
||||
|
||||
public ApiController(Configuration pluginConfiguration)
|
||||
private string CacheFolder => _pluginConfiguration.CacheFolder;
|
||||
public void CancelUpload()
|
||||
{
|
||||
this._pluginConfiguration = pluginConfiguration;
|
||||
cts = new CancellationTokenSource();
|
||||
if (_uploadCancellationTokenSource != null)
|
||||
{
|
||||
PluginLog.Warning("Cancelling upload");
|
||||
_uploadCancellationTokenSource?.Cancel();
|
||||
_fileHub!.InvokeAsync("AbortUpload");
|
||||
}
|
||||
}
|
||||
|
||||
_ = Heartbeat();
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.Debug("Disposing " + nameof(ApiController));
|
||||
|
||||
_cts?.Cancel();
|
||||
_ = DisposeHubConnections();
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadFile(string hash)
|
||||
{
|
||||
IsDownloading = true;
|
||||
var reader = await _fileHub!.StreamAsChannelAsync<byte[]>("DownloadFile", hash);
|
||||
List<byte> downloadedData = new();
|
||||
while (await reader.WaitToReadAsync())
|
||||
{
|
||||
while (reader.TryRead(out var data))
|
||||
{
|
||||
CurrentDownloads[hash] = (CurrentDownloads[hash].Item1 + data.Length, CurrentDownloads[hash].Item2);
|
||||
downloadedData.AddRange(data);
|
||||
//await Task.Delay(25);
|
||||
}
|
||||
}
|
||||
|
||||
IsDownloading = false;
|
||||
return downloadedData.ToArray();
|
||||
}
|
||||
|
||||
public async Task DownloadFiles(List<FileReplacementDto> fileReplacementDto)
|
||||
{
|
||||
foreach (var file in fileReplacementDto)
|
||||
{
|
||||
var fileSize = await _fileHub!.InvokeAsync<long>("GetFileSize", file.Hash);
|
||||
CurrentDownloads[file.Hash] = (0, fileSize);
|
||||
}
|
||||
|
||||
foreach (var file in fileReplacementDto.Where(f => CurrentDownloads[f.Hash].Item2 > 0))
|
||||
{
|
||||
var hash = file.Hash;
|
||||
var data = await DownloadFile(hash);
|
||||
var extractedFile = LZ4Codec.Unwrap(data);
|
||||
var ext = file.GamePaths.First().Split(".", StringSplitOptions.None).Last();
|
||||
var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash + "." + ext);
|
||||
await File.WriteAllBytesAsync(filePath, extractedFile);
|
||||
await using (var db = new FileCacheContext())
|
||||
{
|
||||
db.Add(new FileCache
|
||||
{
|
||||
Filepath = filePath.ToLower(),
|
||||
Hash = file.Hash,
|
||||
LastModifiedDate = DateTime.Now.Ticks.ToString(),
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
Logger.Debug("File downloaded to " + filePath);
|
||||
}
|
||||
|
||||
CurrentDownloads.Clear();
|
||||
}
|
||||
|
||||
public async Task GetCharacterData(Dictionary<string, int> hashedCharacterNames)
|
||||
{
|
||||
await _userHub!.InvokeAsync("GetCharacterData",
|
||||
hashedCharacterNames);
|
||||
}
|
||||
|
||||
public async Task Heartbeat()
|
||||
{
|
||||
while (!ServerAlive && !cts.Token.IsCancellationRequested)
|
||||
while (!ServerAlive && !_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
PluginLog.Debug("Attempting to establish heartbeat connection to " + ApiUri);
|
||||
Logger.Debug("Attempting to establish heartbeat connection to " + ApiUri);
|
||||
_heartbeatHub = new HubConnectionBuilder()
|
||||
.WithUrl(ApiUri + "/heartbeat", options =>
|
||||
{
|
||||
@@ -105,9 +175,9 @@ namespace MareSynchronos.WebAPI
|
||||
#endif
|
||||
}).Build();
|
||||
|
||||
await _heartbeatHub.StartAsync(cts.Token);
|
||||
await _heartbeatHub.StartAsync(_cts.Token);
|
||||
UID = await _heartbeatHub!.InvokeAsync<string>("Heartbeat");
|
||||
PluginLog.Debug("Heartbeat started: " + ApiUri);
|
||||
Logger.Debug("Heartbeat started: " + ApiUri);
|
||||
try
|
||||
{
|
||||
await InitializeHubConnections();
|
||||
@@ -121,7 +191,7 @@ namespace MareSynchronos.WebAPI
|
||||
|
||||
_heartbeatHub.Closed += OnHeartbeatHubOnClosed;
|
||||
_heartbeatHub.Reconnected += OnHeartbeatHubOnReconnected;
|
||||
PluginLog.Debug("Heartbeat established to: " + ApiUri);
|
||||
Logger.Debug("Heartbeat established to: " + ApiUri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -130,46 +200,132 @@ namespace MareSynchronos.WebAPI
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadInitialData()
|
||||
public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash)
|
||||
{
|
||||
var pairedClients = await _userHub!.InvokeAsync<List<ClientPairDto>>("GetPairedClients");
|
||||
PairedClients = pairedClients.ToList();
|
||||
Logger.Debug("Received DTO for " + characterHash);
|
||||
CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Register()
|
||||
{
|
||||
if (!ServerAlive) return;
|
||||
Logger.Debug("Registering at service " + ApiUri);
|
||||
var response = await _userHub!.InvokeAsync<string>("Register");
|
||||
_pluginConfiguration.ClientSecret[ApiUri] = response;
|
||||
_pluginConfiguration.Save();
|
||||
RestartHeartbeat();
|
||||
}
|
||||
|
||||
public void RestartHeartbeat()
|
||||
{
|
||||
PluginLog.Debug("Restarting heartbeat");
|
||||
Logger.Debug("Restarting heartbeat");
|
||||
|
||||
_heartbeatHub!.Closed -= OnHeartbeatHubOnClosed;
|
||||
_heartbeatHub!.Reconnected -= OnHeartbeatHubOnReconnected;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await _heartbeatHub.StopAsync(cts.Token);
|
||||
await _heartbeatHub.StopAsync(_cts.Token);
|
||||
await _heartbeatHub.DisposeAsync();
|
||||
_heartbeatHub = null!;
|
||||
_ = Heartbeat();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnHeartbeatHubOnReconnected(string? s)
|
||||
public async Task SendCharacterData(CharacterCacheDto character, List<string> visibleCharacterIds)
|
||||
{
|
||||
PluginLog.Debug("Reconnected: " + ApiUri);
|
||||
UID = await _heartbeatHub!.InvokeAsync<string>("Heartbeat");
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
Logger.Debug("Sending Character data to service " + ApiUri);
|
||||
|
||||
CancelUpload();
|
||||
_uploadCancellationTokenSource = new CancellationTokenSource();
|
||||
var uploadToken = _uploadCancellationTokenSource.Token;
|
||||
Logger.Debug("New Token Created");
|
||||
|
||||
var filesToUpload = await _fileHub!.InvokeAsync<List<string>>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken);
|
||||
|
||||
IsUploading = true;
|
||||
|
||||
Logger.Debug("Compressing files");
|
||||
Dictionary<string, byte[]> compressedFileData = new();
|
||||
foreach (var file in filesToUpload)
|
||||
{
|
||||
Logger.Debug(file);
|
||||
var data = await GetCompressedFileData(file, uploadToken);
|
||||
compressedFileData.Add(data.Item1, data.Item2);
|
||||
CurrentUploads[data.Item1] = (0, data.Item2.Length);
|
||||
}
|
||||
Logger.Debug("Files compressed, uploading files");
|
||||
foreach (var data in compressedFileData)
|
||||
{
|
||||
await UploadFile(data.Value, data.Key, uploadToken);
|
||||
if (uploadToken.IsCancellationRequested)
|
||||
{
|
||||
PluginLog.Warning("Cancel in filesToUpload loop detected");
|
||||
CurrentUploads.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Logger.Debug("Upload tasks complete, waiting for server to confirm");
|
||||
var anyUploadsOpen = await _fileHub!.InvokeAsync<bool>("IsUploadFinished", uploadToken);
|
||||
Logger.Debug("Uploads open: " + anyUploadsOpen);
|
||||
while (anyUploadsOpen && !uploadToken.IsCancellationRequested)
|
||||
{
|
||||
anyUploadsOpen = await _fileHub!.InvokeAsync<bool>("IsUploadFinished", uploadToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken);
|
||||
Logger.Debug("Waiting for uploads to finish");
|
||||
}
|
||||
|
||||
CurrentUploads.Clear();
|
||||
IsUploading = false;
|
||||
|
||||
if (!uploadToken.IsCancellationRequested)
|
||||
{
|
||||
Logger.Debug("=== Pushing character data ===");
|
||||
await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds, uploadToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Warning("=== Upload operation was cancelled ===");
|
||||
}
|
||||
|
||||
Logger.Debug("== Upload complete for " + character.JobId);
|
||||
_uploadCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private Task OnHeartbeatHubOnClosed(Exception? exception)
|
||||
public async Task<List<string>> SendCharacterName(string hashedName)
|
||||
{
|
||||
PluginLog.Debug("Connection closed: " + ApiUri);
|
||||
Disconnected?.Invoke(null, EventArgs.Empty);
|
||||
RestartHeartbeat();
|
||||
return Task.CompletedTask;
|
||||
return await _userHub!.InvokeAsync<List<string>>("SendCharacterNameHash", hashedName);
|
||||
}
|
||||
|
||||
public async Task SendPairedClientAddition(string uid)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
await _userHub!.SendAsync("SendPairedClientAddition", uid);
|
||||
}
|
||||
|
||||
public async Task SendPairedClientPauseChange(string uid, bool paused)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
await _userHub!.SendAsync("SendPairedClientPauseChange", uid, paused);
|
||||
}
|
||||
|
||||
public async Task SendPairedClientRemoval(string uid)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
await _userHub!.SendAsync("SendPairedClientRemoval", uid);
|
||||
}
|
||||
|
||||
public async Task UpdateCurrentDownloadSize(string hash)
|
||||
{
|
||||
long fileSize = await _fileHub!.InvokeAsync<long>("GetFileSize", hash);
|
||||
}
|
||||
|
||||
private async Task DisposeHubConnections()
|
||||
{
|
||||
if (_fileHub != null)
|
||||
{
|
||||
PluginLog.Debug("Disposing File Hub");
|
||||
Logger.Debug("Disposing File Hub");
|
||||
CancelUpload();
|
||||
await _fileHub!.StopAsync();
|
||||
await _fileHub!.DisposeAsync();
|
||||
@@ -177,17 +333,25 @@ namespace MareSynchronos.WebAPI
|
||||
|
||||
if (_userHub != null)
|
||||
{
|
||||
PluginLog.Debug("Disposing User Hub");
|
||||
Logger.Debug("Disposing User Hub");
|
||||
await _userHub.StopAsync();
|
||||
await _userHub.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
||||
{
|
||||
await using var db = new FileCacheContext();
|
||||
var fileCache = db.FileCaches.First(f => f.Hash == fileHash);
|
||||
return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath, uploadToken), 0,
|
||||
(int)new FileInfo(fileCache.Filepath).Length));
|
||||
}
|
||||
|
||||
private async Task InitializeHubConnections()
|
||||
{
|
||||
await DisposeHubConnections();
|
||||
|
||||
PluginLog.Debug("Creating User Hub");
|
||||
Logger.Debug("Creating User Hub");
|
||||
_userHub = new HubConnectionBuilder()
|
||||
.WithUrl(ApiUri + "/user", options =>
|
||||
{
|
||||
@@ -209,7 +373,7 @@ namespace MareSynchronos.WebAPI
|
||||
_userHub.On<string>("RemoveOnlinePairedPlayer", (s) => PairedClientOffline?.Invoke(s, EventArgs.Empty));
|
||||
_userHub.On<string>("AddOnlinePairedPlayer", (s) => PairedClientOnline?.Invoke(s, EventArgs.Empty));
|
||||
|
||||
PluginLog.Debug("Creating File Hub");
|
||||
Logger.Debug("Creating File Hub");
|
||||
_fileHub = new HubConnectionBuilder()
|
||||
.WithUrl(ApiUri + "/files", options =>
|
||||
{
|
||||
@@ -225,9 +389,27 @@ namespace MareSynchronos.WebAPI
|
||||
#endif
|
||||
})
|
||||
.Build();
|
||||
await _fileHub.StartAsync(cts.Token);
|
||||
await _fileHub.StartAsync(_cts.Token);
|
||||
}
|
||||
|
||||
private async Task LoadInitialData()
|
||||
{
|
||||
var pairedClients = await _userHub!.InvokeAsync<List<ClientPairDto>>("GetPairedClients");
|
||||
PairedClients = pairedClients.ToList();
|
||||
}
|
||||
private Task OnHeartbeatHubOnClosed(Exception? exception)
|
||||
{
|
||||
Logger.Debug("Connection closed: " + ApiUri);
|
||||
Disconnected?.Invoke(null, EventArgs.Empty);
|
||||
RestartHeartbeat();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnHeartbeatHubOnReconnected(string? s)
|
||||
{
|
||||
Logger.Debug("Reconnected: " + ApiUri);
|
||||
UID = await _heartbeatHub!.InvokeAsync<string>("Heartbeat");
|
||||
}
|
||||
private void UpdateLocalClientPairs(ClientPairDto dto, string characterIdentifier)
|
||||
{
|
||||
var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID);
|
||||
@@ -258,15 +440,6 @@ namespace MareSynchronos.WebAPI
|
||||
UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
||||
{
|
||||
await using var db = new FileCacheContext();
|
||||
var fileCache = db.FileCaches.First(f => f.Hash == fileHash);
|
||||
return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath, uploadToken), 0,
|
||||
(int)new FileInfo(fileCache.Filepath).Length));
|
||||
}
|
||||
|
||||
private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken)
|
||||
{
|
||||
if (uploadToken.IsCancellationRequested) return;
|
||||
@@ -290,184 +463,17 @@ namespace MareSynchronos.WebAPI
|
||||
|
||||
channel.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Register()
|
||||
public class CharacterReceivedEventArgs : EventArgs
|
||||
{
|
||||
public CharacterReceivedEventArgs(string characterNameHash, CharacterCacheDto characterData)
|
||||
{
|
||||
if (!ServerAlive) return;
|
||||
PluginLog.Debug("Registering at service " + ApiUri);
|
||||
var response = await _userHub!.InvokeAsync<string>("Register");
|
||||
_pluginConfiguration.ClientSecret[ApiUri] = response;
|
||||
_pluginConfiguration.Save();
|
||||
RestartHeartbeat();
|
||||
CharacterData = characterData;
|
||||
CharacterNameHash = characterNameHash;
|
||||
}
|
||||
|
||||
public void CancelUpload()
|
||||
{
|
||||
if (uploadCancellationTokenSource != null)
|
||||
{
|
||||
PluginLog.Warning("Cancelling upload");
|
||||
uploadCancellationTokenSource?.Cancel();
|
||||
_fileHub!.InvokeAsync("AbortUpload");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendCharacterData(CharacterCacheDto character, List<string> visibleCharacterIds)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
PluginLog.Debug("Sending Character data to service " + ApiUri);
|
||||
|
||||
CancelUpload();
|
||||
uploadCancellationTokenSource = new CancellationTokenSource();
|
||||
var uploadToken = uploadCancellationTokenSource.Token;
|
||||
PluginLog.Debug("New Token Created");
|
||||
|
||||
var filesToUpload = await _fileHub!.InvokeAsync<List<string>>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken);
|
||||
|
||||
IsUploading = true;
|
||||
|
||||
PluginLog.Debug("Compressing files");
|
||||
Dictionary<string, byte[]> compressedFileData = new();
|
||||
foreach (var file in filesToUpload)
|
||||
{
|
||||
PluginLog.Debug(file);
|
||||
var data = await GetCompressedFileData(file, uploadToken);
|
||||
compressedFileData.Add(data.Item1, data.Item2);
|
||||
CurrentUploads[data.Item1] = (0, data.Item2.Length);
|
||||
}
|
||||
PluginLog.Debug("Files compressed, uploading files");
|
||||
foreach (var data in compressedFileData)
|
||||
{
|
||||
await UploadFile(data.Value, data.Key, uploadToken);
|
||||
if (uploadToken.IsCancellationRequested)
|
||||
{
|
||||
PluginLog.Warning("Cancel in filesToUpload loop detected");
|
||||
CurrentUploads.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
PluginLog.Debug("Upload tasks complete, waiting for server to confirm");
|
||||
var anyUploadsOpen = await _fileHub!.InvokeAsync<bool>("IsUploadFinished", uploadToken);
|
||||
PluginLog.Debug("Uploads open: " + anyUploadsOpen);
|
||||
while (anyUploadsOpen && !uploadToken.IsCancellationRequested)
|
||||
{
|
||||
anyUploadsOpen = await _fileHub!.InvokeAsync<bool>("IsUploadFinished", uploadToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken);
|
||||
PluginLog.Debug("Waiting for uploads to finish");
|
||||
}
|
||||
|
||||
CurrentUploads.Clear();
|
||||
IsUploading = false;
|
||||
|
||||
if (!uploadToken.IsCancellationRequested)
|
||||
{
|
||||
PluginLog.Debug("=== Pushing character data ===");
|
||||
await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds, uploadToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Warning("=== Upload operation was cancelled ===");
|
||||
}
|
||||
|
||||
PluginLog.Debug("== Upload complete for " + character.JobId);
|
||||
uploadCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash)
|
||||
{
|
||||
PluginLog.Debug("Received DTO for " + characterHash);
|
||||
CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task UpdateCurrentDownloadSize(string hash)
|
||||
{
|
||||
long fileSize = await _fileHub!.InvokeAsync<long>("GetFileSize", hash);
|
||||
}
|
||||
|
||||
public async Task DownloadFiles(List<FileReplacementDto> fileReplacementDto, string cacheFolder)
|
||||
{
|
||||
foreach (var file in fileReplacementDto)
|
||||
{
|
||||
var fileSize = await _fileHub!.InvokeAsync<long>("GetFileSize", file.Hash);
|
||||
CurrentDownloads[file.Hash] = (0, fileSize);
|
||||
}
|
||||
|
||||
foreach (var file in fileReplacementDto.Where(f => CurrentDownloads[f.Hash].Item2 > 0))
|
||||
{
|
||||
var hash = file.Hash;
|
||||
var data = await DownloadFile(hash);
|
||||
var extractedFile = LZ4.LZ4Codec.Unwrap(data);
|
||||
var ext = file.GamePaths.First().Split(".", StringSplitOptions.None).Last();
|
||||
var filePath = Path.Combine(cacheFolder, file.Hash + "." + ext);
|
||||
await File.WriteAllBytesAsync(filePath, extractedFile);
|
||||
await using (var db = new FileCacheContext())
|
||||
{
|
||||
db.Add(new FileCache
|
||||
{
|
||||
Filepath = filePath.ToLower(),
|
||||
Hash = file.Hash,
|
||||
LastModifiedDate = DateTime.Now.Ticks.ToString(),
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
PluginLog.Debug("File downloaded to " + filePath);
|
||||
}
|
||||
|
||||
CurrentDownloads.Clear();
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadFile(string hash)
|
||||
{
|
||||
IsDownloading = true;
|
||||
var reader = await _fileHub!.StreamAsChannelAsync<byte[]>("DownloadFile", hash);
|
||||
List<byte> downloadedData = new();
|
||||
while (await reader.WaitToReadAsync())
|
||||
{
|
||||
while (reader.TryRead(out var data))
|
||||
{
|
||||
CurrentDownloads[hash] = (CurrentDownloads[hash].Item1 + data.Length, CurrentDownloads[hash].Item2);
|
||||
downloadedData.AddRange(data);
|
||||
//await Task.Delay(25);
|
||||
}
|
||||
}
|
||||
|
||||
IsDownloading = false;
|
||||
return downloadedData.ToArray();
|
||||
}
|
||||
|
||||
public async Task GetCharacterData(Dictionary<string, int> hashedCharacterNames)
|
||||
{
|
||||
await _userHub!.InvokeAsync("GetCharacterData",
|
||||
hashedCharacterNames);
|
||||
}
|
||||
|
||||
public async Task SendPairedClientPauseChange(string uid, bool paused)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
await _userHub!.SendAsync("SendPairedClientPauseChange", uid, paused);
|
||||
}
|
||||
|
||||
public async Task SendPairedClientAddition(string uid)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
await _userHub!.SendAsync("SendPairedClientAddition", uid);
|
||||
}
|
||||
|
||||
public async Task SendPairedClientRemoval(string uid)
|
||||
{
|
||||
if (!IsConnected || SecretKey == "-") return;
|
||||
await _userHub!.SendAsync("SendPairedClientRemoval", uid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cts?.Cancel();
|
||||
_ = DisposeHubConnections();
|
||||
}
|
||||
|
||||
public async Task<List<string>> SendCharacterName(string hashedName)
|
||||
{
|
||||
return await _userHub!.InvokeAsync<List<string>>("SendCharacterNameHash", hashedName);
|
||||
}
|
||||
public CharacterCacheDto CharacterData { get; set; }
|
||||
public string CharacterNameHash { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user