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 Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
using Penumbra.String; using Penumbra.String;
using Weapon = MareSynchronos.Interop.Weapon; using Weapon = MareSynchronos.Interop.Weapon;
using MareSynchronos.FileCache;
namespace MareSynchronos.Factories; namespace MareSynchronos.Factories;
@@ -19,23 +20,23 @@ public class CharacterDataFactory
private readonly DalamudUtil _dalamudUtil; private readonly DalamudUtil _dalamudUtil;
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly TransientResourceManager _transientResourceManager; 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)); Logger.Verbose("Creating " + nameof(CharacterDataFactory));
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_ipcManager = ipcManager; _ipcManager = ipcManager;
_transientResourceManager = transientResourceManager; _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) if (!_ipcManager.Initialized)
{ {
@@ -45,7 +46,16 @@ public class CharacterDataFactory
bool pointerIsZero = true; bool pointerIsZero = true;
try 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) catch (Exception ex)
{ {
@@ -67,17 +77,20 @@ public class CharacterDataFactory
try try
{ {
pathsToForwardResolve.Clear();
pathsToReverseResolve.Clear();
return CreateCharacterData(previousData, playerRelatedObject, token); return CreateCharacterData(previousData, playerRelatedObject, token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
Logger.Debug("Cancelled creating Character data"); Logger.Debug("Cancelled creating Character data");
throw;
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Failed to create " + playerRelatedObject.ObjectKind + " data"); Logger.Debug("Failed to create " + playerRelatedObject.ObjectKind + " data");
Logger.Warn(e.Message); Logger.Debug(e.Message);
Logger.Warn(e.StackTrace ?? string.Empty); Logger.Debug(e.StackTrace ?? string.Empty);
} }
previousData.FileReplacements = previousFileReplacements; previousData.FileReplacements = previousFileReplacements;
@@ -85,23 +98,7 @@ public class CharacterDataFactory
return previousData; return previousData;
} }
private (string, string) GetIndentationForInheritanceLevel(int inheritanceLevel) private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl)
{
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)
{ {
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{ {
@@ -115,26 +112,23 @@ public class CharacterDataFactory
} }
catch catch
{ {
Logger.Warn("Could not get model data for " + objectKind); Logger.Warn("Could not get model data");
return; return;
} }
Logger.Verbose("Checking File Replacement for Model " + mdlPath); Logger.Verbose("Checking File Replacement for Model " + mdlPath);
FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath); AddResolvePath(mdlPath);
DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel);
cache.AddFileReplacement(objectKind, mdlFileReplacement);
for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
{ {
var mtrl = (Material*)mdl->Materials[mtrlIdx]; var mtrl = (Material*)mdl->Materials[mtrlIdx];
if (mtrl == null) continue; 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; string fileName;
try try
@@ -144,25 +138,15 @@ public class CharacterDataFactory
} }
catch catch
{ {
Logger.Warn("Could not get material data for " + objectKind); Logger.Warn("Could not get material data");
return; return;
} }
Logger.Verbose("Checking File Replacement for Material " + fileName); Logger.Verbose("Checking File Replacement for Material " + fileName);
var mtrlPath = fileName.Split("|")[2]; 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); AddResolvePath(mtrlPath);
DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel);
cache.AddFileReplacement(objectKind, mtrlFileReplacement);
var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle;
for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++)
@@ -181,14 +165,14 @@ public class CharacterDataFactory
Logger.Verbose("Checking File Replacement for Texture " + texPath); Logger.Verbose("Checking File Replacement for Texture " + texPath);
AddReplacementsFromTexture(texPath, objectKind, cache, inheritanceLevel + 1); AddReplacementsFromTexture(texPath);
} }
try try
{ {
var shpkPath = "shader/sm5/shpk/" + new ByteString(mtrlResourceHandle->ShpkString).ToString(); var shpkPath = "shader/sm5/shpk/" + new ByteString(mtrlResourceHandle->ShpkString).ToString();
Logger.Verbose("Checking File Replacement for Shader " + shpkPath); Logger.Verbose("Checking File Replacement for Shader " + shpkPath);
AddReplacementsFromShader(shpkPath, objectKind, cache, inheritanceLevel + 1); AddReplacementsFromShader(shpkPath);
} }
catch catch
{ {
@@ -196,87 +180,51 @@ 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 (varPath.IsNullOrEmpty()) return;
if (cache.FileReplacements.ContainsKey(objectKind)) AddResolvePath(varPath, doNotReverseResolve);
{
if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(varPath, StringComparer.Ordinal)))
{
return;
}
} }
var variousReplacement = CreateFileReplacement(varPath, doNotReverseResolve); private void AddReplacementsFromShader(string shpkPath)
DebugPrint(variousReplacement, objectKind, "Various", inheritanceLevel);
cache.AddFileReplacement(objectKind, variousReplacement);
}
private void AddReplacementsFromShader(string shpkPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0)
{ {
if (string.IsNullOrEmpty(shpkPath)) return; if (string.IsNullOrEmpty(shpkPath)) return;
if (cache.FileReplacements.ContainsKey(objectKind)) AddResolvePath(shpkPath, doNotReverseResolve: true);
{
if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(shpkPath, StringComparer.Ordinal)))
{
return;
}
} }
var shpkFileReplacement = CreateFileReplacement(shpkPath, doNotReverseResolve: true); private void AddReplacementsFromTexture(string texPath, bool doNotReverseResolve = true)
DebugPrint(shpkFileReplacement, objectKind, "Shader", inheritanceLevel);
cache.AddFileReplacement(objectKind, shpkFileReplacement);
}
private void AddReplacementsFromTexture(string texPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = true)
{ {
if (string.IsNullOrEmpty(texPath)) return; if (string.IsNullOrEmpty(texPath)) return;
if (cache.FileReplacements.ContainsKey(objectKind)) AddResolvePath(texPath, doNotReverseResolve);
{
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);
if (texPath.Contains("/--", StringComparison.Ordinal)) return; if (texPath.Contains("/--", StringComparison.Ordinal)) return;
var texDx11Replacement = AddResolvePath(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve);
CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve);
DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel);
cache.AddFileReplacement(objectKind, texDx11Replacement);
} }
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 objectKind = playerRelatedObject.ObjectKind;
var charaPointer = playerRelatedObject.Address; 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(); Stopwatch st = Stopwatch.StartNew();
if (playerRelatedObject.HasUnprocessedUpdate)
{
Logger.Debug("Handling unprocessed update for " + objectKind); Logger.Debug("Handling unprocessed update for " + objectKind);
if (previousData.FileReplacements.ContainsKey(objectKind)) if (previousData.FileReplacements.ContainsKey(objectKind))
{ {
previousData.FileReplacements[objectKind].Clear(); previousData.FileReplacements[objectKind].Clear();
} }
else
{
previousData.FileReplacements.Add(objectKind, new());
}
var chara = _dalamudUtil.CreateGameObject(charaPointer)!; var chara = _dalamudUtil.CreateGameObject(charaPointer)!;
while (!DalamudUtil.IsObjectPresent(chara)) while (!DalamudUtil.IsObjectPresent(chara))
@@ -285,7 +233,7 @@ public class CharacterDataFactory
Thread.Sleep(50); Thread.Sleep(50);
} }
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); var human = (Human*)((Character*)charaPointer)->GameObject.DrawObject;
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{ {
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
@@ -296,29 +244,27 @@ public class CharacterDataFactory
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); AddReplacementsFromRenderModel(mdl);
}
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource(charaPointer, item);
} }
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human); AddPlayerSpecificReplacements(objectKind, charaPointer, human);
} }
if (objectKind == ObjectKind.Pet) if (objectKind == ObjectKind.Pet)
{ {
foreach (var item in previousData.FileReplacements[objectKind]) foreach (var item in previousData.FileReplacements[objectKind])
{ {
_transientResourceManager.AddSemiTransientResource(objectKind, item); _transientResourceManager.AddSemiTransientResource(objectKind, item.GamePaths.First());
} }
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.ManipulationString = _ipcManager.PenumbraGetMetaManipulations();
previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer); previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer);
@@ -327,52 +273,75 @@ public class CharacterDataFactory
previousData.PalettePlusPalette = _ipcManager.PalettePlusBuildPalette(); previousData.PalettePlusPalette = _ipcManager.PalettePlusBuildPalette();
Logger.Debug("Handling transient update for " + objectKind); 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(); st.Stop();
Logger.Verbose("Building " + objectKind + " Data took " + st.Elapsed); Logger.Verbose("Building " + objectKind + " Data took " + st.ElapsedMilliseconds + "ms");
return previousData; return previousData;
} }
private unsafe void ManageSemiTransientData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer) private Dictionary<string, List<string>> GetFileReplacementsFromPaths()
{ {
_transientResourceManager.PersistTransientResources(charaPointer, objectKind, CreateFileReplacement); var forwardPaths = pathsToForwardResolve.ToArray();
var reversePaths = pathsToReverseResolve.ToArray();
// get rid of items that have no file replacements anymore Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
foreach (var entry in previousData.FileReplacements.ToList()) 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());
}
}
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 else
{ {
Logger.Verbose("Found semi transient resource: " + item); resolvedPaths[filePath] = new List<string> { forwardPaths[i].ToLowerInvariant() };
previousData.FileReplacements[objectKind].Add(item);
}
}
} }
} }
private unsafe void AddPlayerSpecificReplacements(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer, Human* human) 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))
{
AddResolvePath(item, true);
}
}
private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human)
{ {
var weaponObject = (Weapon*)((Object*)human)->ChildObject; var weaponObject = (Weapon*)((Object*)human)->ChildObject;
@@ -380,42 +349,32 @@ public class CharacterDataFactory
{ {
var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel;
AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0); AddReplacementsFromRenderModel(mainHandWeapon);
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource(charaPointer, item);
}
foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject)) foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject))
{ {
Logger.Verbose("Found transient weapon resource: " + item); Logger.Verbose("Found transient weapon resource: " + item);
AddReplacement(item, objectKind, previousData, 1, doNotReverseResolve: true); AddReplacement(item, doNotReverseResolve: true);
} }
if (weaponObject->NextSibling != (IntPtr)weaponObject) if (weaponObject->NextSibling != (IntPtr)weaponObject)
{ {
var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel;
AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1); AddReplacementsFromRenderModel(offHandWeapon);
foreach (var item in previousData.FileReplacements[objectKind])
{
_transientResourceManager.RemoveTransientResource((IntPtr)offHandWeapon, item);
}
foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon))
{ {
Logger.Verbose("Found transient offhand weapon resource: " + item); 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 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 catch
{ {
@@ -423,43 +382,29 @@ public class CharacterDataFactory
} }
try 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 catch
{ {
Logger.Warn("Could not get Legacy Body Decal Data"); 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 raceSexIdString = raceSexId.ToString("0000");
string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb";
var replacement = CreateFileReplacement(skeletonPath, doNotReverseResolve: true); AddResolvePath(skeletonPath, doNotReverseResolve: true);
cache.AddFileReplacement(objectKind, replacement);
DebugPrint(replacement, objectKind, "SKLB", 0);
} }
private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false) private void AddResolvePath(string path, bool doNotReverseResolve = false)
{ {
var fileReplacement = _fileReplacementFactory.Create(); if (doNotReverseResolve) pathsToForwardResolve.Add(path.ToLowerInvariant());
if (!doNotReverseResolve) else pathsToReverseResolve.Add(path.ToLowerInvariant());
{
fileReplacement.ReverseResolvePath(path);
}
else
{
fileReplacement.ResolvePath(path);
} }
return fileReplacement; 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) public FileCacheEntity? GetFileCacheByPath(string path)
{ {
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); 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 entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
if (entry == null) if (entry == null)
{ {
@@ -140,8 +140,8 @@ public class FileCacheManager : IDisposable
FileInfo fi = new(path); FileInfo fi = new(path);
if (!fi.Exists) return null; if (!fi.Exists) return null;
var fullName = fi.FullName.ToLowerInvariant(); var fullName = fi.FullName.ToLowerInvariant();
if (!fullName.Contains(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), StringComparison.Ordinal)) return null; if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath); return CreateFileCacheEntity(fi, prefixedPath);
} }
@@ -203,7 +203,7 @@ public class FileCacheManager : IDisposable
{ {
if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase)) 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)) else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase))
{ {

View File

@@ -150,7 +150,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
private void PeriodicFileScan(CancellationToken ct) private void PeriodicFileScan(CancellationToken ct)
{ {
TotalFiles = 1; TotalFiles = 1;
var penumbraDir = _ipcManager.PenumbraModDirectory(); var penumbraDir = _ipcManager.PenumbraModDirectory;
bool penDirExists = true; bool penDirExists = true;
bool cacheDirExists = true; bool cacheDirExists = true;
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir)) 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 IpcManager _ipcManager;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private API.Data.CharacterData _cachedData = new(); private API.Data.CharacterData _cachedData = new();
private PlayerRelatedObject? _currentCharacterEquipment; private GameObjectHandler? _currentCharacterEquipment;
private CancellationTokenSource? _downloadCancellationTokenSource = new(); private CancellationTokenSource? _downloadCancellationTokenSource = new();
private bool _isVisible; private bool _isVisible;
@@ -65,6 +65,18 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
Logger.Debug("Checking for files to download for player " + PlayerName); 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); 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; if (string.Equals(characterData.DataHash.Value, _cachedData.DataHash.Value, StringComparison.Ordinal) && !forced) return;
bool updateModdedPaths = false; bool updateModdedPaths = false;
@@ -116,6 +128,14 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
if (objectKind == ObjectKind.Player) 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; bool heelsOffsetDifferent = _cachedData.HeelsOffset != characterData.HeelsOffset;
if (heelsOffsetDifferent) if (heelsOffsetDifferent)
{ {
@@ -189,8 +209,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
return false; return false;
} }
_currentCharacterEquipment?.CheckAndUpdateObject(); if (_currentCharacterEquipment?.CheckAndUpdateObject() ?? false)
if (_currentCharacterEquipment?.HasUnprocessedUpdate ?? false)
{ {
OnPlayerChanged(); OnPlayerChanged();
} }
@@ -247,8 +266,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<PenumbraRedrawMessage>(this, (msg) => IpcManagerOnPenumbraRedrawEvent(((PenumbraRedrawMessage)msg))); Mediator.Subscribe<PenumbraRedrawMessage>(this, (msg) => IpcManagerOnPenumbraRedrawEvent(((PenumbraRedrawMessage)msg)));
_originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); _originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter);
_currentCharacterEquipment = new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, _currentCharacterEquipment = new GameObjectHandler(Mediator, ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, false);
() => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero);
} }
public override string ToString() public override string ToString()
@@ -437,7 +455,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
{ {
PlayerCharacter = msg.Address; PlayerCharacter = msg.Address;
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); cts.CancelAfter(TimeSpan.FromSeconds(10));
_dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName!, PlayerCharacter, 10000, cts.Token); _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName!, PlayerCharacter, 10000, cts.Token);
cts.Dispose(); cts.Dispose();
cts = new CancellationTokenSource(); cts = new CancellationTokenSource();
@@ -460,7 +478,6 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
private void OnPlayerChanged() private void OnPlayerChanged()
{ {
Logger.Debug($"Player {PlayerName} changed, PenumbraRedraw is {RequestedPenumbraRedraw}"); Logger.Debug($"Player {PlayerName} changed, PenumbraRedraw is {RequestedPenumbraRedraw}");
_currentCharacterEquipment!.HasUnprocessedUpdate = false;
if (!RequestedPenumbraRedraw && PlayerCharacter != IntPtr.Zero) if (!RequestedPenumbraRedraw && PlayerCharacter != IntPtr.Zero)
{ {
Logger.Debug($"Saving new Glamourer data"); 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> _penumbraResolvePlayer;
private readonly FuncSubscriber<string, string[]> _reverseResolvePlayer; private readonly FuncSubscriber<string, string[]> _reverseResolvePlayer;
private readonly FuncSubscriber<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; 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<nint, string, string> _penumbraGameObjectResourcePathResolved;
private readonly EventSubscriber<ModSettingChange, string, string, bool> _penumbraModSettingChanged;
private readonly ICallGateSubscriber<string> _heelsGetApiVersion; private readonly ICallGateSubscriber<string> _heelsGetApiVersion;
private readonly ICallGateSubscriber<float> _heelsGetOffset; private readonly ICallGateSubscriber<float> _heelsGetOffset;
@@ -81,9 +82,10 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_penumbraRemoveTemporaryCollection = Penumbra.Api.Ipc.RemoveTemporaryCollectionByName.Subscriber(pi); _penumbraRemoveTemporaryCollection = Penumbra.Api.Ipc.RemoveTemporaryCollectionByName.Subscriber(pi);
_penumbraRemoveTemporaryMod = Penumbra.Api.Ipc.RemoveTemporaryMod.Subscriber(pi); _penumbraRemoveTemporaryMod = Penumbra.Api.Ipc.RemoveTemporaryMod.Subscriber(pi);
_penumbraAssignTemporaryCollection = Penumbra.Api.Ipc.AssignTemporaryCollection.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)); _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"); _glamourerApiVersion = pi.GetIpcSubscriber<int>("Glamourer.ApiVersion");
_glamourerGetAllCustomization = pi.GetIpcSubscriber<GameObject?, string>("Glamourer.GetAllCustomizationFromCharacter"); _glamourerGetAllCustomization = pi.GetIpcSubscriber<GameObject?, string>("Glamourer.GetAllCustomizationFromCharacter");
@@ -125,6 +127,12 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => HandleActionQueue()); Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => HandleActionQueue());
Mediator.Subscribe<GposeFrameworkUpdateMessage>(this, (_) => HandleGposeActionQueue()); Mediator.Subscribe<GposeFrameworkUpdateMessage>(this, (_) => HandleGposeActionQueue());
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ClearActionQueue()); Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ClearActionQueue());
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => CheckPenumbraModPath());
}
private void CheckPenumbraModPath()
{
PenumbraModDirectory = GetPenumbraModDirectory();
} }
private void HandleGposeActionQueue() private void HandleGposeActionQueue()
@@ -142,11 +150,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_inGposeQueueMode = on; _inGposeQueueMode = on;
} }
private void PenumbraModSettingChangedHandler()
{
Mediator.Publish(new PenumbraModSettingChangedMessage());
}
private void ClearActionQueue() private void ClearActionQueue()
{ {
ActionQueue.Clear(); ActionQueue.Clear();
@@ -191,7 +194,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
{ {
try try
{ {
return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 17 }; return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 19 } && _penumbraEnabled.Invoke();
} }
catch catch
{ {
@@ -259,7 +262,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_penumbraDispose.Dispose(); _penumbraDispose.Dispose();
_penumbraInit.Dispose(); _penumbraInit.Dispose();
_penumbraObjectIsRedrawn.Dispose(); _penumbraObjectIsRedrawn.Dispose();
_penumbraModSettingChanged.Dispose();
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
} }
@@ -411,7 +413,9 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
return _penumbraGetMetaManipulations.Invoke(); return _penumbraGetMetaManipulations.Invoke();
} }
public string? PenumbraModDirectory() public string? PenumbraModDirectory;
public string? GetPenumbraModDirectory()
{ {
if (!CheckPenumbraApi()) return null; if (!CheckPenumbraApi()) return null;
return _penumbraResolveModDir!.Invoke().ToLowerInvariant(); 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) private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
{ {
Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex)); Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex));

View File

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

View File

@@ -11,18 +11,17 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
{ {
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly DalamudUtil _dalamudUtil; private readonly DalamudUtil _dalamudUtil;
private readonly PlayerManager _playerManager;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly PairManager _pairManager; 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) FileCacheManager fileDbManager, PairManager pairManager, MareMediator mediator) : base(mediator)
{ {
Logger.Verbose("Creating " + nameof(OnlinePlayerManager)); Logger.Verbose("Creating " + nameof(OnlinePlayerManager));
_apiController = apiController; _apiController = apiController;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_playerManager = playerManager;
_fileDbManager = fileDbManager; _fileDbManager = fileDbManager;
_pairManager = pairManager; _pairManager = pairManager;
@@ -30,6 +29,20 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut()); Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate()); 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) private void PlayerManagerOnPlayerHasChanged(PlayerChangedMessage msg)
@@ -78,11 +91,11 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
private void PushCharacterData(List<UserData> visiblePlayers) private void PushCharacterData(List<UserData> visiblePlayers)
{ {
if (visiblePlayers.Any() && _playerManager.LastCreatedCharacterData != null) if (visiblePlayers.Any() && _lastSentData != null)
{ {
Task.Run(async () => 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.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Utility; using Dalamud.Utility;
using MareSynchronos.API.Data; 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.API.Data.Enum;
using MareSynchronos.Factories;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator; using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Utils; using MareSynchronos.Utils;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -16,13 +14,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
public IntPtr[] PlayerRelatedPointers = Array.Empty<IntPtr>(); public IntPtr[] PlayerRelatedPointers = Array.Empty<IntPtr>();
private readonly string[] _fileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk" }; 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 string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId;
private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new(); private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
private ConcurrentDictionary<ObjectKind, HashSet<FileReplacement>> SemiTransientResources { get; } = new(); private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources { get; } = new();
public TransientResourceManager(ConfigurationService configurationService, DalamudUtil dalamudUtil, FileReplacementFactory fileReplacementFactory, MareMediator mediator) : base(mediator) public TransientResourceManager(ConfigurationService configurationService, DalamudUtil dalamudUtil, MareMediator mediator) : base(mediator)
{ {
_configurationService = configurationService; _configurationService = configurationService;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -32,32 +28,19 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate()); Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
Mediator.Subscribe<ClassJobChangedMessage>(this, (_) => DalamudUtil_ClassJobChanged()); Mediator.Subscribe<ClassJobChangedMessage>(this, (_) => DalamudUtil_ClassJobChanged());
Mediator.Subscribe<PlayerRelatedObjectPointerUpdateMessage>(this, (msg) => PlayerRelatedPointers = ((PlayerRelatedObjectPointerUpdateMessage)msg).RelatedObjects); 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)) if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var linesInConfig))
{ {
int restored = 0; int restored = 0;
foreach (var file in linesInConfig) foreach (var file in linesInConfig)
{ {
try try
{
var fileReplacement = fileReplacementFactory.Create();
fileReplacement.ResolvePath(file);
if (fileReplacement.HasFileReplacement)
{ {
Logger.Debug("Loaded persistent transient resource " + file); Logger.Debug("Loaded persistent transient resource " + file);
SemiTransientResources[ObjectKind.Player].Add(fileReplacement); SemiTransientResources[ObjectKind.Player].Add(file);
restored++; restored++;
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Logger.Warn("Error during loading persistent transient resource " + file, ex); Logger.Warn("Error during loading persistent transient resource " + file, ex);
@@ -70,19 +53,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
private void Manager_PenumbraModSettingChanged() private void Manager_PenumbraModSettingChanged()
{ {
bool successfulValidation = true;
Task.Run(() => Task.Run(() =>
{ {
Logger.Debug("Penumbra Mod Settings changed, verifying SemiTransientResources"); Logger.Debug("Penumbra Mod Settings changed, verifying SemiTransientResources");
foreach (var item in 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>(); return new List<string>();
} }
public List<FileReplacement> GetSemiTransientResources(ObjectKind objectKind) public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
{ {
if (SemiTransientResources.TryGetValue(objectKind, out var result)) 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) private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
{ {
var gamePath = msg.GamePath; var gamePath = msg.GamePath.ToLowerInvariant();
var gameObject = msg.GameObject; var gameObject = msg.GameObject;
var filePath = msg.FilePath; var filePath = msg.FilePath;
if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) 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 (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) return;
if (TransientResources[gameObject].Contains(replacedGamePath) || if (TransientResources[gameObject].Contains(replacedGamePath) ||
SemiTransientResources.Any(r => r.Value.Any(f => SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))))
string.Equals(f.GamePaths.First(), replacedGamePath, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase))
))
{ {
Logger.Verbose("Not adding " + replacedGamePath + ":" + filePath); 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 else
{ {
@@ -183,19 +153,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
} }
} }
public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind)
{
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)
{ {
if (!SemiTransientResources.ContainsKey(objectKind)) if (!SemiTransientResources.ContainsKey(objectKind))
{ {
SemiTransientResources[objectKind] = new HashSet<FileReplacement>(); SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
} }
if (!TransientResources.TryGetValue(gameObject, out var resources)) if (!TransientResources.TryGetValue(gameObject, out var resources))
@@ -203,47 +165,16 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
return; return;
} }
SemiTransientResources[objectKind].RemoveWhere(p => !p.Verify());
var transientResources = resources.ToList(); var transientResources = resources.ToList();
Logger.Debug("Persisting " + transientResources.Count + " transient resources"); Logger.Debug("Persisting " + transientResources.Count + " transient resources");
foreach (var gamePath in transientResources) foreach (var gamePath in transientResources)
{ {
var existingResource = SemiTransientResources[objectKind].Any(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase)); SemiTransientResources[objectKind].Add(gamePath);
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);
}
} }
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements))
{ {
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements;
= fileReplacements.SelectMany(p => p.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToHashSet(StringComparer.OrdinalIgnoreCase);
_configurationService.Save(); _configurationService.Save();
} }
TransientResources[gameObject].Clear(); TransientResources[gameObject].Clear();
@@ -256,22 +187,26 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
SemiTransientResources.Clear(); SemiTransientResources.Clear();
if (SemiTransientResources.ContainsKey(ObjectKind.Player)) if (SemiTransientResources.ContainsKey(ObjectKind.Player))
{ {
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player];
= SemiTransientResources[ObjectKind.Player].SelectMany(p => p.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToHashSet(StringComparer.OrdinalIgnoreCase);
_configurationService.Save(); _configurationService.Save();
} }
} }
internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) internal void AddSemiTransientResource(ObjectKind objectKind, string item)
{ {
if (!SemiTransientResources.ContainsKey(objectKind)) 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)
{ {
SemiTransientResources[objectKind].Add(item); if (TransientResources.TryGetValue(ptr, out var set))
{
set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Text; using System.Text;
using MareSynchronos.Utils;
using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
@@ -10,7 +9,7 @@ namespace MareSynchronos.Models;
public class CharacterData public class CharacterData
{ {
[JsonProperty] [JsonProperty]
public Dictionary<ObjectKind, List<FileReplacement>> FileReplacements { get; set; } = new(); public Dictionary<ObjectKind, HashSet<FileReplacement>> FileReplacements { get; set; } = new();
[JsonProperty] [JsonProperty]
public Dictionary<ObjectKind, string> GlamourerString { get; set; } = new(); public Dictionary<ObjectKind, string> GlamourerString { get; set; } = new();
@@ -33,7 +32,7 @@ public class CharacterData
{ {
if (!fileReplacement.HasFileReplacement) return; 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)); var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => string.Equals(f.ResolvedPath, fileReplacement.ResolvedPath, StringComparison.OrdinalIgnoreCase));
if (existingReplacement != null) if (existingReplacement != null)
@@ -57,16 +56,9 @@ public class CharacterData
}; };
}).ToList()); }).ToList());
Logger.Debug("Adding fileSwaps");
foreach (var item in FileReplacements) foreach (var item in FileReplacements)
{ {
Logger.Debug("Checking fileSwaps for " + item.Key);
var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); 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); 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.FileCache;
using MareSynchronos.Managers;
using MareSynchronos.Utils;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
namespace MareSynchronos.Models; namespace MareSynchronos.Models;
public class FileReplacement public class FileReplacement
{ {
private readonly FileCacheManager _fileDbManager; public FileReplacement(List<string> gamePaths, string filePath, FileCacheManager fileDbManager)
private readonly IpcManager _ipcManager;
public FileReplacement(FileCacheManager fileDbManager, IpcManager ipcManager)
{ {
_fileDbManager = fileDbManager; GamePaths = gamePaths.Select(g => g.Replace('\\', '/')).ToList();
_ipcManager = ipcManager; ResolvedPath = filePath.Replace('\\', '/');
HashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty);
} }
public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); 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 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 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) public string ResolvedPath { get; init; } = string.Empty;
{
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 FileReplacementData ToFileReplacementDto() public FileReplacementData ToFileReplacementDto()
{ {
@@ -87,20 +39,6 @@ public class FileReplacement
public override string ToString() public override string ToString()
{ {
StringBuilder builder = new(); return $"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}";
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));
} }
} }

View File

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

View File

@@ -59,7 +59,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PairManager>(); collection.AddSingleton<PairManager>();
collection.AddSingleton<ApiController>(); collection.AddSingleton<ApiController>();
collection.AddSingleton<PeriodicFileScanner>(); collection.AddSingleton<PeriodicFileScanner>();
collection.AddSingleton<FileReplacementFactory>();
collection.AddSingleton<MareCharaFileManager>(); collection.AddSingleton<MareCharaFileManager>();
collection.AddSingleton<NotificationService>(); collection.AddSingleton<NotificationService>();
@@ -70,9 +69,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IntroUi>(); collection.AddSingleton<IntroUi>();
collection.AddSingleton<DownloadUi>(); collection.AddSingleton<DownloadUi>();
collection.AddScoped<CacheCreationService>();
collection.AddScoped<TransientResourceManager>(); collection.AddScoped<TransientResourceManager>();
collection.AddScoped<CharacterDataFactory>(); collection.AddScoped<CharacterDataFactory>();
collection.AddScoped<PlayerManager>();
collection.AddScoped<OnlinePlayerManager>(); collection.AddScoped<OnlinePlayerManager>();
var serviceProvider = collection.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true, ValidateScopes = true }); 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<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeStartMessage>(this, (_) => UiShared_GposeStart()); Mediator.Subscribe<GposeStartMessage>(this, (_) => UiShared_GposeStart());
Mediator.Subscribe<GposeEndMessage>(this, (_) => UiShared_GposeEnd()); 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); windowSystem.AddWindow(this);
} }

View File

@@ -38,7 +38,7 @@ public partial class UiShared : IDisposable
public string PlayerName => _dalamudUtil.PlayerName; public string PlayerName => _dalamudUtil.PlayerName;
public uint WorldId => _dalamudUtil.WorldId; public uint WorldId => _dalamudUtil.WorldId;
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value; 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 bool EditTrackerPosition { get; set; }
public ImFontPtr UidFont { get; private set; } public ImFontPtr UidFont { get; private set; }
public bool UidFontBuilt { get; private set; } public bool UidFontBuilt { get; private set; }
@@ -468,7 +468,7 @@ public partial class UiShared : IDisposable
{ {
if (!success) return; 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); _isDirectoryWritable = IsDirectoryWritable(path);
_cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40); _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); _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; if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return;
Logger.Verbose($"Starting wait for {name} to draw");
var obj = (GameObject*)characterAddress; var obj = (GameObject*)characterAddress;
const int tick = 250; const int tick = 250;
int curWaitTime = 0; int curWaitTime = 0;
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop // 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"); Logger.Verbose($"Waiting for {name} to finish drawing");
curWaitTime += tick; 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;
}
}