major refactoring, maybe some bugfixes, idk

This commit is contained in:
Stanley Dimant
2022-06-24 00:47:47 +02:00
parent 0fe3f1cf25
commit 2dcd02d170
22 changed files with 997 additions and 949 deletions

View File

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

View File

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

View File

@@ -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,

View File

@@ -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()!);
}
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
#nullable disable
#nullable disable
namespace MareSynchronos.FileCacheDB
{

View File

@@ -1,7 +1,6 @@
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
#nullable disable

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

View File

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

View File

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

View File

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

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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