rebuild PlayerManager to CacheCreationService and optimize creation of the local file cache

This commit is contained in:
rootdarkarchon
2023-02-02 15:25:58 +01:00
parent 86549b2d3f
commit ede62fabae
23 changed files with 461 additions and 737 deletions

View File

@@ -11,6 +11,7 @@ using MareSynchronos.Utils;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
using Penumbra.String;
using Weapon = MareSynchronos.Interop.Weapon;
using MareSynchronos.FileCache;
namespace MareSynchronos.Factories;
@@ -19,23 +20,23 @@ public class CharacterDataFactory
private readonly DalamudUtil _dalamudUtil;
private readonly IpcManager _ipcManager;
private readonly TransientResourceManager _transientResourceManager;
private readonly FileReplacementFactory _fileReplacementFactory;
private readonly FileCacheManager _fileCacheManager;
public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileReplacementFactory fileReplacementFactory)
public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory)
{
Logger.Verbose("Creating " + nameof(CharacterDataFactory));
_dalamudUtil = dalamudUtil;
_ipcManager = ipcManager;
_transientResourceManager = transientResourceManager;
_fileReplacementFactory = fileReplacementFactory;
_fileCacheManager = fileReplacementFactory;
}
private unsafe bool CheckForPointer(IntPtr playerPointer)
private unsafe bool CheckForNullDrawObject(IntPtr playerPointer)
{
return playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null;
return ((Character*)playerPointer)->GameObject.DrawObject == null;
}
public CharacterData BuildCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token)
public CharacterData BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
{
if (!_ipcManager.Initialized)
{
@@ -45,7 +46,16 @@ public class CharacterDataFactory
bool pointerIsZero = true;
try
{
pointerIsZero = CheckForPointer(playerRelatedObject.Address);
pointerIsZero = playerRelatedObject.Address == IntPtr.Zero;
try
{
pointerIsZero = CheckForNullDrawObject(playerRelatedObject.Address);
}
catch
{
pointerIsZero = true;
Logger.Debug("NullRef for " + playerRelatedObject.ObjectKind);
}
}
catch (Exception ex)
{
@@ -67,17 +77,20 @@ public class CharacterDataFactory
try
{
pathsToForwardResolve.Clear();
pathsToReverseResolve.Clear();
return CreateCharacterData(previousData, playerRelatedObject, token);
}
catch (OperationCanceledException)
{
Logger.Debug("Cancelled creating Character data");
throw;
}
catch (Exception e)
{
Logger.Warn("Failed to create " + playerRelatedObject.ObjectKind + " data");
Logger.Warn(e.Message);
Logger.Warn(e.StackTrace ?? string.Empty);
Logger.Debug("Failed to create " + playerRelatedObject.ObjectKind + " data");
Logger.Debug(e.Message);
Logger.Debug(e.StackTrace ?? string.Empty);
}
previousData.FileReplacements = previousFileReplacements;
@@ -85,23 +98,7 @@ public class CharacterDataFactory
return previousData;
}
private (string, string) GetIndentationForInheritanceLevel(int inheritanceLevel)
{
return (string.Join("", Enumerable.Repeat("\t", inheritanceLevel)), string.Join("", Enumerable.Repeat("\t", inheritanceLevel + 2)));
}
private void DebugPrint(FileReplacement fileReplacement, ObjectKind objectKind, string resourceType, int inheritanceLevel)
{
var indentation = GetIndentationForInheritanceLevel(inheritanceLevel);
if (fileReplacement.HasFileReplacement)
{
Logger.Verbose(indentation.Item1 + objectKind + resourceType + " [" + string.Join(", ", fileReplacement.GamePaths) + "]");
Logger.Verbose(indentation.Item2 + "=> " + fileReplacement.ResolvedPath);
}
}
private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0)
private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl)
{
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
@@ -115,26 +112,23 @@ public class CharacterDataFactory
}
catch
{
Logger.Warn("Could not get model data for " + objectKind);
Logger.Warn("Could not get model data");
return;
}
Logger.Verbose("Checking File Replacement for Model " + mdlPath);
FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath);
DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel);
cache.AddFileReplacement(objectKind, mdlFileReplacement);
AddResolvePath(mdlPath);
for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
{
var mtrl = (Material*)mdl->Materials[mtrlIdx];
if (mtrl == null) continue;
AddReplacementsFromMaterial(mtrl, objectKind, cache, inheritanceLevel + 1);
AddReplacementsFromMaterial(mtrl);
}
}
private unsafe void AddReplacementsFromMaterial(Material* mtrl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0)
private unsafe void AddReplacementsFromMaterial(Material* mtrl)
{
string fileName;
try
@@ -144,25 +138,15 @@ public class CharacterDataFactory
}
catch
{
Logger.Warn("Could not get material data for " + objectKind);
Logger.Warn("Could not get material data");
return;
}
Logger.Verbose("Checking File Replacement for Material " + fileName);
var mtrlPath = fileName.Split("|")[2];
if (cache.FileReplacements.ContainsKey(objectKind))
{
if (cache.FileReplacements[objectKind].Any(c => c.ResolvedPath.Contains(mtrlPath, StringComparison.Ordinal)))
{
return;
}
}
var mtrlFileReplacement = CreateFileReplacement(mtrlPath);
DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel);
cache.AddFileReplacement(objectKind, mtrlFileReplacement);
AddResolvePath(mtrlPath);
var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle;
for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++)
@@ -181,14 +165,14 @@ public class CharacterDataFactory
Logger.Verbose("Checking File Replacement for Texture " + texPath);
AddReplacementsFromTexture(texPath, objectKind, cache, inheritanceLevel + 1);
AddReplacementsFromTexture(texPath);
}
try
{
var shpkPath = "shader/sm5/shpk/" + new ByteString(mtrlResourceHandle->ShpkString).ToString();
Logger.Verbose("Checking File Replacement for Shader " + shpkPath);
AddReplacementsFromShader(shpkPath, objectKind, cache, inheritanceLevel + 1);
AddReplacementsFromShader(shpkPath);
}
catch
{
@@ -196,130 +180,92 @@ public class CharacterDataFactory
}
}
private void AddReplacement(string varPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = false)
private void AddReplacement(string varPath, bool doNotReverseResolve = false)
{
if (varPath.IsNullOrEmpty()) return;
if (cache.FileReplacements.ContainsKey(objectKind))
{
if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(varPath, StringComparer.Ordinal)))
{
return;
}
}
var variousReplacement = CreateFileReplacement(varPath, doNotReverseResolve);
DebugPrint(variousReplacement, objectKind, "Various", inheritanceLevel);
cache.AddFileReplacement(objectKind, variousReplacement);
AddResolvePath(varPath, doNotReverseResolve);
}
private void AddReplacementsFromShader(string shpkPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0)
private void AddReplacementsFromShader(string shpkPath)
{
if (string.IsNullOrEmpty(shpkPath)) return;
if (cache.FileReplacements.ContainsKey(objectKind))
{
if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(shpkPath, StringComparer.Ordinal)))
{
return;
}
}
var shpkFileReplacement = CreateFileReplacement(shpkPath, doNotReverseResolve: true);
DebugPrint(shpkFileReplacement, objectKind, "Shader", inheritanceLevel);
cache.AddFileReplacement(objectKind, shpkFileReplacement);
AddResolvePath(shpkPath, doNotReverseResolve: true);
}
private void AddReplacementsFromTexture(string texPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = true)
private void AddReplacementsFromTexture(string texPath, bool doNotReverseResolve = true)
{
if (string.IsNullOrEmpty(texPath)) return;
if (cache.FileReplacements.ContainsKey(objectKind))
{
if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(texPath, StringComparer.Ordinal)))
{
return;
}
}
var texFileReplacement = CreateFileReplacement(texPath, doNotReverseResolve);
DebugPrint(texFileReplacement, objectKind, "Texture", inheritanceLevel);
cache.AddFileReplacement(objectKind, texFileReplacement);
AddResolvePath(texPath, doNotReverseResolve);
if (texPath.Contains("/--", StringComparison.Ordinal)) return;
var texDx11Replacement =
CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve);
DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel);
cache.AddFileReplacement(objectKind, texDx11Replacement);
AddResolvePath(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve);
}
private unsafe CharacterData CreateCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token)
private unsafe CharacterData CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
{
var objectKind = playerRelatedObject.ObjectKind;
var charaPointer = playerRelatedObject.Address;
if (!previousData.FileReplacements.ContainsKey(objectKind))
{
previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance);
}
_dalamudUtil.WaitWhileCharacterIsDrawing(playerRelatedObject.ObjectKind.ToString(), playerRelatedObject.Address, ct: token);
Stopwatch st = Stopwatch.StartNew();
if (playerRelatedObject.HasUnprocessedUpdate)
Logger.Debug("Handling unprocessed update for " + objectKind);
if (previousData.FileReplacements.ContainsKey(objectKind))
{
Logger.Debug("Handling unprocessed update for " + objectKind);
previousData.FileReplacements[objectKind].Clear();
}
if (previousData.FileReplacements.ContainsKey(objectKind))
var chara = _dalamudUtil.CreateGameObject(charaPointer)!;
while (!DalamudUtil.IsObjectPresent(chara))
{
Logger.Verbose("Character is null but it shouldn't be, waiting");
Thread.Sleep(50);
}
var human = (Human*)((Character*)charaPointer)->GameObject.DrawObject;
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
previousData.FileReplacements[objectKind].Clear();
}
else
{
previousData.FileReplacements.Add(objectKind, new());
continue;
}
var chara = _dalamudUtil.CreateGameObject(charaPointer)!;
while (!DalamudUtil.IsObjectPresent(chara))
{
Logger.Verbose("Character is null but it shouldn't be, waiting");
Thread.Sleep(50);
}
token.ThrowIfCancellationRequested();
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
continue;
}
AddReplacementsFromRenderModel(mdl);
}
token.ThrowIfCancellationRequested();
AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0);
}
if (objectKind == ObjectKind.Player)
{
AddPlayerSpecificReplacements(objectKind, charaPointer, human);
}
if (objectKind == ObjectKind.Pet)
{
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource(charaPointer, item);
_transientResourceManager.AddSemiTransientResource(objectKind, item.GamePaths.First());
}
if (objectKind == ObjectKind.Player)
{
AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human);
}
if (objectKind == ObjectKind.Pet)
{
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.AddSemiTransientResource(objectKind, item);
}
previousData.FileReplacements[objectKind].Clear();
}
previousData.FileReplacements[objectKind].Clear();
}
Dictionary<string, List<string>> resolvedPaths = GetFileReplacementsFromPaths();
previousData.FileReplacements[objectKind] = new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)));
previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations();
previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer);
previousData.HeelsOffset = _ipcManager.GetHeelsOffset();
@@ -327,52 +273,75 @@ public class CharacterDataFactory
previousData.PalettePlusPalette = _ipcManager.PalettePlusBuildPalette();
Logger.Debug("Handling transient update for " + objectKind);
ManageSemiTransientData(previousData, objectKind, charaPointer);
_transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList());
pathsToForwardResolve.Clear();
pathsToReverseResolve.Clear();
ManageSemiTransientData(objectKind, charaPointer);
var resolvedTransientPaths = GetFileReplacementsFromPaths();
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)))
{
previousData.FileReplacements[objectKind].Add(replacement);
}
foreach (var item in previousData.FileReplacements[objectKind])
{
Logger.Debug(item.ToString());
}
st.Stop();
Logger.Verbose("Building " + objectKind + " Data took " + st.Elapsed);
Logger.Verbose("Building " + objectKind + " Data took " + st.ElapsedMilliseconds + "ms");
return previousData;
}
private unsafe void ManageSemiTransientData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer)
private Dictionary<string, List<string>> GetFileReplacementsFromPaths()
{
_transientResourceManager.PersistTransientResources(charaPointer, objectKind, CreateFileReplacement);
// get rid of items that have no file replacements anymore
foreach (var entry in previousData.FileReplacements.ToList())
var forwardPaths = pathsToForwardResolve.ToArray();
var reversePaths = pathsToReverseResolve.ToArray();
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
var result = _ipcManager.PenumbraResolvePaths(pathsToForwardResolve.ToArray(), pathsToReverseResolve.ToArray());
for (int i = 0; i < forwardPaths.Length; i++)
{
foreach (var item in entry.Value.ToList())
var filePath = result.forward[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list))
{
if (!item.HasFileReplacement) previousData.FileReplacements[entry.Key].Remove(item);
list.Add(forwardPaths[i].ToLowerInvariant());
}
else
{
resolvedPaths[filePath] = new List<string> { forwardPaths[i].ToLowerInvariant() };
}
}
for (int i = 0; i < reversePaths.Length; i++)
{
var filePath = reversePaths[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.AddRange(result.reverse[i].Select(c => c.ToLowerInvariant()));
}
else
{
resolvedPaths[filePath] = new List<string>(result.reverse[i].Select(c => c.ToLowerInvariant()).ToList());
}
}
return resolvedPaths;
}
private unsafe void ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer)
{
_transientResourceManager.PersistTransientResources(charaPointer, objectKind);
foreach (var item in _transientResourceManager.GetSemiTransientResources(objectKind))
{
if (!previousData.FileReplacements.ContainsKey(objectKind))
{
previousData.FileReplacements.Add(objectKind, new());
}
if (!previousData.FileReplacements[objectKind].Any(k => k.GamePaths.Any(p => item.GamePaths.Contains(p, StringComparer.OrdinalIgnoreCase))))
{
var penumResolve = _ipcManager.PenumbraResolvePath(item.GamePaths.First()).ToLowerInvariant();
var gamePath = item.GamePaths.First().ToLowerInvariant();
if (string.Equals(penumResolve, gamePath, StringComparison.Ordinal))
{
Logger.Verbose("PenumResolve was same as GamePath, not adding " + item);
_transientResourceManager.RemoveTransientResource(charaPointer, item);
}
else
{
Logger.Verbose("Found semi transient resource: " + item);
previousData.FileReplacements[objectKind].Add(item);
}
}
AddResolvePath(item, true);
}
}
private unsafe void AddPlayerSpecificReplacements(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer, Human* human)
private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human)
{
var weaponObject = (Weapon*)((Object*)human)->ChildObject;
@@ -380,42 +349,32 @@ public class CharacterDataFactory
{
var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel;
AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0);
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource(charaPointer, item);
}
AddReplacementsFromRenderModel(mainHandWeapon);
foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject))
{
Logger.Verbose("Found transient weapon resource: " + item);
AddReplacement(item, objectKind, previousData, 1, doNotReverseResolve: true);
AddReplacement(item, doNotReverseResolve: true);
}
if (weaponObject->NextSibling != (IntPtr)weaponObject)
{
var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel;
AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1);
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource((IntPtr)offHandWeapon, item);
}
AddReplacementsFromRenderModel(offHandWeapon);
foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon))
{
Logger.Verbose("Found transient offhand weapon resource: " + item);
AddReplacement(item, objectKind, previousData, 1, doNotReverseResolve: true);
AddReplacement(item, doNotReverseResolve: true);
}
}
}
AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, objectKind, previousData);
AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId);
try
{
AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->Decal->FileName()).ToString(), objectKind, previousData, 0, doNotReverseResolve: false);
AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->Decal->FileName()).ToString(), doNotReverseResolve: false);
}
catch
{
@@ -423,43 +382,29 @@ public class CharacterDataFactory
}
try
{
AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), objectKind, previousData, 0, doNotReverseResolve: false);
AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), doNotReverseResolve: false);
}
catch
{
Logger.Warn("Could not get Legacy Body Decal Data");
}
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource(charaPointer, item);
}
}
private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache)
private void AddReplacementSkeleton(ushort raceSexId)
{
string raceSexIdString = raceSexId.ToString("0000");
string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb";
var replacement = CreateFileReplacement(skeletonPath, doNotReverseResolve: true);
cache.AddFileReplacement(objectKind, replacement);
DebugPrint(replacement, objectKind, "SKLB", 0);
AddResolvePath(skeletonPath, doNotReverseResolve: true);
}
private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false)
private void AddResolvePath(string path, bool doNotReverseResolve = false)
{
var fileReplacement = _fileReplacementFactory.Create();
if (!doNotReverseResolve)
{
fileReplacement.ReverseResolvePath(path);
}
else
{
fileReplacement.ResolvePath(path);
}
return fileReplacement;
if (doNotReverseResolve) pathsToForwardResolve.Add(path.ToLowerInvariant());
else pathsToReverseResolve.Add(path.ToLowerInvariant());
}
private HashSet<string> pathsToForwardResolve = new(StringComparer.Ordinal);
private HashSet<string> pathsToReverseResolve = new(StringComparer.Ordinal);
}

View File

@@ -1,22 +0,0 @@
using MareSynchronos.FileCache;
using MareSynchronos.Managers;
using MareSynchronos.Models;
namespace MareSynchronos.Factories;
public class FileReplacementFactory
{
private readonly FileCacheManager fileCacheManager;
private readonly IpcManager ipcManager;
public FileReplacementFactory(FileCacheManager fileCacheManager, IpcManager ipcManager)
{
this.fileCacheManager = fileCacheManager;
this.ipcManager = ipcManager;
}
public FileReplacement Create()
{
return new FileReplacement(fileCacheManager, ipcManager);
}
}

View File

@@ -109,8 +109,8 @@ public class FileCacheManager : IDisposable
public FileCacheEntity? GetFileCacheByPath(string path)
{
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
var entry = _fileCaches.FirstOrDefault(f => f.Value.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)).Value;
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{
@@ -140,8 +140,8 @@ public class FileCacheManager : IDisposable
FileInfo fi = new(path);
if (!fi.Exists) return null;
var fullName = fi.FullName.ToLowerInvariant();
if (!fullName.Contains(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath);
}
@@ -203,7 +203,7 @@ public class FileCacheManager : IDisposable
{
if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase))
{
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory(), StringComparison.Ordinal));
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory, StringComparison.Ordinal));
}
else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase))
{

View File

@@ -150,7 +150,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
private void PeriodicFileScan(CancellationToken ct)
{
TotalFiles = 1;
var penumbraDir = _ipcManager.PenumbraModDirectory();
var penumbraDir = _ipcManager.PenumbraModDirectory;
bool penDirExists = true;
bool cacheDirExists = true;
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))

View File

@@ -0,0 +1,114 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Factories;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
namespace MareSynchronos.Managers;
public class CacheCreationService : MediatorSubscriberBase, IDisposable
{
private readonly CharacterDataFactory _characterDataFactory;
private readonly IpcManager _ipcManager;
private readonly ApiController _apiController;
private Task? _cacheCreationTask;
private Dictionary<ObjectKind, GameObjectHandler> _cachesToCreate = new();
private CharacterData _lastCreatedData = new();
private CancellationTokenSource cts = new();
private List<GameObjectHandler> _playerRelatedObjects;
public unsafe CacheCreationService(MareMediator mediator, CharacterDataFactory characterDataFactory, IpcManager ipcManager,
ApiController apiController, DalamudUtil dalamudUtil) : base(mediator)
{
_characterDataFactory = characterDataFactory;
_ipcManager = ipcManager;
_apiController = apiController;
Mediator.Subscribe<CreateCacheForObjectMessage>(this, (msg) =>
{
var actualMsg = (CreateCacheForObjectMessage)msg;
_cachesToCreate[actualMsg.ObjectToCreateFor.ObjectKind] = actualMsg.ObjectToCreateFor;
});
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (msg) => ProcessCacheCreation());
Mediator.Subscribe<CustomizePlusMessage>(this, (msg) => CustomizePlusChanged((CustomizePlusMessage)msg));
Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) => HeelsOffsetChanged((HeelsOffsetMessage)msg));
Mediator.Subscribe<PalettePlusMessage>(this, (msg) => PalettePlusChanged((PalettePlusMessage)msg));
_playerRelatedObjects = new List<GameObjectHandler>()
{
new(Mediator, ObjectKind.Player, () => dalamudUtil.PlayerPointer),
new(Mediator, ObjectKind.MinionOrMount, () => (IntPtr)((Character*)dalamudUtil.PlayerPointer)->CompanionObject),
new(Mediator, ObjectKind.Pet, () => dalamudUtil.GetPet()),
new(Mediator, ObjectKind.Companion, () => dalamudUtil.GetCompanion()),
};
}
private void PalettePlusChanged(PalettePlusMessage msg)
{
if (!string.Equals(msg.Data, _lastCreatedData.PalettePlusPalette, StringComparison.Ordinal))
{
_lastCreatedData.PalettePlusPalette = msg.Data ?? string.Empty;
Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData));
}
}
private void HeelsOffsetChanged(HeelsOffsetMessage msg)
{
if (msg.Offset != _lastCreatedData.HeelsOffset)
{
_lastCreatedData.HeelsOffset = msg.Offset;
Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData));
}
}
private void CustomizePlusChanged(CustomizePlusMessage msg)
{
if (!string.Equals(msg.Data, _lastCreatedData.CustomizePlusScale, StringComparison.Ordinal))
{
_lastCreatedData.CustomizePlusScale = msg.Data ?? string.Empty;
Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData));
}
}
private void ProcessCacheCreation()
{
if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true))
{
var toCreate = _cachesToCreate.ToList();
_cachesToCreate.Clear();
_cacheCreationTask = Task.Run(() =>
{
try
{
foreach (var obj in toCreate)
{
var data = _characterDataFactory.BuildCharacterData(_lastCreatedData, obj.Value, cts.Token);
}
Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData));
}
catch (Exception ex)
{
Logger.Error("Error during Cache Creation Processing", ex);
}
finally
{
Logger.Debug("Cache Creation complete");
}
}, cts.Token);
}
else if (_cachesToCreate.Any())
{
Logger.Debug("Cache Creation stored until previous creation finished");
}
}
public override void Dispose()
{
base.Dispose();
_playerRelatedObjects.ForEach(p => p.Dispose());
cts.Dispose();
}
}

View File

@@ -19,7 +19,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
private readonly IpcManager _ipcManager;
private readonly FileCacheManager _fileDbManager;
private API.Data.CharacterData _cachedData = new();
private PlayerRelatedObject? _currentCharacterEquipment;
private GameObjectHandler? _currentCharacterEquipment;
private CancellationTokenSource? _downloadCancellationTokenSource = new();
private bool _isVisible;
@@ -65,6 +65,18 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
Logger.Debug("Checking for files to download for player " + PlayerName);
Logger.Debug("Hash for data is " + characterData.DataHash.Value + ", current cache hash is " + _cachedData.DataHash.Value);
if (!_ipcManager.CheckPenumbraApi())
{
Mediator.Publish(new NotificationMessage("Penumbra inactive", "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Mare.", NotificationType.Error));
return;
}
if (!_ipcManager.CheckGlamourerApi())
{
Mediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Mare.", NotificationType.Error));
return;
}
if (string.Equals(characterData.DataHash.Value, _cachedData.DataHash.Value, StringComparison.Ordinal) && !forced) return;
bool updateModdedPaths = false;
@@ -116,6 +128,14 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
if (objectKind == ObjectKind.Player)
{
bool manipDataDifferent = !string.Equals(_cachedData.ManipulationData, characterData.ManipulationData, StringComparison.Ordinal);
if (manipDataDifferent)
{
Logger.Debug("Updating " + objectKind);
charaDataToUpdate.Add(objectKind);
continue;
}
bool heelsOffsetDifferent = _cachedData.HeelsOffset != characterData.HeelsOffset;
if (heelsOffsetDifferent)
{
@@ -189,8 +209,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
return false;
}
_currentCharacterEquipment?.CheckAndUpdateObject();
if (_currentCharacterEquipment?.HasUnprocessedUpdate ?? false)
if (_currentCharacterEquipment?.CheckAndUpdateObject() ?? false)
{
OnPlayerChanged();
}
@@ -247,8 +266,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<PenumbraRedrawMessage>(this, (msg) => IpcManagerOnPenumbraRedrawEvent(((PenumbraRedrawMessage)msg)));
_originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter);
_currentCharacterEquipment = new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero,
() => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero);
_currentCharacterEquipment = new GameObjectHandler(Mediator, ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, false);
}
public override string ToString()
@@ -437,7 +455,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
{
PlayerCharacter = msg.Address;
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));
cts.CancelAfter(TimeSpan.FromSeconds(10));
_dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName!, PlayerCharacter, 10000, cts.Token);
cts.Dispose();
cts = new CancellationTokenSource();
@@ -460,7 +478,6 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
private void OnPlayerChanged()
{
Logger.Debug($"Player {PlayerName} changed, PenumbraRedraw is {RequestedPenumbraRedraw}");
_currentCharacterEquipment!.HasUnprocessedUpdate = false;
if (!RequestedPenumbraRedraw && PlayerCharacter != IntPtr.Zero)
{
Logger.Debug($"Saving new Glamourer data");

View File

@@ -35,8 +35,9 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
private readonly FuncSubscriber<string, string> _penumbraResolvePlayer;
private readonly FuncSubscriber<string, string[]> _reverseResolvePlayer;
private readonly FuncSubscriber<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> _penumbraAddTemporaryMod;
private readonly FuncSubscriber<string[], string[], (string[], string[][])> _penumbraResolvePaths;
private readonly FuncSubscriber<bool> _penumbraEnabled;
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
private readonly EventSubscriber<ModSettingChange, string, string, bool> _penumbraModSettingChanged;
private readonly ICallGateSubscriber<string> _heelsGetApiVersion;
private readonly ICallGateSubscriber<float> _heelsGetOffset;
@@ -81,9 +82,10 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_penumbraRemoveTemporaryCollection = Penumbra.Api.Ipc.RemoveTemporaryCollectionByName.Subscriber(pi);
_penumbraRemoveTemporaryMod = Penumbra.Api.Ipc.RemoveTemporaryMod.Subscriber(pi);
_penumbraAssignTemporaryCollection = Penumbra.Api.Ipc.AssignTemporaryCollection.Subscriber(pi);
_penumbraResolvePaths = Penumbra.Api.Ipc.ResolvePlayerPaths.Subscriber(pi);
_penumbraEnabled = Penumbra.Api.Ipc.GetEnabledState.Subscriber(pi);
_penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, (ptr, arg1, arg2) => ResourceLoaded((IntPtr)ptr, arg1, arg2));
_penumbraModSettingChanged = Penumbra.Api.Ipc.ModSettingChanged.Subscriber(pi, (modsetting, a, b, c) => PenumbraModSettingChangedHandler());
_glamourerApiVersion = pi.GetIpcSubscriber<int>("Glamourer.ApiVersion");
_glamourerGetAllCustomization = pi.GetIpcSubscriber<GameObject?, string>("Glamourer.GetAllCustomizationFromCharacter");
@@ -125,6 +127,12 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => HandleActionQueue());
Mediator.Subscribe<GposeFrameworkUpdateMessage>(this, (_) => HandleGposeActionQueue());
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ClearActionQueue());
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => CheckPenumbraModPath());
}
private void CheckPenumbraModPath()
{
PenumbraModDirectory = GetPenumbraModDirectory();
}
private void HandleGposeActionQueue()
@@ -142,11 +150,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_inGposeQueueMode = on;
}
private void PenumbraModSettingChangedHandler()
{
Mediator.Publish(new PenumbraModSettingChangedMessage());
}
private void ClearActionQueue()
{
ActionQueue.Clear();
@@ -191,7 +194,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
{
try
{
return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 17 };
return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 19 } && _penumbraEnabled.Invoke();
}
catch
{
@@ -259,7 +262,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_penumbraDispose.Dispose();
_penumbraInit.Dispose();
_penumbraObjectIsRedrawn.Dispose();
_penumbraModSettingChanged.Dispose();
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
}
@@ -411,7 +413,9 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
return _penumbraGetMetaManipulations.Invoke();
}
public string? PenumbraModDirectory()
public string? PenumbraModDirectory;
public string? GetPenumbraModDirectory()
{
if (!CheckPenumbraApi()) return null;
return _penumbraResolveModDir!.Invoke().ToLowerInvariant();
@@ -495,6 +499,11 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
});
}
public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse)
{
return _penumbraResolvePaths.Invoke(forward, reverse);
}
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
{
Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex));

View File

@@ -97,7 +97,7 @@ public class NotificationService : MediatorSubscriberBase
private void PrintErrorChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] ").AddUiForeground("Error: ", 534).AddItalicsOn().AddUiForeground(message ?? string.Empty, 534).AddUiForegroundOff().AddItalicsOff();
_chatGui.Print(se.BuiltString);
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
}

View File

@@ -11,18 +11,17 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
{
private readonly ApiController _apiController;
private readonly DalamudUtil _dalamudUtil;
private readonly PlayerManager _playerManager;
private readonly FileCacheManager _fileDbManager;
private readonly PairManager _pairManager;
private CharacterData? _lastSentData;
public OnlinePlayerManager(ApiController apiController, DalamudUtil dalamudUtil, PlayerManager playerManager,
public OnlinePlayerManager(ApiController apiController, DalamudUtil dalamudUtil,
FileCacheManager fileDbManager, PairManager pairManager, MareMediator mediator) : base(mediator)
{
Logger.Verbose("Creating " + nameof(OnlinePlayerManager));
_apiController = apiController;
_dalamudUtil = dalamudUtil;
_playerManager = playerManager;
_fileDbManager = fileDbManager;
_pairManager = pairManager;
@@ -30,6 +29,20 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
{
var newData = ((CharacterDataCreatedMessage)msg).CharacterData.ToAPI();
if (_lastSentData == null || _lastSentData != null && !string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal))
{
Logger.Debug("Pushing data for visible players");
_lastSentData = newData;
PushCharacterData(_pairManager.VisibleUsers);
}
else
{
Logger.Debug("Not sending data for " + newData.DataHash.Value);
}
});
}
private void PlayerManagerOnPlayerHasChanged(PlayerChangedMessage msg)
@@ -78,11 +91,11 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
private void PushCharacterData(List<UserData> visiblePlayers)
{
if (visiblePlayers.Any() && _playerManager.LastCreatedCharacterData != null)
if (visiblePlayers.Any() && _lastSentData != null)
{
Task.Run(async () =>
{
await _apiController.PushCharacterData(_playerManager.LastCreatedCharacterData, visiblePlayers).ConfigureAwait(false);
await _apiController.PushCharacterData(_lastSentData, visiblePlayers).ConfigureAwait(false);
});
}
}

View File

@@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Utility;
using MareSynchronos.API.Data;

View File

@@ -1,279 +0,0 @@
using MareSynchronos.Factories;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using MareSynchronos.Models;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Mediator;
#if DEBUG
#endif
namespace MareSynchronos.Managers;
public class PlayerManager : MediatorSubscriberBase, IDisposable
{
private readonly ApiController _apiController;
private readonly CharacterDataFactory _characterDataFactory;
private readonly DalamudUtil _dalamudUtil;
private readonly IpcManager _ipcManager;
public API.Data.CharacterData? LastCreatedCharacterData { get; private set; }
public Models.CharacterData PermanentDataCache { get; private set; } = new();
private readonly Dictionary<ObjectKind, Func<bool>> _objectKindsToUpdate = new();
private CancellationTokenSource? _playerChangedCts = new();
private CancellationTokenSource _transientUpdateCts = new();
private readonly List<PlayerRelatedObject> _playerRelatedObjects = new();
public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager,
CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil,
MareMediator mediator) : base(mediator)
{
Logger.Verbose("Creating " + nameof(PlayerManager));
_apiController = apiController;
_ipcManager = ipcManager;
_characterDataFactory = characterDataFactory;
_dalamudUtil = dalamudUtil;
Mediator.Subscribe<CustomizePlusMessage>(this, (msg) => CustomizePlusChanged((CustomizePlusMessage)msg));
Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) => HeelsOffsetChanged((HeelsOffsetMessage)msg));
Mediator.Subscribe<PalettePlusMessage>(this, (msg) => PalettePlusChanged((PalettePlusMessage)msg));
Mediator.Subscribe<ConnectedMessage>(this, (_) => ApiControllerOnConnected());
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ApiController_Disconnected());
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => DalamudUtilOnDelayedFrameworkUpdate());
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => DalamudUtilOnFrameworkUpdate());
Mediator.Subscribe<TransientResourceChangedMessage>(this, (msg) => HandleTransientResourceLoad((TransientResourceChangedMessage)msg));
Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected);
if (_apiController.IsConnected)
{
ApiControllerOnConnected();
}
_playerRelatedObjects = new List<PlayerRelatedObject>()
{
new(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer),
new(ObjectKind.MinionOrMount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject),
new(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()),
new(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()),
};
}
private void DalamudUtilOnFrameworkUpdate()
{
Mediator.Publish(new PlayerRelatedObjectPointerUpdateMessage(_playerRelatedObjects.Select(f => f.CurrentAddress).ToArray()));
}
public void HandleTransientResourceLoad(TransientResourceChangedMessage msg)
{
foreach (var obj in _playerRelatedObjects)
{
if (obj.Address == msg.Address && !obj.HasUnprocessedUpdate)
{
_transientUpdateCts.Cancel();
_transientUpdateCts = new CancellationTokenSource();
var token = _transientUpdateCts.Token;
Task.Run(async () =>
{
Logger.Debug("Delaying transient resource load update");
await Task.Delay(750, token).ConfigureAwait(false);
if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return;
Logger.Debug("Firing transient resource load update");
obj.HasTransientsUpdate = true;
}, token);
return;
}
}
}
private void HeelsOffsetChanged(HeelsOffsetMessage change)
{
var player = _playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player);
if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change.Offset && !player.IsProcessing)
{
Logger.Debug("Heels offset changed to " + change.Offset);
player.HasTransientsUpdate = true;
}
}
private void CustomizePlusChanged(CustomizePlusMessage msg)
{
var change = msg.Data ?? string.Empty;
var player = _playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player);
if (LastCreatedCharacterData != null && !string.Equals(LastCreatedCharacterData.CustomizePlusData, change, StringComparison.Ordinal) && !player.IsProcessing)
{
Logger.Debug("CustomizePlus data changed to " + change);
player.HasTransientsUpdate = true;
}
}
private void PalettePlusChanged(PalettePlusMessage msg)
{
var change = msg.Data ?? string.Empty;
var player = _playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player);
if (LastCreatedCharacterData != null && !string.Equals(LastCreatedCharacterData.PalettePlusData, change, StringComparison.Ordinal) && !player.IsProcessing)
{
Logger.Debug("PalettePlus data changed to " + change);
player.HasTransientsUpdate = true;
}
}
public override void Dispose()
{
base.Dispose();
_playerChangedCts?.Cancel();
}
private unsafe void DalamudUtilOnDelayedFrameworkUpdate()
{
if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return;
_playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject());
if (_playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && !c.IsProcessing))
{
OnPlayerOrAttachedObjectsChanged();
}
}
private void ApiControllerOnConnected()
{
Logger.Debug("ApiController Connected");
Mediator.Subscribe<PenumbraRedrawMessage>(this, (msg) => IpcManager_PenumbraRedrawEvent((PenumbraRedrawMessage)msg));
}
private void ApiController_Disconnected()
{
Logger.Debug(nameof(ApiController_Disconnected));
Mediator.Unsubscribe<PenumbraRedrawMessage>(this);
}
private async Task<API.Data.CharacterData?> CreateFullCharacterCacheDto(CancellationToken token)
{
foreach (var unprocessedObject in _playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList())
{
Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind);
PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token);
if (!token.IsCancellationRequested)
{
unprocessedObject.HasUnprocessedUpdate = false;
unprocessedObject.IsProcessing = false;
unprocessedObject.HasTransientsUpdate = false;
}
token.ThrowIfCancellationRequested();
}
int timeOut = 10000;
while (!PermanentDataCache.IsReady && !token.IsCancellationRequested && timeOut >= 0)
{
Logger.Verbose("Waiting until cache is ready (Timeout: " + TimeSpan.FromMilliseconds(timeOut) + ")");
await Task.Delay(50, token).ConfigureAwait(false);
timeOut -= 50;
}
if (token.IsCancellationRequested || timeOut <= 0) return null;
Logger.Verbose("Cache creation complete");
var cache = PermanentDataCache.ToAPI();
//Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented));
return cache;
}
private void IpcManager_PenumbraRedrawEvent(PenumbraRedrawMessage msg)
{
Logger.Verbose("RedrawEvent for addr " + msg.Address);
foreach (var item in _playerRelatedObjects)
{
if (msg.Address == item.Address)
{
Logger.Debug("Penumbra redraw Event for " + item.ObjectKind);
item.HasUnprocessedUpdate = true;
}
}
if (_playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && (!c.IsProcessing || (c.IsProcessing && c.DoNotSendUpdate))))
{
OnPlayerOrAttachedObjectsChanged();
}
}
private void OnPlayerOrAttachedObjectsChanged()
{
var unprocessedObjects = _playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList();
foreach (var unprocessedObject in unprocessedObjects)
{
unprocessedObject.IsProcessing = true;
}
Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind)));
bool doNotSendUpdate = unprocessedObjects.All(c => c.DoNotSendUpdate);
unprocessedObjects.ForEach(p => p.DoNotSendUpdate = false);
_playerChangedCts?.Cancel();
_playerChangedCts = new CancellationTokenSource();
var token = _playerChangedCts.Token;
// fix for redraw from anamnesis
while ((!_dalamudUtil.IsPlayerPresent || string.Equals(_dalamudUtil.PlayerName, "--", StringComparison.Ordinal)) && !token.IsCancellationRequested)
{
Logger.Debug("Waiting Until Player is Present");
Thread.Sleep(100);
}
if (token.IsCancellationRequested)
{
Logger.Debug("Cancelled");
return;
}
if (!_ipcManager.Initialized)
{
Logger.Warn("Penumbra not active, doing nothing.");
return;
}
Task.Run(async () =>
{
API.Data.CharacterData? cacheData = null;
try
{
Mediator.Publish(new HaltScanMessage("Character creation"));
foreach (var item in unprocessedObjects)
{
_dalamudUtil.WaitWhileCharacterIsDrawing("self " + item.ObjectKind.ToString(), item.Address, item.ObjectKind == ObjectKind.MinionOrMount ? 1000 : 10000, token);
}
cacheData = (await CreateFullCharacterCacheDto(token).ConfigureAwait(false));
}
catch { }
finally
{
Mediator.Publish(new ResumeScanMessage("Character creation"));
}
if (cacheData == null || token.IsCancellationRequested) return;
#if DEBUG
//var json = JsonConvert.SerializeObject(cacheDto, Formatting.Indented);
//Logger.Verbose(json);
#endif
if (string.Equals(LastCreatedCharacterData?.DataHash.Value ?? string.Empty, cacheData.DataHash.Value, StringComparison.Ordinal))
{
Logger.Debug("Not sending data, already sent");
return;
}
LastCreatedCharacterData = cacheData;
if (_apiController.IsConnected && !token.IsCancellationRequested && !doNotSendUpdate)
{
Logger.Verbose("Invoking PlayerHasChanged");
Mediator.Publish(new PlayerChangedMessage(cacheData));
}
}, token);
}
}

View File

@@ -1,8 +1,6 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Factories;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using System.Collections.Concurrent;
@@ -16,13 +14,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
public IntPtr[] PlayerRelatedPointers = Array.Empty<IntPtr>();
private readonly string[] _fileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk" };
[Obsolete]
private string PersistentDataCache => Path.Combine(_configurationService.ConfigurationDirectory, "PersistentTransientData.lst");
private string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId;
private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
private ConcurrentDictionary<ObjectKind, HashSet<FileReplacement>> SemiTransientResources { get; } = new();
public TransientResourceManager(ConfigurationService configurationService, DalamudUtil dalamudUtil, FileReplacementFactory fileReplacementFactory, MareMediator mediator) : base(mediator)
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources { get; } = new();
public TransientResourceManager(ConfigurationService configurationService, DalamudUtil dalamudUtil, MareMediator mediator) : base(mediator)
{
_configurationService = configurationService;
_dalamudUtil = dalamudUtil;
@@ -32,16 +28,8 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
Mediator.Subscribe<ClassJobChangedMessage>(this, (_) => DalamudUtil_ClassJobChanged());
Mediator.Subscribe<PlayerRelatedObjectPointerUpdateMessage>(this, (msg) => PlayerRelatedPointers = ((PlayerRelatedObjectPointerUpdateMessage)msg).RelatedObjects);
// migrate obsolete data to new format
if (File.Exists(PersistentDataCache))
{
var persistentEntities = File.ReadAllLines(PersistentDataCache).ToHashSet(StringComparer.OrdinalIgnoreCase);
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = persistentEntities;
_configurationService.Save();
File.Delete(PersistentDataCache);
}
SemiTransientResources.TryAdd(ObjectKind.Player, new HashSet<FileReplacement>());
SemiTransientResources.TryAdd(ObjectKind.Player, new HashSet<string>(StringComparer.Ordinal));
if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var linesInConfig))
{
int restored = 0;
@@ -49,14 +37,9 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
{
try
{
var fileReplacement = fileReplacementFactory.Create();
fileReplacement.ResolvePath(file);
if (fileReplacement.HasFileReplacement)
{
Logger.Debug("Loaded persistent transient resource " + file);
SemiTransientResources[ObjectKind.Player].Add(fileReplacement);
restored++;
}
Logger.Debug("Loaded persistent transient resource " + file);
SemiTransientResources[ObjectKind.Player].Add(file);
restored++;
}
catch (Exception ex)
{
@@ -70,20 +53,12 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
private void Manager_PenumbraModSettingChanged()
{
bool successfulValidation = true;
Task.Run(() =>
{
Logger.Debug("Penumbra Mod Settings changed, verifying SemiTransientResources");
foreach (var item in SemiTransientResources)
{
item.Value.RemoveWhere(p =>
{
var verified = p.Verify();
successfulValidation &= verified;
return !verified;
});
if (!successfulValidation)
Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer));
Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer));
}
});
}
@@ -126,19 +101,19 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
return new List<string>();
}
public List<FileReplacement> GetSemiTransientResources(ObjectKind objectKind)
public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
{
if (SemiTransientResources.TryGetValue(objectKind, out var result))
{
return result.ToList();
return result;
}
return new List<FileReplacement>();
return new HashSet<string>();
}
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
{
var gamePath = msg.GamePath;
var gamePath = msg.GamePath.ToLowerInvariant();
var gameObject = msg.GameObject;
var filePath = msg.FilePath;
if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
@@ -166,14 +141,9 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) return;
if (TransientResources[gameObject].Contains(replacedGamePath) ||
SemiTransientResources.Any(r => r.Value.Any(f =>
string.Equals(f.GamePaths.First(), replacedGamePath, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase))
))
SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))))
{
Logger.Verbose("Not adding " + replacedGamePath + ":" + filePath);
Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f.GamePaths.First(), replacedGamePath, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase))).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath));
}
else
{
@@ -183,19 +153,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
}
}
public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement)
{
if (TransientResources.ContainsKey(gameObject))
{
TransientResources[gameObject].RemoveWhere(f => fileReplacement.GamePaths.Any(g => string.Equals(g, f, StringComparison.OrdinalIgnoreCase)));
}
}
public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func<string, bool, FileReplacement> createFileReplacement)
public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind)
{
if (!SemiTransientResources.ContainsKey(objectKind))
{
SemiTransientResources[objectKind] = new HashSet<FileReplacement>();
SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
}
if (!TransientResources.TryGetValue(gameObject, out var resources))
@@ -203,47 +165,16 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
return;
}
SemiTransientResources[objectKind].RemoveWhere(p => !p.Verify());
var transientResources = resources.ToList();
Logger.Debug("Persisting " + transientResources.Count + " transient resources");
foreach (var gamePath in transientResources)
{
var existingResource = SemiTransientResources[objectKind].Any(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase));
if (existingResource)
{
Logger.Debug("Semi Transient resource replaced: " + gamePath);
SemiTransientResources[objectKind].RemoveWhere(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase));
}
try
{
var fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), arg2: true);
if (!fileReplacement.HasFileReplacement)
fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), arg2: false);
if (fileReplacement.HasFileReplacement)
{
Logger.Debug("Persisting " + gamePath.ToLowerInvariant());
if (SemiTransientResources[objectKind].Add(fileReplacement))
{
Logger.Debug("Added " + fileReplacement);
}
else
{
Logger.Debug("Not added " + fileReplacement);
}
}
}
catch (Exception ex)
{
Logger.Warn("Issue during transient file persistence", ex);
}
SemiTransientResources[objectKind].Add(gamePath);
}
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements))
{
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey]
= fileReplacements.SelectMany(p => p.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToHashSet(StringComparer.OrdinalIgnoreCase);
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements;
_configurationService.Save();
}
TransientResources[gameObject].Clear();
@@ -256,22 +187,26 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
SemiTransientResources.Clear();
if (SemiTransientResources.ContainsKey(ObjectKind.Player))
{
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey]
= SemiTransientResources[ObjectKind.Player].SelectMany(p => p.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToHashSet(StringComparer.OrdinalIgnoreCase);
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player];
_configurationService.Save();
}
}
internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item)
internal void AddSemiTransientResource(ObjectKind objectKind, string item)
{
if (!SemiTransientResources.ContainsKey(objectKind))
{
SemiTransientResources[objectKind] = new HashSet<FileReplacement>();
SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
}
if (!SemiTransientResources[objectKind].Any(f => string.Equals(f.ResolvedPath, item.ResolvedPath, StringComparison.OrdinalIgnoreCase)))
SemiTransientResources[objectKind].Add(item.ToLowerInvariant());
}
internal void ClearTransientPaths(IntPtr ptr, List<string> list)
{
if (TransientResources.TryGetValue(ptr, out var set))
{
SemiTransientResources[objectKind].Add(item);
set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
}
}
}

View File

@@ -94,8 +94,8 @@ public class MarePlugin : MediatorSubscriberBase, IDisposable
_runtimeServiceScope?.Dispose();
_runtimeServiceScope = _serviceProvider.CreateScope();
_runtimeServiceScope.ServiceProvider.GetRequiredService<CacheCreationService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<PlayerManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
}
catch (Exception ex)

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>0.7.13</Version>
<Version>0.7.14</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>
@@ -33,7 +33,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.2" />
<PackageReference Include="Penumbra.Api" Version="1.0.6" />
<PackageReference Include="Penumbra.Api" Version="1.0.7" />
<PackageReference Include="Penumbra.String" Version="1.0.1" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.Models;
namespace MareSynchronos.Mediator;
@@ -35,4 +36,7 @@ public record ResumeScanMessage(string Source) : IMessage;
public record NotificationMessage
(string Title, string Message, NotificationType Type, uint TimeShownOnScreen = 3000) : IMessage;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : IMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : IMessage;
#pragma warning restore MA0048 // File name must match type name

View File

@@ -1,6 +1,5 @@
using Newtonsoft.Json;
using System.Text;
using MareSynchronos.Utils;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data;
@@ -10,7 +9,7 @@ namespace MareSynchronos.Models;
public class CharacterData
{
[JsonProperty]
public Dictionary<ObjectKind, List<FileReplacement>> FileReplacements { get; set; } = new();
public Dictionary<ObjectKind, HashSet<FileReplacement>> FileReplacements { get; set; } = new();
[JsonProperty]
public Dictionary<ObjectKind, string> GlamourerString { get; set; } = new();
@@ -33,7 +32,7 @@ public class CharacterData
{
if (!fileReplacement.HasFileReplacement) return;
if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List<FileReplacement>());
if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new HashSet<FileReplacement>());
var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => string.Equals(f.ResolvedPath, fileReplacement.ResolvedPath, StringComparison.OrdinalIgnoreCase));
if (existingReplacement != null)
@@ -57,16 +56,9 @@ public class CharacterData
};
}).ToList());
Logger.Debug("Adding fileSwaps");
foreach (var item in FileReplacements)
{
Logger.Debug("Checking fileSwaps for " + item.Key);
var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto());
Logger.Debug("Adding " + fileSwapsToAdd.Count() + " file swaps");
foreach (var swap in fileSwapsToAdd)
{
Logger.Debug("Adding: " + swap.GamePaths.First() + ":" + swap.FileSwapPath);
}
fileReplacements[item.Key].AddRange(fileSwapsToAdd);
}

View File

@@ -1,79 +1,31 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using MareSynchronos.FileCache;
using MareSynchronos.Managers;
using MareSynchronos.Utils;
using MareSynchronos.API.Data;
namespace MareSynchronos.Models;
public class FileReplacement
{
private readonly FileCacheManager _fileDbManager;
private readonly IpcManager _ipcManager;
public FileReplacement(FileCacheManager fileDbManager, IpcManager ipcManager)
public FileReplacement(List<string> gamePaths, string filePath, FileCacheManager fileDbManager)
{
_fileDbManager = fileDbManager;
_ipcManager = ipcManager;
GamePaths = gamePaths.Select(g => g.Replace('\\', '/')).ToList();
ResolvedPath = filePath.Replace('\\', '/');
HashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty);
}
public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash);
public List<string> GamePaths { get; set; } = new();
public List<string> GamePaths { get; init; } = new();
public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal));
public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && !string.Equals(GamePaths[0], ResolvedPath, StringComparison.Ordinal);
public string Hash { get; private set; } = string.Empty;
public string Hash => HashLazy.Value;
public string ResolvedPath { get; set; } = string.Empty;
private Lazy<string> HashLazy;
private void SetResolvedPath(string path)
{
ResolvedPath = path.ToLowerInvariant().Replace('\\', '/');
if (!HasFileReplacement || IsFileSwap) return;
_ = Task.Run(() =>
{
try
{
var cache = _fileDbManager.GetFileCacheByPath(ResolvedPath)!;
Hash = cache.Hash;
}
catch (Exception ex)
{
Logger.Warn("Could not set Hash for " + ResolvedPath + ", resetting to original", ex);
ResolvedPath = GamePaths[0];
}
});
}
public bool Verify()
{
if (!IsFileSwap)
{
var cache = _fileDbManager.GetFileCacheByPath(ResolvedPath);
if (cache == null)
{
Logger.Warn("Replacement Failed verification: " + GamePaths[0]);
return false;
}
Hash = cache.Hash;
return true;
}
ResolvePath(GamePaths[0]);
var success = IsFileSwap;
if (!success)
{
Logger.Warn("FileSwap Failed verification: " + GamePaths[0]);
}
return success;
}
public string ResolvedPath { get; init; } = string.Empty;
public FileReplacementData ToFileReplacementDto()
{
@@ -87,20 +39,6 @@ public class FileReplacement
public override string ToString()
{
StringBuilder builder = new();
builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}");
return builder.ToString();
}
internal void ReverseResolvePath(string path)
{
GamePaths = _ipcManager.PenumbraReverseResolvePlayer(path).ToList();
SetResolvedPath(path);
}
internal void ResolvePath(string path)
{
GamePaths = new List<string> { path };
SetResolvedPath(_ipcManager.PenumbraResolvePath(path));
return $"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}";
}
}

View File

@@ -3,12 +3,15 @@ using System.Runtime.InteropServices;
using MareSynchronos.Utils;
using Penumbra.String;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Mediator;
namespace MareSynchronos.Models;
public class PlayerRelatedObject
public class GameObjectHandler : MediatorSubscriberBase
{
private readonly MareMediator _mediator;
private readonly Func<IntPtr> getAddress;
private readonly bool _sendUpdates;
public unsafe Character* Character => (Character*)Address;
@@ -31,26 +34,31 @@ public class PlayerRelatedObject
}
}
public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func<IntPtr> getAddress)
public GameObjectHandler(MareMediator mediator, ObjectKind objectKind, Func<IntPtr> getAddress, bool sendUpdates = true) : base(mediator)
{
_mediator = mediator;
ObjectKind = objectKind;
Address = address;
DrawObjectAddress = drawObjectAddress;
this.getAddress = getAddress;
_sendUpdates = sendUpdates;
_name = string.Empty;
Mediator.Subscribe<TransientResourceChangedMessage>(this, (msg) =>
{
var actualMsg = (TransientResourceChangedMessage)msg;
if (actualMsg.Address != Address || !sendUpdates) return;
Mediator.Publish(new CreateCacheForObjectMessage(this));
});
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => CheckAndUpdateObject());
}
public byte[] EquipSlotData { get; set; } = new byte[40];
public byte[] CustomizeData { get; set; } = new byte[26];
public byte? HatState { get; set; }
public byte? VisorWeaponState { get; set; }
private bool _doNotSendUpdate;
public bool HasTransientsUpdate { get; set; } = false;
public bool HasUnprocessedUpdate { get; set; } = false;
public bool DoNotSendUpdate { get; set; } = false;
public bool IsProcessing { get; set; } = false;
public unsafe void CheckAndUpdateObject()
public unsafe bool CheckAndUpdateObject()
{
var curPtr = CurrentAddress;
if (curPtr != IntPtr.Zero)
@@ -68,7 +76,10 @@ public class PlayerRelatedObject
Address = curPtr;
DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject;
HasUnprocessedUpdate = true;
if (_sendUpdates && !_doNotSendUpdate)
Mediator.Publish(new CreateCacheForObjectMessage(this));
return true;
}
}
else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero)
@@ -77,12 +88,14 @@ public class PlayerRelatedObject
DrawObjectAddress = IntPtr.Zero;
Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + Address + ", " + DrawObjectAddress);
}
return false;
}
private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData)
{
bool hasChanges = false;
DoNotSendUpdate = false;
_doNotSendUpdate = false;
for (int i = 0; i < EquipSlotData.Length; i++)
{
var data = Marshal.ReadByte((IntPtr)equipSlotData, i);
@@ -107,10 +120,10 @@ public class PlayerRelatedObject
var newWeaponOrVisorState = Marshal.ReadByte((IntPtr)customizeData + 31, 0);
if (newHatState != HatState)
{
if (HatState != null && !hasChanges && !HasUnprocessedUpdate)
if (HatState != null && !hasChanges)
{
Logger.Debug("Not Sending Update, only Hat changed");
DoNotSendUpdate = true;
_doNotSendUpdate = true;
}
HatState = newHatState;
hasChanges = true;
@@ -120,10 +133,10 @@ public class PlayerRelatedObject
if (newWeaponOrVisorState != VisorWeaponState)
{
if (VisorWeaponState != null && !hasChanges && !HasUnprocessedUpdate)
if (VisorWeaponState != null && !hasChanges)
{
Logger.Debug("Not Sending Update, only Visor/Weapon changed");
DoNotSendUpdate = true;
_doNotSendUpdate = true;
}
VisorWeaponState = newWeaponOrVisorState;
hasChanges = true;

View File

@@ -59,7 +59,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PairManager>();
collection.AddSingleton<ApiController>();
collection.AddSingleton<PeriodicFileScanner>();
collection.AddSingleton<FileReplacementFactory>();
collection.AddSingleton<MareCharaFileManager>();
collection.AddSingleton<NotificationService>();
@@ -70,9 +69,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IntroUi>();
collection.AddSingleton<DownloadUi>();
collection.AddScoped<CacheCreationService>();
collection.AddScoped<TransientResourceManager>();
collection.AddScoped<CharacterDataFactory>();
collection.AddScoped<PlayerManager>();
collection.AddScoped<OnlinePlayerManager>();
var serviceProvider = collection.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true, ValidateScopes = true });

View File

@@ -57,7 +57,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeStartMessage>(this, (_) => UiShared_GposeStart());
Mediator.Subscribe<GposeEndMessage>(this, (_) => UiShared_GposeEnd());
Mediator.Subscribe<PlayerChangedMessage>(this, (msg) => LastCreatedCharacterData = ((PlayerChangedMessage)msg).Data);
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) => LastCreatedCharacterData = ((CharacterDataCreatedMessage)msg).CharacterData.ToAPI());
windowSystem.AddWindow(this);
}

View File

@@ -38,7 +38,7 @@ public partial class UiShared : IDisposable
public string PlayerName => _dalamudUtil.PlayerName;
public uint WorldId => _dalamudUtil.WorldId;
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value;
public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory() ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory());
public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory);
public bool EditTrackerPosition { get; set; }
public ImFontPtr UidFont { get; private set; }
public bool UidFontBuilt { get; private set; }
@@ -468,7 +468,7 @@ public partial class UiShared : IDisposable
{
if (!success) return;
_isPenumbraDirectory = string.Equals(path.ToLowerInvariant(), _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(), StringComparison.Ordinal);
_isPenumbraDirectory = string.Equals(path.ToLowerInvariant(), _ipcManager.PenumbraModDirectory?.ToLowerInvariant(), StringComparison.Ordinal);
_isDirectoryWritable = IsDirectoryWritable(path);
_cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40);
_cacheDirectoryIsValidPath = Regex.IsMatch(path, @"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript);

View File

@@ -226,11 +226,13 @@ public class DalamudUtil : IDisposable
{
if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return;
Logger.Verbose($"Starting wait for {name} to draw");
var obj = (GameObject*)characterAddress;
const int tick = 250;
int curWaitTime = 0;
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something
while ((obj->DrawObject == null || (obj->RenderFlags & 0b100000000000) == 0b100000000000) && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something
{
Logger.Verbose($"Waiting for {name} to finish drawing");
curWaitTime += tick;

View File

@@ -0,0 +1,45 @@
using MareSynchronos.Models;
namespace MareSynchronos.Utils;
public class FileReplacementComparer : IEqualityComparer<FileReplacement>
{
public static FileReplacementComparer Instance => _instance;
private static FileReplacementComparer _instance = new();
private FileReplacementComparer() { }
public bool Equals(FileReplacement? x, FileReplacement? y)
{
if (x == null || y == null) return false;
return x.ResolvedPath.Equals(y.ResolvedPath) && CompareLists(x.GamePaths, y.GamePaths);
}
public int GetHashCode(FileReplacement obj)
{
return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths));
}
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked(hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
private bool CompareLists(List<string> list1, List<string> list2)
{
if (list1.Count != list2.Count)
return false;
for (int i = 0; i < list1.Count; i++)
{
if (!string.Equals(list1[i], list2[i], StringComparison.OrdinalIgnoreCase))
return false;
}
return true;
}
}