From 0fe3f1cf2552b0ecd2d48a8407b760a6d030089c Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Thu, 23 Jun 2022 21:29:52 +0200 Subject: [PATCH] somewhat working integration of penumbra --- .../Factories/CharacterCacheFactory.cs | 106 ++++++++ MareSynchronos/Managers/CharacterManager.cs | 251 ++++++------------ MareSynchronos/Managers/IpcManager.cs | 45 +++- MareSynchronos/MareSynchronos.json | 2 +- MareSynchronos/Models/CharacterCache.cs | 11 +- MareSynchronos/Models/FileReplacement.cs | 1 - MareSynchronos/Plugin.cs | 152 ++--------- MareSynchronos/UI/IntroUI.cs | 6 +- MareSynchronos/UI/PluginUI.cs | 3 +- MareSynchronos/WebAPI/ApiController.cs | 25 +- 10 files changed, 275 insertions(+), 327 deletions(-) create mode 100644 MareSynchronos/Factories/CharacterCacheFactory.cs diff --git a/MareSynchronos/Factories/CharacterCacheFactory.cs b/MareSynchronos/Factories/CharacterCacheFactory.cs new file mode 100644 index 0000000..9e618a4 --- /dev/null +++ b/MareSynchronos/Factories/CharacterCacheFactory.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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 Penumbra.GameData.ByteString; +using Penumbra.Interop.Structs; + +namespace MareSynchronos.Factories +{ + public class CharacterCacheFactory + { + private readonly ClientState _clientState; + private readonly IpcManager _ipcManager; + private readonly FileReplacementFactory _factory; + + public CharacterCacheFactory(ClientState clientState, IpcManager ipcManager, FileReplacementFactory factory) + { + _clientState = clientState; + _ipcManager = ipcManager; + _factory = factory; + } + + private string GetPlayerName() + { + return _clientState.LocalPlayer!.Name.ToString(); + } + + public unsafe CharacterCache BuildCharacterCache() + { + var cache = new CharacterCache(); + + while (_clientState.LocalPlayer == null) + { + PluginLog.Debug("Character is null but it shouldn't be, waiting"); + Thread.Sleep(50); + } + var model = (CharacterBase*)((Character*)_clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); + for (var idx = 0; idx < model->SlotCount; ++idx) + { + var mdl = (RenderModel*)model->ModelArray[idx]; + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + continue; + } + + var mdlPath = new Utf8String(mdl->ResourceHandle->FileName()).ToString(); + + FileReplacement cachedMdlResource = _factory.Create(); + cachedMdlResource.GamePaths = _ipcManager.PenumbraReverseResolvePath(mdlPath, GetPlayerName()); + cachedMdlResource.SetResolvedPath(mdlPath); + //PluginLog.Verbose("Resolving for model " + mdlPath); + + cache.AddAssociatedResource(cachedMdlResource, null!, null!); + + for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) + { + var mtrl = (Material*)mdl->Materials[mtrlIdx]; + if (mtrl == null) continue; + + //var mtrlFileResource = factory.Create(); + var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; + //PluginLog.Verbose("Resolving for material " + mtrlPath); + var cachedMtrlResource = _factory.Create(); + cachedMtrlResource.GamePaths = _ipcManager.PenumbraReverseResolvePath(mtrlPath, GetPlayerName()); + cachedMtrlResource.SetResolvedPath(mtrlPath); + cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!); + + var mtrlResource = (MtrlResource*)mtrl->ResourceHandle; + for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++) + { + var texPath = new Utf8String(mtrlResource->TexString(resIdx)).ToString(); + + if (string.IsNullOrEmpty(texPath.ToString())) continue; + + var cachedTexResource = _factory.Create(); + cachedTexResource.GamePaths = new[] { texPath }; + cachedTexResource.SetResolvedPath(_ipcManager.PenumbraResolvePath(texPath, GetPlayerName())!); + if (!cachedTexResource.HasFileReplacement) + { + // try resolving tex with -- in name instead + texPath = texPath.Insert(texPath.LastIndexOf('/') + 1, "--"); + var reResolvedPath = _ipcManager.PenumbraResolvePath(texPath, GetPlayerName())!; + if (reResolvedPath != texPath) + { + cachedTexResource.GamePaths = new[] { texPath }; + cachedTexResource.SetResolvedPath(reResolvedPath); + } + } + cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource); + } + } + } + + return cache; + } + } +} diff --git a/MareSynchronos/Managers/CharacterManager.cs b/MareSynchronos/Managers/CharacterManager.cs index 7f75f5f..f746381 100644 --- a/MareSynchronos/Managers/CharacterManager.cs +++ b/MareSynchronos/Managers/CharacterManager.cs @@ -2,144 +2,67 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Logging; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.Factories; using MareSynchronos.Models; using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Interop.Structs; using Penumbra.PlayerWatch; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Dalamud.Configuration; 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? CharacterCache { get; set; } + public PlayerCharacter? PlayerCharacter { get; set; } + } + public class CharacterManager : IDisposable { private readonly ApiController _apiController; readonly Dictionary _cachedLocalPlayers = new(); private readonly Dictionary<(string, int), CharacterCacheDto> _characterCache = new(); private readonly ClientState _clientState; - private readonly FileReplacementFactory _factory; private readonly Framework _framework; 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 HashSet _onlinePairedUsers = new(); + private List _onlineCachedPlayers = new(); - public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, FileReplacementFactory factory, - Configuration pluginConfiguration) + private Dictionary _onlinePairedUsers = new(); + + public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, + Configuration pluginConfiguration, CharacterCacheFactory characterCacheFactory) { this._clientState = clientState; this._framework = framework; this._apiController = apiController; this._objectTable = objectTable; this._ipcManager = ipcManager; - this._factory = factory; _pluginConfiguration = pluginConfiguration; + _characterCacheFactory = characterCacheFactory; _watcher = PlayerWatchFactory.Create(framework, clientState, objectTable); } - public unsafe CharacterCache BuildCharacterCache() - { - var cache = new CharacterCache(); - - while (_clientState.LocalPlayer == null) - { - PluginLog.Debug("Character is null but it shouldn't be, waiting"); - Thread.Sleep(50); - } - var model = (CharacterBase*)((Character*)_clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); - for (var idx = 0; idx < model->SlotCount; ++idx) - { - var mdl = (RenderModel*)model->ModelArray[idx]; - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - continue; - } - - var mdlPath = new Utf8String(mdl->ResourceHandle->FileName()).ToString(); - - FileReplacement cachedMdlResource = _factory.Create(); - cachedMdlResource.GamePaths = _ipcManager.PenumbraReverseResolvePath(mdlPath, GetPlayerName()); - cachedMdlResource.SetResolvedPath(mdlPath); - PluginLog.Verbose("Resolving for model " + mdlPath); - - cache.AddAssociatedResource(cachedMdlResource, null!, null!); - - var imc = (ResourceHandle*)model->IMCArray[idx]; - if (imc != null) - { - byte[] imcData = new byte[imc->Data->DataLength / sizeof(long)]; - Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength / sizeof(long)); - string imcDataStr = BitConverter.ToString(imcData).Replace("-", ""); - cachedMdlResource.ImcData = imcDataStr; - } - cache.AddAssociatedResource(cachedMdlResource, null!, null!); - - for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)mdl->Materials[mtrlIdx]; - if (mtrl == null) continue; - - //var mtrlFileResource = factory.Create(); - var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; - PluginLog.Verbose("Resolving for material " + mtrlPath); - var cachedMtrlResource = _factory.Create(); - cachedMtrlResource.GamePaths = _ipcManager.PenumbraReverseResolvePath(mtrlPath, GetPlayerName()); - cachedMtrlResource.SetResolvedPath(mtrlPath); - cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!); - - var mtrlResource = (MtrlResource*)mtrl->ResourceHandle; - for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++) - { - var texPath = new Utf8String(mtrlResource->TexString(resIdx)).ToString(); - - if (string.IsNullOrEmpty(texPath.ToString())) continue; - PluginLog.Verbose("Resolving for texture " + texPath); - - var cachedTexResource = _factory.Create(); - cachedTexResource.GamePaths = new[] { texPath }; - cachedTexResource.SetResolvedPath(_ipcManager.PenumbraResolvePath(texPath, GetPlayerName())!); - cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource); - } - } - } - - return cache; - } - - public async Task DebugJson() - { - var cache = CreateFullCharacterCache(); - while (!cache.IsCompleted) - { - await Task.Delay(50); - } - - PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented)); - } - public void Dispose() { _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; @@ -161,15 +84,10 @@ namespace MareSynchronos.Managers } } - public void StopWatchPlayer(string name) - { - _watcher.RemovePlayerFromWatch(name); - } - public async Task UpdatePlayersFromService(Dictionary currentLocalPlayers) { PluginLog.Debug("Updating local players from service"); - currentLocalPlayers = currentLocalPlayers.Where(k => _onlinePairedUsers.Contains(k.Key)) + currentLocalPlayers = currentLocalPlayers.Where(k => _onlinePairedUsers.ContainsKey(k.Key)) .ToDictionary(k => k.Key, k => k.Value); await _apiController.GetCharacterData(currentLocalPlayers .ToDictionary( @@ -177,11 +95,6 @@ namespace MareSynchronos.Managers k => (int)k.Value.ClassJob.Id)); } - public void WatchPlayer(string name) - { - _watcher.AddPlayerToWatch(name); - } - internal void StartWatchingPlayer() { _watcher.AddPlayerToWatch(GetPlayerName()); @@ -206,11 +119,12 @@ namespace MareSynchronos.Managers { PluginLog.Debug(nameof(ApiController_Connected)); PluginLog.Debug("MyHashedName:" + Crypto.GetHash256(GetPlayerName() + _clientState.LocalPlayer!.HomeWorld.Id)); + _lastSentHash = string.Empty; var apiTask = _apiController.SendCharacterName(Crypto.GetHash256(GetPlayerName() + _clientState.LocalPlayer!.HomeWorld.Id)); Task.WaitAll(apiTask); - _onlinePairedUsers = new HashSet(apiTask.Result); + _onlinePairedUsers = apiTask.Result.ToDictionary(k => k, k => string.Empty); var assignTask = AssignLocalPlayersData(); Task.WaitAll(assignTask); PluginLog.Debug("Online and paired users: " + string.Join(",", _onlinePairedUsers)); @@ -230,6 +144,7 @@ namespace MareSynchronos.Managers { RestoreCharacter(character); } + _onlinePairedUsers.Clear(); _lastSentHash = string.Empty; } @@ -248,35 +163,21 @@ namespace MareSynchronos.Managers private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) { - PlayerCharacter? playerObject = null; PluginLog.Debug("Received hash for " + e.CharacterNameHash); - 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; - playerObject = (PlayerCharacter)obj; - var hashedName = Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString()); - if (e.CharacterNameHash == hashedName) - { - break; - } + string otherPlayerName; - playerObject = null; - } - - if (playerObject == null) + var localPlayers = GetLocalPlayers(); + if (localPlayers.ContainsKey(e.CharacterNameHash)) { - PluginLog.Debug("Found no suitable hash for " + e.CharacterNameHash); - return; + _onlinePairedUsers[e.CharacterNameHash] = localPlayers[e.CharacterNameHash].Name.ToString(); + otherPlayerName = _onlinePairedUsers[e.CharacterNameHash]; } else { - PluginLog.Debug("Found suitable player for hash: " + playerObject.Name.ToString()); + PluginLog.Debug("Found no local player for " + e.CharacterNameHash); + return; } - var otherPlayerName = playerObject.Name.ToString(); - _characterCache[(e.CharacterNameHash, e.CharacterData.JobId)] = e.CharacterData; List toDownloadReplacements; @@ -291,6 +192,7 @@ namespace MareSynchronos.Managers } PluginLog.Debug("Downloading missing files for player " + otherPlayerName); + // todo: make this cancellable var downloadTask = _apiController.DownloadFiles(toDownloadReplacements, _pluginConfiguration.CacheFolder); while (!downloadTask.IsCompleted) { @@ -299,10 +201,11 @@ namespace MareSynchronos.Managers PluginLog.Debug("Assigned hash to visible player: " + otherPlayerName); _ipcManager.PenumbraRemoveTemporaryCollection(otherPlayerName); - _ipcManager.PenumbraCreateTemporaryCollection(otherPlayerName); + var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(otherPlayerName); Dictionary moddedPaths = new(); - using (var db = new FileCacheContext()) + try { + using var db = new FileCacheContext(); foreach (var item in e.CharacterData.FileReplacements) { foreach (var gamePath in item.GamePaths) @@ -310,44 +213,42 @@ namespace MareSynchronos.Managers var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash); if (fileCache != null) { - PluginLog.Debug("Modifying: " + gamePath + " => " + fileCache.Filepath); moddedPaths.Add(gamePath, fileCache.Filepath); } } } } + catch (Exception ex) + { + PluginLog.Error(ex, "Something went wrong during calculation replacements"); + } - _ipcManager.PenumbraSetTemporaryMods(otherPlayerName, moddedPaths); + WaitWhileCharacterIsDrawing(localPlayers[e.CharacterNameHash].Address); + + _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, e.CharacterData.ManipulationData); _ipcManager.GlamourerApplyCharacterCustomization(e.CharacterData.GlamourerData, otherPlayerName); - _ipcManager.PenumbraRedraw(otherPlayerName); } private void ApiControllerOnUnpairedFromOther(object? sender, EventArgs e) { var characterHash = (string?)sender; if (string.IsNullOrEmpty(characterHash)) return; - RestoreCharacter(characterHash); + RestoreCharacter(new KeyValuePair(characterHash, _onlinePairedUsers[characterHash])); } - private void RestoreCharacter(string characterHash) + private void RestoreCharacter(KeyValuePair character) { - var players = GetLocalPlayers(); + if (string.IsNullOrEmpty(character.Value)) return; - foreach (var entry in _characterCache.Where(c => c.Key.Item1 == characterHash)) + foreach (var entry in _characterCache.Where(c => c.Key.Item1 == character.Key)) { _characterCache.Remove(entry.Key); } - foreach (var player in players) - { - if (player.Key != characterHash) continue; - var playerName = player.Value.Name.ToString(); - RestorePreviousCharacter(playerName); - PluginLog.Debug("Removed from pairing, restoring glamourer state for " + playerName); - _ipcManager.PenumbraRemoveTemporaryCollection(playerName); - _ipcManager.GlamourerRevertCharacterCustomization(playerName); - break; - } + 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) @@ -359,7 +260,7 @@ namespace MareSynchronos.Managers private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e) { PluginLog.Debug("Player online: " + sender!); - _onlinePairedUsers.Add((string)sender!); + _onlinePairedUsers.Add((string)sender!, string.Empty); } private async Task AssignLocalPlayersData() @@ -392,8 +293,9 @@ namespace MareSynchronos.Managers private async Task CreateFullCharacterCache() { - var cache = BuildCharacterCache(); - cache.SetGlamourerData(_ipcManager.GlamourerGetCharacterCustomization()!); + 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 () => { @@ -428,8 +330,9 @@ namespace MareSynchronos.Managers var pObj = (PlayerCharacter)obj; var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString()); - if (!_onlinePairedUsers.Contains(hashedName)) continue; + 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(); @@ -493,6 +396,7 @@ namespace MareSynchronos.Managers } } } + private unsafe void PlayerChanged(string name) { //if (sender == null) return; @@ -505,18 +409,7 @@ namespace MareSynchronos.Managers _playerChangedTask = Task.Run(() => { - var obj = (GameObject*)_clientState.LocalPlayer!.Address; - - PluginLog.Debug("Waiting for charater to be drawn"); - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something - { - //PluginLog.Debug("Waiting for character to finish drawing"); - Thread.Sleep(10); - } - PluginLog.Debug("Character finished drawing"); - - // wait half a second just in case - Thread.Sleep(500); + WaitWhileCharacterIsDrawing(_clientState.LocalPlayer!.Address); var characterCacheTask = CreateFullCharacterCache(); Task.WaitAll(characterCacheTask); @@ -532,6 +425,20 @@ namespace MareSynchronos.Managers }); } + 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); @@ -541,14 +448,26 @@ namespace MareSynchronos.Managers private void Watcher_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) { - if (actor.Name.ToString() == _clientState.LocalPlayer!.Name.ToString()) + try { - PluginLog.Debug("Watcher: PlayerChanged"); - PlayerChanged(actor.Name.ToString()); + // fix for redraw from anamnesis + while (_clientState.LocalPlayer == null) + { + Thread.Sleep(100); + } + if (actor.Name.ToString() == _clientState.LocalPlayer!.Name.ToString()) + { + PluginLog.Debug("Watcher: PlayerChanged"); + PlayerChanged(actor.Name.ToString()); + } + else + { + PluginLog.Debug("PlayerChanged: " + actor.Name.ToString()); + } } - else + catch(Exception ex) { - PluginLog.Debug("PlayerChanged: " + actor.Name.ToString()); + PluginLog.Error(ex, "Actor was null or broken " + actor); } } } diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 041c783..08b137a 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -2,9 +2,14 @@ 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; namespace MareSynchronos.Managers { @@ -22,8 +27,8 @@ namespace MareSynchronos.Managers private readonly ICallGateSubscriber? _penumbraRedraw; private readonly ICallGateSubscriber? _penumbraReverseResolvePath; private readonly ICallGateSubscriber _glamourerRevertCustomization; - - private readonly ICallGateSubscriber, List, int, int> + private readonly ICallGateSubscriber _penumbraGetMetaManipulations; + private readonly ICallGateSubscriber, string, int, int> _penumbraSetTemporaryMod; private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; @@ -47,13 +52,15 @@ namespace MareSynchronos.Managers _glamourerApiVersion = _pluginInterface.GetIpcSubscriber("Glamourer.ApiVersion"); _glamourerRevertCustomization = _pluginInterface.GetIpcSubscriber("Glamourer.RevertCharacterCustomization"); _penumbraObjectIsRedrawn = _pluginInterface.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); + _penumbraGetMetaManipulations = + _pluginInterface.GetIpcSubscriber("Penumbra.GetMetaManipulations"); _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); _penumbraInit.Subscribe(RedrawSelf); _penumbraSetTemporaryMod = _pluginInterface - .GetIpcSubscriber, List, int, + .GetIpcSubscriber, string, int, int>("Penumbra.AddTemporaryMod"); _penumbraCreateTemporaryCollection = @@ -109,13 +116,17 @@ namespace MareSynchronos.Managers public string[] PenumbraReverseResolvePath(string path, string characterName) { if (!CheckPenumbraApi()) return new[] { path }; - return _penumbraReverseResolvePath!.InvokeFunc(path, characterName); + 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; - return _penumbraResolvePath!.InvokeFunc(path, characterName); + var resolvedPath = _penumbraResolvePath!.InvokeFunc(path, characterName); + PluginLog.Verbose("Resolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPath)); + return resolvedPath; } public string? PenumbraModDirectory() @@ -133,6 +144,7 @@ namespace MareSynchronos.Managers public void GlamourerApplyCharacterCustomization(string customization, string characterName) { if (!CheckGlamourerApi()) return; + PluginLog.Debug("GlamourerString: " + customization); _glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName); } @@ -148,25 +160,34 @@ namespace MareSynchronos.Managers _penumbraRedraw!.InvokeAction(actorName, 0); } - public void PenumbraCreateTemporaryCollection(string characterName) + public string PenumbraCreateTemporaryCollection(string characterName) { - if (!CheckPenumbraApi()) return; + if (!CheckPenumbraApi()) return string.Empty; PluginLog.Debug("Creating temp collection for " + characterName); - //penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); + 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); + _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); } - public void PenumbraSetTemporaryMods(string characterName, IReadOnlyDictionary modPaths) + public void PenumbraSetTemporaryMods(string collectionName, Dictionary modPaths, string manipulationData) { if (!CheckPenumbraApi()) return; - PluginLog.Debug("Assigning temp mods for " + characterName); - //penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", characterName, modPaths, new List(), 0); + + 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() diff --git a/MareSynchronos/MareSynchronos.json b/MareSynchronos/MareSynchronos.json index c721c6d..717e086 100644 --- a/MareSynchronos/MareSynchronos.json +++ b/MareSynchronos/MareSynchronos.json @@ -1,7 +1,7 @@ { "Author": "darkarchon", "Name": "Mare Synchronos", - "Punchline": "", + "Punchline": "Let others see you as you see yourself.", "Description": "", "InternalName": "mareSynchronos", "ApplicableVersion": "any", diff --git a/MareSynchronos/Models/CharacterCache.cs b/MareSynchronos/Models/CharacterCache.cs index e28729d..1dac9e5 100644 --- a/MareSynchronos/Models/CharacterCache.cs +++ b/MareSynchronos/Models/CharacterCache.cs @@ -19,7 +19,8 @@ namespace MareSynchronos.Models FileReplacements = AllReplacements.Select(f => f.ToFileReplacementDto()).ToList(), GlamourerData = GlamourerString, Hash = CacheHash, - JobId = (int)JobId + JobId = (int)JobId, + ManipulationData = ManipulationString }; } @@ -34,13 +35,15 @@ namespace MareSynchronos.Models public List FileReplacements { get; set; } = new List(); [JsonProperty] - public string GlamourerString { get; private set; } = string.Empty; + public string GlamourerString { get; set; } = string.Empty; public bool IsReady => FileReplacements.All(f => f.Computed); [JsonProperty] public string CacheHash { get; set; } = string.Empty; + public string ManipulationString { get; set; } = string.Empty; + [JsonProperty] public uint JobId { get; set; } = 0; public void AddAssociatedResource(FileReplacement resource, FileReplacement? mdlParent, FileReplacement? mtrlParent) @@ -93,10 +96,6 @@ namespace MareSynchronos.Models } } - public void SetGlamourerData(string glamourerString) - { - GlamourerString = glamourerString; - } public override string ToString() { StringBuilder stringBuilder = new(); diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index 4f0d809..fc84f13 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -21,7 +21,6 @@ namespace MareSynchronos.Models { GamePaths = GamePaths, Hash = Hash, - ImcData = ImcData }; } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 43899b5..79c5806 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -39,6 +39,7 @@ namespace MareSynchronos private readonly PluginUi _pluginUi; private readonly WindowSystem _windowSystem; private CharacterManager? _characterManager; + public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, Framework framework, ObjectTable objectTable, ClientState clientState) { @@ -59,14 +60,13 @@ namespace MareSynchronos var uiSharedComponent = new UIShared(_ipcManager, _apiController, _fileCacheManager, _configuration); - // you might normally want to embed resources and load them from the manifest stream _pluginUi = new PluginUi(_windowSystem, uiSharedComponent, _configuration, _apiController); _introUi = new IntroUI(_windowSystem, uiSharedComponent, _configuration, _fileCacheManager); _introUi.FinishedRegistration += (_, _) => { + _introUi.IsOpen = false; _pluginUi.IsOpen = true; - _introUi?.Dispose(); - ClientState_Login(null, EventArgs.Empty); + ReLaunchCharacterManager(); }; new FileCacheContext().Dispose(); // make sure db is initialized I guess @@ -96,41 +96,25 @@ namespace MareSynchronos _apiController?.Dispose(); } + private void ClientState_Login(object? sender, EventArgs e) { PluginLog.Debug("Client login"); _pluginInterface.UiBuilder.Draw += Draw; + _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; + _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the Mare Synchronos UI" + }); if (!_configuration.HasValidSetup) { _introUi.IsOpen = true; return; } - else - { - _introUi.IsOpen = false; - } - Task.Run(async () => - { - while (_clientState.LocalPlayer == null) - { - await Task.Delay(50); - } - - _characterManager = new CharacterManager( - _clientState, _framework, _apiController, _objectTable, _ipcManager, new FileReplacementFactory(_ipcManager), _configuration); - _characterManager.StartWatchingPlayer(); - _ipcManager.PenumbraRedraw(_clientState.LocalPlayer!.Name.ToString()); - }); - - _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; - - _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) - { - HelpMessage = "Opens the Mare Synchronos UI" - }); + ReLaunchCharacterManager(); } private void ClientState_Logout(object? sender, EventArgs e) @@ -142,47 +126,26 @@ namespace MareSynchronos _commandManager.RemoveHandler(CommandName); } - private void CopyFile(FileReplacement replacement, string targetDirectory, Dictionary? resourceDict = null) + public void ReLaunchCharacterManager() { - if (replacement.HasFileReplacement) + _characterManager?.Dispose(); + + Task.Run(async () => { - PluginLog.Debug("Copying file \"" + replacement.ResolvedPath + "\""); - var db1 = new FileCacheContext(); - var fileCache = db1.FileCaches.Single(f => f.Filepath.Contains(replacement.ResolvedPath.Replace('/', '\\'))); - db1.Dispose(); - try + while (_clientState.LocalPlayer == null) { - var ext = new FileInfo(fileCache.Filepath).Extension; - var newFilePath = Path.Combine(targetDirectory, "files", fileCache.Hash.ToLower() + ext); - string lc4HcPath = Path.Combine(targetDirectory, "files", "lz4hc." + fileCache.Hash.ToLower() + ext); - if (!File.Exists(lc4HcPath)) - { + await Task.Delay(50); + } - Stopwatch st = Stopwatch.StartNew(); - File.WriteAllBytes(lc4HcPath, LZ4Codec.WrapHC(File.ReadAllBytes(fileCache.Filepath), 0, (int)new FileInfo(fileCache.Filepath).Length)); - st.Stop(); - PluginLog.Debug("Compressed " + new FileInfo(fileCache.Filepath).Length + " bytes to " + new FileInfo(lc4HcPath).Length + " bytes in " + st.Elapsed); - File.Copy(fileCache.Filepath, newFilePath); - if (resourceDict != null) - { - foreach (var path in replacement.GamePaths) - { - resourceDict[path] = $"files\\{fileCache.Hash.ToLower() + ext}"; - } - } - else - { - //File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," }); - } - } - } - catch (Exception ex) - { - PluginLog.Error(ex, "error during copy"); - } - } + 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()); + }); } - + private void Draw() { _windowSystem.Draw(); @@ -190,66 +153,6 @@ namespace MareSynchronos private void OnCommand(string command, string args) { - if (args == "printjson") - { - _ = _characterManager?.DebugJson(); - } - - if (args.StartsWith("watch")) - { - var playerName = args.Replace("watch", "").Trim(); - _characterManager!.WatchPlayer(playerName); - } - - if (args.StartsWith("stop")) - { - var playerName = args.Replace("watch", "").Trim(); - _characterManager!.StopWatchPlayer(playerName); - } - - if (args == "createtestmod") - { - Task.Run(() => - { - var playerName = _clientState.LocalPlayer!.Name.ToString(); - var modName = $"Mare Synchronos Test Mod {playerName}"; - var modDirectory = _ipcManager!.PenumbraModDirectory()!; - string modDirectoryPath = Path.Combine(modDirectory, modName); - if (Directory.Exists(modDirectoryPath)) - { - Directory.Delete(modDirectoryPath, true); - } - - Directory.CreateDirectory(modDirectoryPath); - Directory.CreateDirectory(Path.Combine(modDirectoryPath, "files")); - Meta meta = new() - { - Name = modName, - Author = playerName, - Description = "Mare Synchronous Test Mod Export", - }; - - var resources = _characterManager!.BuildCharacterCache(); - var metaJson = JsonConvert.SerializeObject(meta); - File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson); - - DefaultMod defaultMod = new(); - - //using var db = new FileCacheContext(); - Stopwatch st = Stopwatch.StartNew(); - Parallel.ForEach(resources.AllReplacements, resource => - { - CopyFile(resource, modDirectoryPath, defaultMod.Files); - }); - PluginLog.Debug("Compression took " + st.Elapsed); - - var defaultModJson = JsonConvert.SerializeObject(defaultMod); - File.WriteAllText(Path.Combine(modDirectoryPath, "default_mod.json"), defaultModJson); - - PluginLog.Debug("Mod created to " + modDirectoryPath); - }); - } - if (string.IsNullOrEmpty(args)) { _pluginUi.Toggle(); @@ -258,7 +161,10 @@ namespace MareSynchronos private void OpenConfigUi() { - _pluginUi.Toggle(); + if(_configuration.HasValidSetup) + _pluginUi.Toggle(); + else + _introUi.Toggle(); } } } diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 316526d..8edc5b7 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -43,7 +43,6 @@ namespace MareSynchronos.UI }; _windowSystem.AddWindow(this); - IsOpen = true; } public override void Draw() @@ -164,7 +163,10 @@ namespace MareSynchronos.UI ImGui.Separator(); UIShared.TextWrapped(_pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri]); ImGui.Separator(); - ImGui.Button("Copy secret key to clipboard"); + if (ImGui.Button("Copy secret key to clipboard")) + { + ImGui.SetClipboardText(_pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri]); + } ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); UIShared.TextWrapped("This is the only time you will be able to see this key in the UI. You can copy it to make a backup somewhere."); ImGui.PopStyleColor(); diff --git a/MareSynchronos/UI/PluginUI.cs b/MareSynchronos/UI/PluginUI.cs index b3768be..8a90f88 100644 --- a/MareSynchronos/UI/PluginUI.cs +++ b/MareSynchronos/UI/PluginUI.cs @@ -238,9 +238,8 @@ namespace MareSynchronos.UI { if (_apiController.PairedClients.All(w => w.OtherUID != tempNameUID)) { - _ = _apiController.SendPairedClientAddition(tempNameUID); - tempNameUID = string.Empty; + _ = _apiController.SendPairedClientAddition(tempNameUID); } } ImGui.PopFont(); diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs index e6472f9..9cdd8a6 100644 --- a/MareSynchronos/WebAPI/ApiController.cs +++ b/MareSynchronos/WebAPI/ApiController.cs @@ -1,27 +1,17 @@ -using Dalamud.Configuration; -using Dalamud.Logging; -using MareSynchronos.Models; +using Dalamud.Logging; using System; -using System.Buffers.Text; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net.Http; -using System.Runtime.InteropServices.ComTypes; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -using Dalamud.Game.ClientState.Objects.Types; using LZ4; using MareSynchronos.API; using MareSynchronos.FileCacheDB; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.VisualBasic; namespace MareSynchronos.WebAPI { @@ -124,7 +114,7 @@ namespace MareSynchronos.WebAPI await LoadInitialData(); Connected?.Invoke(this, EventArgs.Empty); } - catch + catch { //PluginLog.Error(ex, "Error during Heartbeat initialization"); } @@ -241,9 +231,15 @@ namespace MareSynchronos.WebAPI private void UpdateLocalClientPairs(ClientPairDto dto, string characterIdentifier) { var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); + if (dto.IsRemoved) + { + PairedClients.RemoveAll(p => p.OtherUID == dto.OtherUID); + UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); + return; + } if (entry == null) { - UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); + PairedClients.Add(dto); return; } @@ -325,7 +321,7 @@ namespace MareSynchronos.WebAPI var uploadToken = uploadCancellationTokenSource.Token; PluginLog.Debug("New Token Created"); - var filesToUpload = await _fileHub!.InvokeAsync>("SendFiles", character.FileReplacements, uploadToken); + var filesToUpload = await _fileHub!.InvokeAsync>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); IsUploading = true; @@ -333,6 +329,7 @@ namespace MareSynchronos.WebAPI Dictionary 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);