From 0df75fe0852088c1da81c5198ff246e91c8f33d2 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sun, 26 Jun 2022 14:34:56 +0200 Subject: [PATCH] move handling of player data application to CachedPlayer --- .../Managers/CachedPlayersManager.cs | 198 ++++++++++ .../Managers/CharacterCacheManager.cs | 354 ------------------ .../{CharacterManager.cs => PlayerManager.cs} | 16 +- MareSynchronos/Models/CachedPlayer.cs | 198 +++++++++- MareSynchronos/Plugin.cs | 8 +- MareSynchronos/Utils/DalamudUtil.cs | 20 +- 6 files changed, 411 insertions(+), 383 deletions(-) create mode 100644 MareSynchronos/Managers/CachedPlayersManager.cs delete mode 100644 MareSynchronos/Managers/CharacterCacheManager.cs rename MareSynchronos/Managers/{CharacterManager.cs => PlayerManager.cs} (89%) diff --git a/MareSynchronos/Managers/CachedPlayersManager.cs b/MareSynchronos/Managers/CachedPlayersManager.cs new file mode 100644 index 0000000..71aa249 --- /dev/null +++ b/MareSynchronos/Managers/CachedPlayersManager.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.SubKinds; +using MareSynchronos.Models; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Penumbra.PlayerWatch; + +namespace MareSynchronos.Managers; + +public class CachedPlayersManager : IDisposable +{ + private readonly ApiController _apiController; + private readonly ClientState _clientState; + private readonly DalamudUtil _dalamudUtil; + private readonly Framework _framework; + private readonly IpcManager _ipcManager; + private readonly IPlayerWatcher _watcher; + private readonly ObjectTable _objectTable; + private readonly List _onlineCachedPlayers = new(); + private readonly List _localVisiblePlayers = new(); + private DateTime _lastPlayerObjectCheck = DateTime.Now; + + public CachedPlayersManager(ClientState clientState, Framework framework, ObjectTable objectTable, ApiController apiController, DalamudUtil dalamudUtil, IpcManager ipcManager, IPlayerWatcher watcher) + { + Logger.Debug("Creating " + nameof(CachedPlayersManager)); + + _clientState = clientState; + _framework = framework; + _objectTable = objectTable; + _apiController = apiController; + _dalamudUtil = dalamudUtil; + _ipcManager = ipcManager; + _watcher = watcher; + + _clientState.Login += ClientStateOnLogin; + _clientState.Logout += ClientStateOnLogout; + + _apiController.PairedClientOnline += ApiControllerOnPairedClientOnline; + _apiController.PairedClientOffline += ApiControllerOnPairedClientOffline; + _apiController.PairedWithOther += ApiControllerOnPairedWithOther; + _apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther; + _apiController.Disconnected += ApiControllerOnDisconnected; + _ipcManager.PenumbraDisposed += IpcManagerOnPenumbraDisposed; + + if (clientState.IsLoggedIn) + { + ClientStateOnLogin(null, EventArgs.Empty); + } + } + + private void IpcManagerOnPenumbraDisposed(object? sender, EventArgs e) + { + _onlineCachedPlayers.ForEach(p => p.DisposePlayer()); + } + + private void ApiControllerOnDisconnected(object? sender, EventArgs e) + { + RestoreAllCharacters(); + } + + private void ClientStateOnLogin(object? sender, EventArgs e) + { + _framework.Update += FrameworkOnUpdate; + } + + private void ClientStateOnLogout(object? sender, EventArgs e) + { + _framework.Update -= FrameworkOnUpdate; + } + + public void AddInitialPairs(List apiTaskResult) + { + _onlineCachedPlayers.Clear(); + _onlineCachedPlayers.AddRange(apiTaskResult.Select(CreateCachedPlayer)); + Logger.Debug("Online and paired users: " + string.Join(",", _onlineCachedPlayers)); + } + + public void Dispose() + { + Logger.Debug("Disposing " + nameof(CachedPlayersManager)); + + RestoreAllCharacters(); + + _apiController.PairedClientOnline -= ApiControllerOnPairedClientOnline; + _apiController.PairedClientOffline -= ApiControllerOnPairedClientOffline; + _apiController.PairedWithOther -= ApiControllerOnPairedWithOther; + _apiController.UnpairedFromOther -= ApiControllerOnUnpairedFromOther; + _ipcManager.PenumbraDisposed -= ApiControllerOnDisconnected; + _framework.Update -= FrameworkOnUpdate; + _clientState.Login -= ClientStateOnLogin; + _clientState.Logout -= ClientStateOnLogout; + } + + private void RestoreAllCharacters() + { + _onlineCachedPlayers.ForEach(p => p.DisposePlayer()); + _onlineCachedPlayers.Clear(); + } + + public async Task UpdatePlayersFromService(Dictionary playerJobIds) + { + if (!playerJobIds.Any()) return; + Logger.Debug("Getting data for new players: " + string.Join(Environment.NewLine, playerJobIds)); + await _apiController.GetCharacterData(playerJobIds); + } + + private void ApiControllerOnPairedClientOffline(object? sender, EventArgs e) + { + Logger.Debug("Player offline: " + sender!); + RemovePlayer((string)sender!); + } + + private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e) + { + Logger.Debug("Player online: " + sender!); + AddPlayer((string)sender!); + return; + } + + private void ApiControllerOnPairedWithOther(object? sender, EventArgs e) + { + var characterHash = (string?)sender; + if (string.IsNullOrEmpty(characterHash)) return; + Logger.Debug("Pairing with " + characterHash); + AddPlayer(characterHash); + } + + private void ApiControllerOnUnpairedFromOther(object? sender, EventArgs e) + { + var characterHash = (string?)sender; + if (string.IsNullOrEmpty(characterHash)) return; + Logger.Debug("Unpairing from " + characterHash); + RemovePlayer(characterHash); + } + + private void AddPlayer(string characterNameHash) + { + if (_onlineCachedPlayers.Any(p => p.PlayerNameHash == characterNameHash)) return; + _onlineCachedPlayers.Add(CreateCachedPlayer(characterNameHash)); + } + + private void RemovePlayer(string characterHash) + { + var cachedPlayer = _onlineCachedPlayers.Single(p => p.PlayerNameHash == characterHash); + cachedPlayer.DisposePlayer(); + _onlineCachedPlayers.Remove(cachedPlayer); + } + + private void FrameworkOnUpdate(Framework framework) + { + if (_clientState.LocalPlayer == null) return; + + if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(0.25)) return; + + _localVisiblePlayers.Clear(); + if (!_ipcManager.Initialized) return; + string ownName = _dalamudUtil.PlayerName; + var playerCharacters = _objectTable.Where(obj => + obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && + obj.Name.ToString() != ownName).ToList(); + foreach (var obj in playerCharacters) + { + var pObj = (PlayerCharacter)obj; + var pObjName = pObj.Name.ToString(); + _localVisiblePlayers.Add(pObjName); + var existingCachedPlayer = _onlineCachedPlayers.SingleOrDefault(p => p.PlayerName == pObjName); + if (existingCachedPlayer != null) + { + existingCachedPlayer.IsVisible = true; + continue; + } + + var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString()); + _onlineCachedPlayers.SingleOrDefault(p => p.PlayerNameHash == hashedName)?.InitializePlayer(pObj); + } + + _onlineCachedPlayers.Where(p => !string.IsNullOrEmpty(p.PlayerName) && !_localVisiblePlayers.Contains(p.PlayerName)) + .ToList().ForEach(p => p.DisposePlayer()); + + Task.Run(async () => await UpdatePlayersFromService(_onlineCachedPlayers + .Where(p => p.PlayerCharacter != null && p.IsVisible && !p.WasVisible) + .ToDictionary(k => k.PlayerNameHash, k => (int)k.PlayerCharacter!.ClassJob.Id))); + + _lastPlayerObjectCheck = DateTime.Now; + } + + private CachedPlayer CreateCachedPlayer(string hashedName) + { + return new CachedPlayer(hashedName, _ipcManager, _apiController, _dalamudUtil, _watcher); + } +} \ No newline at end of file diff --git a/MareSynchronos/Managers/CharacterCacheManager.cs b/MareSynchronos/Managers/CharacterCacheManager.cs deleted file mode 100644 index 09dc764..0000000 --- a/MareSynchronos/Managers/CharacterCacheManager.cs +++ /dev/null @@ -1,354 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Logging; -using MareSynchronos.API; -using MareSynchronos.FileCacheDB; -using MareSynchronos.Models; -using MareSynchronos.Utils; -using MareSynchronos.WebAPI; -using Penumbra.PlayerWatch; - -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 IPlayerWatcher _watcher; - private readonly ObjectTable _objectTable; - private readonly List _onlineCachedPlayers = new(); - private readonly List _localVisiblePlayers = new(); - private DateTime _lastPlayerObjectCheck = DateTime.Now; - - public CharacterCacheManager(ClientState clientState, Framework framework, ObjectTable objectTable, ApiController apiController, DalamudUtil dalamudUtil, IpcManager ipcManager, IPlayerWatcher watcher) - { - Logger.Debug("Creating " + nameof(CharacterCacheManager)); - - _clientState = clientState; - _framework = framework; - _objectTable = objectTable; - _apiController = apiController; - _dalamudUtil = dalamudUtil; - _ipcManager = ipcManager; - _watcher = watcher; - - _clientState.Login += ClientStateOnLogin; - _clientState.Logout += ClientStateOnLogout; - - _apiController.CharacterReceived += ApiControllerOnCharacterReceived; - _apiController.PairedClientOnline += ApiControllerOnPairedClientOnline; - _apiController.PairedClientOffline += ApiControllerOnPairedClientOffline; - _apiController.PairedWithOther += ApiControllerOnPairedWithOther; - _apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther; - _apiController.Disconnected += ApiControllerOnDisconnected; - _ipcManager.PenumbraDisposed += IpcManagerOnPenumbraDisposed; - _ipcManager.PenumbraRedrawEvent += IpcManagerOnPenumbraRedrawEvent; - _watcher.PlayerChanged += WatcherOnPlayerChanged; - - if (clientState.IsLoggedIn) - { - ClientStateOnLogin(null, EventArgs.Empty); - } - } - - private void IpcManagerOnPenumbraRedrawEvent(object? objectTableIndex, EventArgs e) - { - var objTableObj = _objectTable[(int)objectTableIndex!]; - if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return; - string objTableObjName = objTableObj.Name.ToString(); - var cachedPlayer = _onlineCachedPlayers.SingleOrDefault(p => p.PlayerName == objTableObjName); - if (cachedPlayer != null) - { - Task.Run(() => - { - cachedPlayer.PlayerCharacter = (PlayerCharacter)objTableObj; - _dalamudUtil.WaitWhileCharacterIsDrawing(cachedPlayer.PlayerCharacter.Address); - - cachedPlayer.RequestedRedraws--; - Logger.Warn( - $"Penumbra Redraw for {cachedPlayer.PlayerName}: RequestedRedraws now {cachedPlayer.RequestedRedraws}"); - }); - } - } - - private void WatcherOnPlayerChanged(Character actor) - { - var cachedChar = _onlineCachedPlayers.SingleOrDefault(p => p.PlayerName == actor.Name.ToString()); - if (cachedChar == null) return; - Logger.Warn($"Player {cachedChar.PlayerName} changed, RequestedRedraws {cachedChar.RequestedRedraws}"); - if (cachedChar.RequestedRedraws == 0) - { - Logger.Warn($"Saving new Glamourer data for job " + cachedChar.JobId); - cachedChar.LastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(cachedChar.PlayerName!); - } - } - - private void IpcManagerOnPenumbraDisposed(object? sender, EventArgs e) - { - foreach (var character in _onlineCachedPlayers.ToList()) - { - RestoreCharacter(character); - if (!string.IsNullOrEmpty(character.PlayerName)) - _watcher.RemovePlayerFromWatch(character.PlayerName); - character.IsVisible = false; - } - } - - private void ApiControllerOnDisconnected(object? sender, EventArgs e) - { - RestoreAllCharacters(); - } - - private void ClientStateOnLogin(object? sender, EventArgs e) - { - _framework.Update += FrameworkOnUpdate; - } - - private void ClientStateOnLogout(object? sender, EventArgs e) - { - _framework.Update -= FrameworkOnUpdate; - } - - public void AddInitialPairs(List apiTaskResult) - { - _onlineCachedPlayers.Clear(); - _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; - _ipcManager.PenumbraDisposed -= ApiControllerOnDisconnected; - _framework.Update -= FrameworkOnUpdate; - _clientState.Login -= ClientStateOnLogin; - _clientState.Logout -= ClientStateOnLogout; - _watcher.PlayerChanged -= WatcherOnPlayerChanged; - RestoreAllCharacters(); - } - - private void RestoreAllCharacters() - { - foreach (var character in _onlineCachedPlayers.ToList()) - { - RestoreCharacter(character); - if (!string.IsNullOrEmpty(character.PlayerName)) - _watcher.RemovePlayerFromWatch(character.PlayerName); - } - - _onlineCachedPlayers.Clear(); - } - - public async Task UpdatePlayersFromService(Dictionary 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; - } - - var cachedPlayer = _onlineCachedPlayers.Single(p => p.PlayerNameHash == e.CharacterNameHash); - - cachedPlayer.CharacterCache[e.CharacterData.JobId] = e.CharacterData; - - List 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 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); - cachedPlayer.RequestedRedraws++; - Logger.Warn( - $"Request Redraw for {cachedPlayer.PlayerName}: RequestedRedraws now {cachedPlayer.RequestedRedraws}"); - _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, e.CharacterData.ManipulationData); - _ipcManager.GlamourerRevertCharacterCustomization(otherPlayerName); - _ipcManager.GlamourerApplyAll(e.CharacterData.GlamourerData, otherPlayerName); - }); - } - - private void ApiControllerOnPairedClientOffline(object? sender, EventArgs e) - { - Logger.Debug("Player offline: " + sender!); - RestoreCharacter(_onlineCachedPlayers.SingleOrDefault(f => f.PlayerNameHash == (string)sender!)); - _onlineCachedPlayers.RemoveAll(p => p.PlayerNameHash == ((string)sender!)); - } - - private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e) - { - Logger.Debug("Player online: " + sender!); - if (_onlineCachedPlayers.Any(c => c.PlayerNameHash == (string)sender!)) return; - _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 { { 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(); - if (!_ipcManager.Initialized) return; - 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); - _watcher.RemovePlayerFromWatch(item.PlayerName!); - RestoreCharacter(item); - } - - var newVisiblePlayers = _onlineCachedPlayers.Where(p => p.IsVisible && !p.WasVisible && p.PlayerCharacter != null).ToList(); - if (newVisiblePlayers.Any()) - { - foreach (var player in newVisiblePlayers) - { - Logger.Debug("New watched player, adding to watch and getting glamourer data: " + player.PlayerName); - _watcher.AddPlayerToWatch(player.PlayerName!); - player.OriginalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(player.PlayerName!); - } - - 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 (character == null || string.IsNullOrEmpty(character.PlayerName)) return; - try - { - Logger.Debug("Restoring state for " + character.PlayerName); - _ipcManager.PenumbraRemoveTemporaryCollection(character.PlayerName); - _ipcManager.GlamourerRevertCharacterCustomization(character.PlayerName); - _ipcManager.GlamourerApplyOnlyCustomization(character.OriginalGlamourerData, character.PlayerName); - _ipcManager.GlamourerApplyOnlyEquipment(character.LastGlamourerData, character.PlayerName); - } - catch (Exception ex) - { - Logger.Warn(ex.Message + Environment.NewLine + ex.StackTrace); - } - - character.Reset(); - } -} \ No newline at end of file diff --git a/MareSynchronos/Managers/CharacterManager.cs b/MareSynchronos/Managers/PlayerManager.cs similarity index 89% rename from MareSynchronos/Managers/CharacterManager.cs rename to MareSynchronos/Managers/PlayerManager.cs index 3adda38..62fe914 100644 --- a/MareSynchronos/Managers/CharacterManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -14,10 +14,10 @@ using System.Threading.Tasks; namespace MareSynchronos.Managers { - public class CharacterManager : IDisposable + public class PlayerManager : IDisposable { private readonly ApiController _apiController; - private readonly CharacterCacheManager _characterCacheManager; + private readonly CachedPlayersManager _cachedPlayersManager; private readonly CharacterDataFactory _characterDataFactory; private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; @@ -26,23 +26,23 @@ namespace MareSynchronos.Managers private string _lastSentHash = string.Empty; private Task? _playerChangedTask; - public CharacterManager(ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, - CharacterDataFactory characterDataFactory, CharacterCacheManager characterCacheManager, DalamudUtil dalamudUtil, IPlayerWatcher watcher) + public PlayerManager(ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, + CharacterDataFactory characterDataFactory, CachedPlayersManager cachedPlayersManager, DalamudUtil dalamudUtil, IPlayerWatcher watcher) { - Logger.Debug("Creating " + nameof(CharacterManager)); + Logger.Debug("Creating " + nameof(PlayerManager)); _apiController = apiController; _objectTable = objectTable; _ipcManager = ipcManager; _characterDataFactory = characterDataFactory; - _characterCacheManager = characterCacheManager; + _cachedPlayersManager = cachedPlayersManager; _dalamudUtil = dalamudUtil; _watcher = watcher; } public void Dispose() { - Logger.Debug("Disposing " + nameof(CharacterManager)); + Logger.Debug("Disposing " + nameof(PlayerManager)); _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; _apiController.Connected -= ApiController_Connected; @@ -73,7 +73,7 @@ namespace MareSynchronos.Managers Task.WaitAll(apiTask); - _characterCacheManager.AddInitialPairs(apiTask.Result); + _cachedPlayersManager.AddInitialPairs(apiTask.Result); _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; } diff --git a/MareSynchronos/Models/CachedPlayer.cs b/MareSynchronos/Models/CachedPlayer.cs index 14b7916..3a239fc 100644 --- a/MareSynchronos/Models/CachedPlayer.cs +++ b/MareSynchronos/Models/CachedPlayer.cs @@ -1,19 +1,36 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; using MareSynchronos.API; +using MareSynchronos.FileCacheDB; +using MareSynchronos.Managers; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Penumbra.PlayerWatch; namespace MareSynchronos.Models; public class CachedPlayer { - private bool _isVisible = false; + private readonly DalamudUtil _dalamudUtil; + private readonly IpcManager _ipcManager; + private readonly ApiController _apiController; + private readonly IPlayerWatcher _watcher; + private bool _isVisible; - public CachedPlayer(string nameHash) + public CachedPlayer(string nameHash, IpcManager ipcManager, ApiController apiController, DalamudUtil dalamudUtil, IPlayerWatcher watcher) { PlayerNameHash = nameHash; + _ipcManager = ipcManager; + _apiController = apiController; + _dalamudUtil = dalamudUtil; + _watcher = watcher; } - public Dictionary CharacterCache { get; set; } = new(); public bool IsVisible { get => _isVisible; @@ -21,31 +38,180 @@ public class CachedPlayer { WasVisible = _isVisible; _isVisible = value; + } } - public string OriginalGlamourerData { get; set; } - public string LastGlamourerData { get; set; } - public int? JobId => (int?)PlayerCharacter?.ClassJob.Id; + private string _lastGlamourerData = string.Empty; + + private string _originalGlamourerData = string.Empty; + public PlayerCharacter? PlayerCharacter { get; set; } - public string? PlayerName { get; set; } + + public string? PlayerName { get; private set; } + public string PlayerNameHash { get; } + + public bool RequestedPenumbraRedraw { get; set; } + public bool WasVisible { get; private set; } - public int RequestedRedraws + + private readonly Dictionary _cache = new(); + + private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) { - get => _requestedRedraws; - set => _requestedRedraws = value < 0 ? 0 : value; + if (string.IsNullOrEmpty(PlayerName) || e.CharacterNameHash != PlayerNameHash) return; + Logger.Debug("Received data for " + this); + + List toDownloadReplacements; + using (var db = new FileCacheContext()) + { + Logger.Debug("Checking for files to download for player " + PlayerName); + Logger.Debug("Hash for data is " + e.CharacterData.Hash); + if (!_cache.ContainsKey(e.CharacterData.Hash)) + { + Logger.Debug("Received total " + e.CharacterData.FileReplacements.Count + " file replacement data"); + _cache[e.CharacterData.Hash] = e.CharacterData; + } + else + { + Logger.Debug("Had valid local cache for " + PlayerName); + } + } + + // todo: make this cancellable + Task.Run(async () => + { + Dictionary moddedPaths; + while ((toDownloadReplacements = TryCalculateModdedDictionary(_cache[e.CharacterData.Hash], out moddedPaths)).Count > 0) + { + Logger.Debug("Downloading missing files for player " + PlayerName); + await _apiController.DownloadFiles(toDownloadReplacements); + } + + ApplyCharacterData(e.CharacterData, moddedPaths); + }); } - private int _requestedRedraws; - public void Reset() + private List TryCalculateModdedDictionary(CharacterCacheDto cache, + out Dictionary moddedDictionary) { - PlayerName = string.Empty; - PlayerCharacter = null; + List missingFiles = new(); + moddedDictionary = new Dictionary(); + try + { + using var db = new FileCacheContext(); + foreach (var item in cache.FileReplacements) + { + foreach (var gamePath in item.GamePaths) + { + var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash); + if (fileCache != null) + { + moddedDictionary.Add(gamePath, fileCache.Filepath); + } + else + { + missingFiles.Add(item); + } + } + } + } + catch (Exception ex) + { + PluginLog.Error(ex, "Something went wrong during calculation replacements"); + } + Logger.Debug("ModdedPaths calculated, missing files: " + missingFiles.Count); + return missingFiles; + } + + private void ApplyCharacterData(CharacterCacheDto cache, Dictionary moddedPaths) + { + _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName!); + var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(PlayerName!); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter!.Address); + RequestedPenumbraRedraw = true; + Logger.Warn( + $"Request Redraw for {PlayerName}: RequestedRedraws now {RequestedPenumbraRedraw}"); + _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, cache.ManipulationData); + _ipcManager.GlamourerRevertCharacterCustomization(PlayerName!); + _ipcManager.GlamourerApplyAll(cache.GlamourerData, PlayerName!); + } + + public void DisposePlayer() + { + Logger.Debug("Disposing " + PlayerNameHash); + if (string.IsNullOrEmpty(PlayerName)) return; + try + { + Logger.Debug("Restoring state for " + PlayerName); + IsVisible = false; + _watcher.RemovePlayerFromWatch(PlayerName); + _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); + _ipcManager.GlamourerRevertCharacterCustomization(PlayerName); + _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerName); + _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerName); + } + catch (Exception ex) + { + Logger.Warn(ex.Message + Environment.NewLine + ex.StackTrace); + } + finally + { + _watcher.PlayerChanged -= WatcherOnPlayerChanged; + _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; + _apiController.CharacterReceived -= ApiControllerOnCharacterReceived; + PlayerName = string.Empty; + PlayerCharacter = null; + } + } + + public void InitializePlayer(PlayerCharacter character) + { + IsVisible = true; + PlayerName = character.Name.ToString(); + PlayerCharacter = character; + Logger.Debug("Initializing Player " + this); + _watcher.AddPlayerToWatch(PlayerName!); + _watcher.PlayerChanged += WatcherOnPlayerChanged; + _ipcManager.PenumbraRedrawEvent += IpcManagerOnPenumbraRedrawEvent; + _apiController.CharacterReceived += ApiControllerOnCharacterReceived; + _originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerName); } public override string ToString() { - return PlayerNameHash + " : " + PlayerName + " : HasChar " + (PlayerCharacter != null); + return PlayerNameHash + ":" + PlayerName + ":HasChar " + (PlayerCharacter != null); + } + + private void IpcManagerOnPenumbraRedrawEvent(object? sender, EventArgs e) + { + var player = _dalamudUtil.GetPlayerCharacterFromObjectTableIndex((int)sender!); + if (player == null || player.Name.ToString() != PlayerName) return; + Task.Run(() => + { + PlayerCharacter = player; + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter.Address); + + RequestedPenumbraRedraw = false; + Logger.Debug( + $"Penumbra Redraw for {PlayerName}: RequestedRedraws now {RequestedPenumbraRedraw}"); + }); + } + + private void WatcherOnPlayerChanged(Character actor) + { + if (actor.Name.ToString() != PlayerName) return; + Logger.Debug($"Player {PlayerName} changed, RequestedRedraws {RequestedPenumbraRedraw}"); + PlayerCharacter = _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName!); + if (PlayerCharacter is null) + { + Logger.Debug($"Invalid PlayerCharacter for {PlayerName}"); + } + else if (!RequestedPenumbraRedraw && PlayerCharacter is not null) + { + Logger.Debug($"Saving new Glamourer data"); + _lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerName!); + } } } \ No newline at end of file diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 640cdd9..0ba1de2 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -31,9 +31,9 @@ namespace MareSynchronos private readonly DalamudPluginInterface _pluginInterface; private readonly PluginUi _pluginUi; private readonly WindowSystem _windowSystem; - private CharacterManager? _characterManager; + private PlayerManager? _characterManager; private readonly DalamudUtil _dalamudUtil; - private CharacterCacheManager? _characterCacheManager; + private CachedPlayersManager? _characterCacheManager; private readonly IPlayerWatcher _playerWatcher; private readonly DownloadUi _downloadUi; @@ -162,9 +162,9 @@ namespace MareSynchronos { var characterCacheFactory = new CharacterDataFactory(_dalamudUtil, _ipcManager); - _characterCacheManager = new CharacterCacheManager(_clientState, _framework, _objectTable, + _characterCacheManager = new CachedPlayersManager(_clientState, _framework, _objectTable, _apiController, _dalamudUtil, _ipcManager, _playerWatcher); - _characterManager = new CharacterManager(_apiController, _objectTable, _ipcManager, + _characterManager = new PlayerManager(_apiController, _objectTable, _ipcManager, characterCacheFactory, _characterCacheManager, _dalamudUtil, _playerWatcher); _characterManager.StartWatchingPlayer(); } diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index 9eda827..e45bbae 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -21,7 +21,7 @@ namespace MareSynchronos.Utils public bool IsPlayerPresent => _clientState.LocalPlayer != null; - public string PlayerName => _clientState.LocalPlayer!.Name.ToString(); + public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; public int PlayerJobId => (int)_clientState.LocalPlayer!.ClassJob.Id; @@ -49,6 +49,24 @@ namespace MareSynchronos.Utils return allLocalPlayers; } + public PlayerCharacter? GetPlayerCharacterFromObjectTableIndex(int index) + { + var objTableObj = _objectTable[index]; + if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; + return (PlayerCharacter)objTableObj; + } + + public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) + { + foreach (var item in _objectTable) + { + if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + if (item.Name.ToString() == characterName) return (PlayerCharacter)item; + } + + return null; + } + public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress) { if (!_clientState.IsLoggedIn) return;