forked from Eauldane/SnowcloakClient
Initial
This commit is contained in:
50
MareSynchronos/PlayerData/Data/CharacterData.cs
Normal file
50
MareSynchronos/PlayerData/Data/CharacterData.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using MareSynchronos.API.Data;
|
||||
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Data;
|
||||
|
||||
public class CharacterData
|
||||
{
|
||||
public Dictionary<ObjectKind, string> CustomizePlusScale { get; set; } = [];
|
||||
public Dictionary<ObjectKind, HashSet<FileReplacement>> FileReplacements { get; set; } = [];
|
||||
public Dictionary<ObjectKind, string> GlamourerString { get; set; } = [];
|
||||
public string HeelsData { get; set; } = string.Empty;
|
||||
public string HonorificData { get; set; } = string.Empty;
|
||||
public string ManipulationString { get; set; } = string.Empty;
|
||||
public string PetNamesData { get; set; } = string.Empty;
|
||||
public string MoodlesData { get; set; } = string.Empty;
|
||||
|
||||
public API.Data.CharacterData ToAPI()
|
||||
{
|
||||
Dictionary<ObjectKind, List<FileReplacementData>> fileReplacements =
|
||||
FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap)
|
||||
.GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g =>
|
||||
{
|
||||
return new FileReplacementData()
|
||||
{
|
||||
GamePaths = g.SelectMany(f => f.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
|
||||
Hash = g.First().Hash,
|
||||
};
|
||||
}).ToList());
|
||||
|
||||
foreach (var item in FileReplacements)
|
||||
{
|
||||
var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto());
|
||||
fileReplacements[item.Key].AddRange(fileSwapsToAdd);
|
||||
}
|
||||
|
||||
return new API.Data.CharacterData()
|
||||
{
|
||||
FileReplacements = fileReplacements,
|
||||
GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value),
|
||||
ManipulationData = ManipulationString,
|
||||
HeelsData = HeelsData,
|
||||
CustomizePlusData = CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value),
|
||||
HonorificData = HonorificData,
|
||||
PetNamesData = PetNamesData,
|
||||
MoodlesData = MoodlesData
|
||||
};
|
||||
}
|
||||
}
|
42
MareSynchronos/PlayerData/Data/FileReplacement.cs
Normal file
42
MareSynchronos/PlayerData/Data/FileReplacement.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using MareSynchronos.API.Data;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Data;
|
||||
|
||||
public partial class FileReplacement
|
||||
{
|
||||
public FileReplacement(string[] gamePaths, string filePath)
|
||||
{
|
||||
GamePaths = gamePaths.Select(g => g.Replace('\\', '/').ToLowerInvariant()).ToHashSet(StringComparer.Ordinal);
|
||||
ResolvedPath = filePath.Replace('\\', '/');
|
||||
}
|
||||
|
||||
public HashSet<string> GamePaths { get; init; }
|
||||
|
||||
public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal));
|
||||
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
public bool IsFileSwap => !LocalPathRegex().IsMatch(ResolvedPath) && GamePaths.All(p => !LocalPathRegex().IsMatch(p));
|
||||
public string ResolvedPath { get; init; }
|
||||
|
||||
public FileReplacementData ToFileReplacementDto()
|
||||
{
|
||||
return new FileReplacementData
|
||||
{
|
||||
GamePaths = [.. GamePaths],
|
||||
Hash = Hash,
|
||||
FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HasReplacement:{HasFileReplacement},IsFileSwap:{IsFileSwap} - {string.Join(",", GamePaths)} => {ResolvedPath}";
|
||||
}
|
||||
|
||||
#pragma warning disable MA0009
|
||||
[GeneratedRegex(@"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript)]
|
||||
private static partial Regex LocalPathRegex();
|
||||
#pragma warning restore MA0009
|
||||
}
|
47
MareSynchronos/PlayerData/Data/FileReplacementComparer.cs
Normal file
47
MareSynchronos/PlayerData/Data/FileReplacementComparer.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace MareSynchronos.PlayerData.Data;
|
||||
|
||||
public class FileReplacementComparer : IEqualityComparer<FileReplacement>
|
||||
{
|
||||
private static readonly FileReplacementComparer _instance = new();
|
||||
|
||||
private FileReplacementComparer()
|
||||
{ }
|
||||
|
||||
public static FileReplacementComparer Instance => _instance;
|
||||
|
||||
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 bool CompareLists(HashSet<string> list1, HashSet<string> list2)
|
||||
{
|
||||
if (list1.Count != list2.Count)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < list1.Count; i++)
|
||||
{
|
||||
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source) where T : notnull
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (T element in source)
|
||||
{
|
||||
hash = unchecked(hash +
|
||||
EqualityComparer<T>.Default.GetHashCode(element));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
using MareSynchronos.API.Data;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Data;
|
||||
|
||||
public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData>
|
||||
{
|
||||
private static readonly FileReplacementDataComparer _instance = new();
|
||||
|
||||
private FileReplacementDataComparer()
|
||||
{ }
|
||||
|
||||
public static FileReplacementDataComparer Instance => _instance;
|
||||
|
||||
public bool Equals(FileReplacementData? x, FileReplacementData? y)
|
||||
{
|
||||
if (x == null || y == null) return false;
|
||||
return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int GetHashCode(FileReplacementData obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath));
|
||||
}
|
||||
|
||||
private static bool CompareHashSets(HashSet<string> list1, HashSet<string> list2)
|
||||
{
|
||||
if (list1.Count != list2.Count)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < list1.Count; i++)
|
||||
{
|
||||
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source) where T : notnull
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (T element in source)
|
||||
{
|
||||
hash = unchecked(hash +
|
||||
EqualityComparer<T>.Default.GetHashCode(element));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
14
MareSynchronos/PlayerData/Data/PlayerChanges.cs
Normal file
14
MareSynchronos/PlayerData/Data/PlayerChanges.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace MareSynchronos.PlayerData.Pairs;
|
||||
|
||||
public enum PlayerChanges
|
||||
{
|
||||
ModFiles = 1,
|
||||
ModManip = 2,
|
||||
Glamourer = 3,
|
||||
Customize = 4,
|
||||
Heels = 5,
|
||||
Honorific = 7,
|
||||
ForcedRedraw = 8,
|
||||
Moodles = 9,
|
||||
PetNames = 10,
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Factories;
|
||||
|
||||
public class FileDownloadManagerFactory
|
||||
{
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MareMediator _mareMediator;
|
||||
|
||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mareMediator = mareMediator;
|
||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_fileCompactor = fileCompactor;
|
||||
}
|
||||
|
||||
public FileDownloadManager Create()
|
||||
{
|
||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Factories;
|
||||
|
||||
public class GameObjectHandlerFactory
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
|
||||
public GameObjectHandlerFactory(ILoggerFactory loggerFactory, PerformanceCollectorService performanceCollectorService, MareMediator mareMediator,
|
||||
DalamudUtilService dalamudUtilService)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
_mareMediator = mareMediator;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
}
|
||||
|
||||
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||
{
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(_loggerFactory.CreateLogger<GameObjectHandler>(),
|
||||
_performanceCollectorService, _mareMediator, _dalamudUtilService, objectKind, getAddressFunc, isWatched)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
30
MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs
Normal file
30
MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Factories;
|
||||
|
||||
public class PairAnalyzerFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
|
||||
public PairAnalyzerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator,
|
||||
FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_mareMediator = mareMediator;
|
||||
_modelAnalyzer = modelAnalyzer;
|
||||
}
|
||||
|
||||
public PairAnalyzer Create(Pair pair)
|
||||
{
|
||||
return new PairAnalyzer(_loggerFactory.CreateLogger<PairAnalyzer>(), pair, _mareMediator,
|
||||
_fileCacheManager, _modelAnalyzer);
|
||||
}
|
||||
}
|
33
MareSynchronos/PlayerData/Factories/PairFactory.cs
Normal file
33
MareSynchronos/PlayerData/Factories/PairFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Factories;
|
||||
|
||||
public class PairFactory
|
||||
{
|
||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly MareConfigService _mareConfig;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory,
|
||||
MareMediator mareMediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_cachedPlayerFactory = cachedPlayerFactory;
|
||||
_mareMediator = mareMediator;
|
||||
_mareConfig = mareConfig;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
public Pair Create(UserData userData)
|
||||
{
|
||||
return new Pair(_loggerFactory.CreateLogger<Pair>(), userData, _cachedPlayerFactory, _mareMediator, _mareConfig, _serverConfigurationManager);
|
||||
}
|
||||
}
|
62
MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs
Normal file
62
MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Interop.Ipc;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Factories;
|
||||
|
||||
public class PairHandlerFactory
|
||||
{
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
||||
private readonly VisibilityService _visibilityService;
|
||||
private readonly NoSnapService _noSnapService;
|
||||
|
||||
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
||||
ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory,
|
||||
MareConfigService configService, VisibilityService visibilityService, NoSnapService noSnapService)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_mareMediator = mareMediator;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_pairAnalyzerFactory = pairAnalyzerFactory;
|
||||
_configService = configService;
|
||||
_visibilityService = visibilityService;
|
||||
_noSnapService = noSnapService;
|
||||
}
|
||||
|
||||
public PairHandler Create(Pair pair)
|
||||
{
|
||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService, _noSnapService);
|
||||
}
|
||||
}
|
365
MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs
Normal file
365
MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs
Normal file
@@ -0,0 +1,365 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Interop.Ipc;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Data;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CharacterData = MareSynchronos.PlayerData.Data.CharacterData;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Factories;
|
||||
|
||||
public class PlayerDataFactory
|
||||
{
|
||||
private static readonly string[] _allowedExtensionsForGamePaths = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILogger<PlayerDataFactory> _logger;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly TransientResourceManager _transientResourceManager;
|
||||
|
||||
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
|
||||
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
|
||||
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_ipcManager = ipcManager;
|
||||
_transientResourceManager = transientResourceManager;
|
||||
_fileCacheManager = fileReplacementFactory;
|
||||
_performanceCollector = performanceCollector;
|
||||
_modelAnalyzer = modelAnalyzer;
|
||||
_mareMediator = mareMediator;
|
||||
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
||||
}
|
||||
|
||||
public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||
{
|
||||
if (!_ipcManager.Initialized)
|
||||
{
|
||||
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
||||
}
|
||||
|
||||
if (playerRelatedObject == null) return;
|
||||
|
||||
bool pointerIsZero = true;
|
||||
try
|
||||
{
|
||||
pointerIsZero = playerRelatedObject.Address == IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
pointerIsZero = await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
pointerIsZero = true;
|
||||
_logger.LogDebug("NullRef for {object}", playerRelatedObject);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not create data for {object}", playerRelatedObject);
|
||||
}
|
||||
|
||||
if (pointerIsZero)
|
||||
{
|
||||
_logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
|
||||
previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind);
|
||||
previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind);
|
||||
previousData.CustomizePlusScale.Remove(playerRelatedObject.ObjectKind);
|
||||
return;
|
||||
}
|
||||
|
||||
var previousFileReplacements = previousData.FileReplacements.ToDictionary(d => d.Key, d => d.Value);
|
||||
var previousGlamourerData = previousData.GlamourerString.ToDictionary(d => d.Key, d => d.Value);
|
||||
var previousCustomize = previousData.CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value);
|
||||
|
||||
try
|
||||
{
|
||||
await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
|
||||
{
|
||||
await CreateCharacterData(previousData, playerRelatedObject, token).ConfigureAwait(false);
|
||||
}).ConfigureAwait(true);
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogDebug("Cancelled creating Character data for {object}", playerRelatedObject);
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject);
|
||||
}
|
||||
|
||||
previousData.FileReplacements = previousFileReplacements;
|
||||
previousData.GlamourerString = previousGlamourerData;
|
||||
previousData.CustomizePlusScale = previousCustomize;
|
||||
}
|
||||
|
||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||
{
|
||||
return ((Character*)playerPointer)->GameObject.DrawObject == null;
|
||||
}
|
||||
|
||||
private async Task<CharacterData> CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||
{
|
||||
var objectKind = playerRelatedObject.ObjectKind;
|
||||
|
||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||
|
||||
if (!previousData.FileReplacements.TryGetValue(objectKind, out HashSet<FileReplacement>? value))
|
||||
{
|
||||
previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
value.Clear();
|
||||
}
|
||||
|
||||
previousData.CustomizePlusScale.Remove(objectKind);
|
||||
|
||||
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false);
|
||||
int totalWaitTime = 10000;
|
||||
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
|
||||
{
|
||||
_logger.LogTrace("Character is null but it shouldn't be, waiting");
|
||||
await Task.Delay(50, token).ConfigureAwait(false);
|
||||
totalWaitTime -= 50;
|
||||
}
|
||||
Dictionary<string, List<ushort>>? boneIndices =
|
||||
objectKind != ObjectKind.Player
|
||||
? null
|
||||
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||
|
||||
DateTime start = DateTime.UtcNow;
|
||||
|
||||
// penumbra call, it's currently broken
|
||||
Dictionary<string, HashSet<string>>? resolvedPaths;
|
||||
|
||||
resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
|
||||
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
|
||||
|
||||
previousData.FileReplacements[objectKind] =
|
||||
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
||||
.Where(p => p.HasFileReplacement).ToHashSet();
|
||||
previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !_allowedExtensionsForGamePaths.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
}
|
||||
|
||||
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
|
||||
// or we get into redraw city for every change and nothing works properly
|
||||
if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
||||
{
|
||||
_logger.LogDebug("Persisting {item}", item);
|
||||
_transientResourceManager.AddSemiTransientResource(objectKind, item);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
|
||||
|
||||
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
|
||||
_transientResourceManager.ClearTransientPaths(playerRelatedObject.Address, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList());
|
||||
|
||||
// get all remaining paths and resolve them
|
||||
var transientPaths = ManageSemiTransientData(objectKind, playerRelatedObject.Address);
|
||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
previousData.FileReplacements[objectKind].Add(replacement);
|
||||
}
|
||||
|
||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. previousData.FileReplacements[objectKind]]);
|
||||
|
||||
// make sure we only return data that actually has file replacements
|
||||
foreach (var item in previousData.FileReplacements)
|
||||
{
|
||||
previousData.FileReplacements[item.Key] = new HashSet<FileReplacement>(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
|
||||
}
|
||||
|
||||
// gather up data from ipc
|
||||
previousData.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
||||
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
|
||||
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
|
||||
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
|
||||
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
|
||||
previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false);
|
||||
_logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]);
|
||||
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
||||
previousData.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty;
|
||||
_logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale[playerRelatedObject.ObjectKind]);
|
||||
previousData.HonorificData = await getHonorificTitle.ConfigureAwait(false);
|
||||
_logger.LogDebug("Honorific is now: {data}", previousData.HonorificData);
|
||||
previousData.HeelsData = await getHeelsOffset.ConfigureAwait(false);
|
||||
_logger.LogDebug("Heels is now: {heels}", previousData.HeelsData);
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
previousData.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
||||
_logger.LogDebug("Pet Nicknames is now: {petnames}", previousData.PetNamesData);
|
||||
previousData.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
|
||||
}
|
||||
|
||||
if (previousData.FileReplacements.TryGetValue(objectKind, out HashSet<FileReplacement>? fileReplacements))
|
||||
{
|
||||
var toCompute = fileReplacements.Where(f => !f.IsFileSwap).ToArray();
|
||||
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
|
||||
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
|
||||
foreach (var file in toCompute)
|
||||
{
|
||||
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
||||
}
|
||||
var removed = fileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
||||
if (removed > 0)
|
||||
{
|
||||
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
||||
}
|
||||
}
|
||||
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
try
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, previousData, objectKind).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Failed to verify player animations, continuing without further verification");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
|
||||
|
||||
return previousData;
|
||||
}
|
||||
|
||||
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterData previousData, ObjectKind objectKind)
|
||||
{
|
||||
if (boneIndices == null) return;
|
||||
|
||||
foreach (var kvp in boneIndices)
|
||||
{
|
||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||
}
|
||||
|
||||
if (boneIndices.All(u => u.Value.Count == 0)) return;
|
||||
|
||||
int noValidationFailed = 0;
|
||||
foreach (var file in previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||
{
|
||||
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
|
||||
bool validationFailed = false;
|
||||
if (skeletonIndices != null)
|
||||
{
|
||||
// 105 is the maximum vanilla skellington spoopy bone index
|
||||
if (skeletonIndices.All(k => k.Value.Max() <= 105))
|
||||
{
|
||||
_logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||
|
||||
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
|
||||
{
|
||||
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
|
||||
{
|
||||
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
||||
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
|
||||
validationFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validationFailed)
|
||||
{
|
||||
noValidationFailed++;
|
||||
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
|
||||
previousData.FileReplacements[objectKind].Remove(file);
|
||||
foreach (var gamePath in file.GamePaths)
|
||||
{
|
||||
_transientResourceManager.RemoveTransientResource(objectKind, gamePath);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (noValidationFailed > 0)
|
||||
{
|
||||
_mareMediator.Publish(new NotificationMessage("Invalid Skeleton Setup",
|
||||
$"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " +
|
||||
$"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).",
|
||||
NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
|
||||
{
|
||||
var forwardPaths = forwardResolve.ToArray();
|
||||
var reversePaths = reverseResolve.ToArray();
|
||||
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
|
||||
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
{
|
||||
var filePath = forward[i].ToLowerInvariant();
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
{
|
||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = [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(reverse[i].Select(c => c.ToLowerInvariant()));
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||
}
|
||||
|
||||
private HashSet<string> ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer)
|
||||
{
|
||||
_transientResourceManager.PersistTransientResources(charaPointer, objectKind);
|
||||
|
||||
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
||||
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
|
||||
{
|
||||
pathsToResolve.Add(path);
|
||||
}
|
||||
|
||||
return pathsToResolve;
|
||||
}
|
||||
}
|
487
MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs
Normal file
487
MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs
Normal file
@@ -0,0 +1,487 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Handlers;
|
||||
|
||||
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly Func<IntPtr> _getAddress;
|
||||
private readonly bool _isOwnedObject;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private CancellationTokenSource? _clearCts = new();
|
||||
private Task? _delayedZoningTask;
|
||||
private bool _haltProcessing = false;
|
||||
private bool _ignoreSendAfterRedraw = false;
|
||||
private int _ptrNullCounter = 0;
|
||||
private byte _classJob = 0;
|
||||
private CancellationTokenSource _zoningCts = new();
|
||||
|
||||
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
|
||||
MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool ownedObject = true) : base(logger, mediator)
|
||||
{
|
||||
_performanceCollector = performanceCollector;
|
||||
ObjectKind = objectKind;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_getAddress = () =>
|
||||
{
|
||||
_dalamudUtil.EnsureIsOnFramework();
|
||||
return getAddress.Invoke();
|
||||
};
|
||||
_isOwnedObject = ownedObject;
|
||||
Name = string.Empty;
|
||||
|
||||
if (ownedObject)
|
||||
{
|
||||
Mediator.Subscribe<TransientResourceChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (_delayedZoningTask?.IsCompleted ?? true)
|
||||
{
|
||||
if (msg.Address != Address) return;
|
||||
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||
|
||||
Mediator.Subscribe<CutsceneStartMessage>(this, (_) =>
|
||||
{
|
||||
_haltProcessing = true;
|
||||
});
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) =>
|
||||
{
|
||||
_haltProcessing = false;
|
||||
ZoneSwitchEnd();
|
||||
});
|
||||
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.Address == Address)
|
||||
{
|
||||
_haltProcessing = true;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.Address == Address)
|
||||
{
|
||||
_haltProcessing = false;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
_ignoreSendAfterRedraw = true;
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
_ignoreSendAfterRedraw = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||
|
||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private enum DrawCondition
|
||||
{
|
||||
None,
|
||||
DrawObjectZero,
|
||||
RenderFlags,
|
||||
ModelInSlotLoaded,
|
||||
ModelFilesInSlotLoaded
|
||||
}
|
||||
|
||||
public byte RaceId { get; private set; }
|
||||
public byte Gender { get; private set; }
|
||||
public byte TribeId { get; private set; }
|
||||
|
||||
public IntPtr Address { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public ObjectKind ObjectKind { get; }
|
||||
private byte[] CustomizeData { get; set; } = new byte[26];
|
||||
private IntPtr DrawObjectAddress { get; set; }
|
||||
private byte[] EquipSlotData { get; set; } = new byte[40];
|
||||
private ushort[] MainHandData { get; set; } = new ushort[3];
|
||||
private ushort[] OffHandData { get; set; } = new ushort[3];
|
||||
|
||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||
{
|
||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (IsBeingDrawn()) return true;
|
||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||
{
|
||||
act.Invoke(chara);
|
||||
}
|
||||
return false;
|
||||
}).ConfigureAwait(false))
|
||||
{
|
||||
await Task.Delay(250, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void CompareNameAndThrow(string name)
|
||||
{
|
||||
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Player name not equal to requested name, pointer invalid");
|
||||
}
|
||||
if (Address == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Player pointer is zero, pointer invalid");
|
||||
}
|
||||
}
|
||||
|
||||
public IntPtr CurrentAddress()
|
||||
{
|
||||
_dalamudUtil.EnsureIsOnFramework();
|
||||
return _getAddress.Invoke();
|
||||
}
|
||||
|
||||
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
|
||||
{
|
||||
return _dalamudUtil.CreateGameObject(Address);
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
Address = IntPtr.Zero;
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
_haltProcessing = false;
|
||||
}
|
||||
|
||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var owned = _isOwnedObject ? "Self" : "Other";
|
||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
||||
}
|
||||
|
||||
private unsafe void CheckAndUpdateObject()
|
||||
{
|
||||
var prevAddr = Address;
|
||||
var prevDrawObj = DrawObjectAddress;
|
||||
|
||||
Address = _getAddress();
|
||||
if (Address != IntPtr.Zero)
|
||||
{
|
||||
_ptrNullCounter = 0;
|
||||
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
|
||||
DrawObjectAddress = drawObjAddr;
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (_haltProcessing) return;
|
||||
|
||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||
bool addrDiff = Address != prevAddr;
|
||||
|
||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||
{
|
||||
if (_clearCts != null)
|
||||
{
|
||||
Logger.LogDebug("[{this}] Cancelling Clear Task", this);
|
||||
_clearCts.CancelDispose();
|
||||
_clearCts = null;
|
||||
}
|
||||
var chara = (Character*)Address;
|
||||
var name = chara->GameObject.NameString;
|
||||
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
|
||||
if (nameChange)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
bool equipDiff = false;
|
||||
|
||||
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
||||
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
||||
{
|
||||
var classJob = chara->CharacterData.ClassJob;
|
||||
if (classJob != _classJob)
|
||||
{
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
}
|
||||
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head);
|
||||
|
||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
else
|
||||
{
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if (equipDiff && !_isOwnedObject && !_ignoreSendAfterRedraw) // send the message out immediately and cancel out, no reason to continue if not self
|
||||
{
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
Mediator.Publish(new CharacterChangedMessage(this));
|
||||
return;
|
||||
}
|
||||
|
||||
bool customizeDiff = false;
|
||||
|
||||
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
||||
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
||||
{
|
||||
var gender = ((Human*)DrawObjectAddress)->Customize.Sex;
|
||||
var raceId = ((Human*)DrawObjectAddress)->Customize.Race;
|
||||
var tribeId = ((Human*)DrawObjectAddress)->Customize.Tribe;
|
||||
|
||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||
{
|
||||
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
||||
Gender = gender;
|
||||
RaceId = raceId;
|
||||
TribeId = tribeId;
|
||||
}
|
||||
|
||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||
}
|
||||
else
|
||||
{
|
||||
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||
{
|
||||
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
||||
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||
}
|
||||
}
|
||||
else if (addrDiff || drawObjDiff)
|
||||
{
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
_clearCts?.CancelDispose();
|
||||
_clearCts = new();
|
||||
var token = _clearCts.Token;
|
||||
_ = Task.Run(() => ClearAsync(token), token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearAsync(CancellationToken token)
|
||||
{
|
||||
Logger.LogDebug("[{this}] Running Clear Task", this);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this);
|
||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||
_clearCts = null;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
|
||||
for (int i = 0; i < customizeData.Length; i++)
|
||||
{
|
||||
var data = customizeData[i];
|
||||
if (CustomizeData[i] != data)
|
||||
{
|
||||
CustomizeData[i] = data;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
for (int i = 0; i < EquipSlotData.Length; i++)
|
||||
{
|
||||
var data = equipSlotData[i];
|
||||
if (EquipSlotData[i] != data)
|
||||
{
|
||||
EquipSlotData[i] = data;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||
MainHandData[0] = weapon->ModelSetId;
|
||||
hasChanges |= weapon->Variant != MainHandData[1];
|
||||
MainHandData[1] = weapon->Variant;
|
||||
hasChanges |= weapon->SecondaryId != MainHandData[2];
|
||||
MainHandData[2] = weapon->SecondaryId;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||
OffHandData[0] = weapon->ModelSetId;
|
||||
hasChanges |= weapon->Variant != OffHandData[1];
|
||||
OffHandData[1] = weapon->Variant;
|
||||
hasChanges |= weapon->SecondaryId != OffHandData[2];
|
||||
OffHandData[2] = weapon->SecondaryId;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
if (!_delayedZoningTask?.IsCompleted ?? false) return;
|
||||
|
||||
try
|
||||
{
|
||||
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}"
|
||||
+ $"+{Address.ToString("X")}", CheckAndUpdateObject);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe IntPtr GetDrawObjUnsafe(nint curPtr)
|
||||
{
|
||||
return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->DrawObject;
|
||||
}
|
||||
|
||||
private bool IsBeingDrawn()
|
||||
{
|
||||
var curPtr = _getAddress();
|
||||
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr: {ptr}", this, curPtr.ToString("X"));
|
||||
|
||||
if (curPtr == IntPtr.Zero && _ptrNullCounter < 2)
|
||||
{
|
||||
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, counter is {cnt}", this, _ptrNullCounter);
|
||||
_ptrNullCounter++;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (curPtr == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, returning", this);
|
||||
|
||||
Address = IntPtr.Zero;
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
throw new ArgumentNullException($"CurPtr for {this} turned ZERO");
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsAnythingDrawing)
|
||||
{
|
||||
Logger.LogTrace("[{this}] IsBeingDrawn, Global draw block", this);
|
||||
return true;
|
||||
}
|
||||
|
||||
var drawObj = GetDrawObjUnsafe(curPtr);
|
||||
Logger.LogTrace("[{this}] IsBeingDrawn, DrawObjPtr: {ptr}", this, drawObj.ToString("X"));
|
||||
var isDrawn = IsBeingDrawnUnsafe(drawObj, curPtr);
|
||||
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, isDrawn);
|
||||
return isDrawn != DrawCondition.None;
|
||||
}
|
||||
|
||||
private unsafe DrawCondition IsBeingDrawnUnsafe(IntPtr drawObj, IntPtr curPtr)
|
||||
{
|
||||
var drawObjZero = drawObj == IntPtr.Zero;
|
||||
if (drawObjZero) return DrawCondition.DrawObjectZero;
|
||||
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->RenderFlags) != 0x0;
|
||||
if (renderFlags) return DrawCondition.RenderFlags;
|
||||
|
||||
if (ObjectKind == ObjectKind.Player)
|
||||
{
|
||||
var modelInSlotLoaded = (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0);
|
||||
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
|
||||
var modelFilesInSlotLoaded = (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0);
|
||||
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
|
||||
return DrawCondition.None;
|
||||
}
|
||||
|
||||
return DrawCondition.None;
|
||||
}
|
||||
|
||||
private void ZoneSwitchEnd()
|
||||
{
|
||||
if (!_isOwnedObject || _haltProcessing) return;
|
||||
|
||||
_clearCts?.Cancel();
|
||||
_clearCts?.Dispose();
|
||||
_clearCts = null;
|
||||
try
|
||||
{
|
||||
_zoningCts?.CancelAfter(2500);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Zoning CTS cancel issue");
|
||||
}
|
||||
}
|
||||
|
||||
private void ZoneSwitchStart()
|
||||
{
|
||||
if (!_isOwnedObject || _haltProcessing) return;
|
||||
|
||||
_zoningCts = new();
|
||||
Logger.LogDebug("[{obj}] Starting Delay After Zoning", this);
|
||||
_delayedZoningTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(120), _zoningCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cancelled
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
||||
_zoningCts.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
916
MareSynchronos/PlayerData/Handlers/PairHandler.cs
Normal file
916
MareSynchronos/PlayerData/Handlers/PairHandler.cs
Normal file
@@ -0,0 +1,916 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.FileCache;
|
||||
using MareSynchronos.Interop.Ipc;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Factories;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Events;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Utils;
|
||||
using MareSynchronos.WebAPI.Files;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Handlers;
|
||||
|
||||
public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||
|
||||
private readonly MareConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly FileDownloadManager _downloadManager;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private readonly VisibilityService _visibilityService;
|
||||
private readonly NoSnapService _noSnapService;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||
private Guid _applicationId;
|
||||
private Task? _applicationTask;
|
||||
private CharacterData? _cachedData = null;
|
||||
private GameObjectHandler? _charaHandler;
|
||||
private readonly Dictionary<ObjectKind, Guid?> _customizeIds = [];
|
||||
private CombatData? _dataReceivedInDowntime;
|
||||
private CancellationTokenSource? _downloadCancellationTokenSource = new();
|
||||
private bool _forceApplyMods = false;
|
||||
private bool _isVisible;
|
||||
private Guid _deferred = Guid.Empty;
|
||||
private Guid _penumbraCollection = Guid.Empty;
|
||||
private bool _redrawOnNextApplication = false;
|
||||
|
||||
public PairHandler(ILogger<PairHandler> logger, Pair pair, PairAnalyzer pairAnalyzer,
|
||||
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||
IpcManager ipcManager, FileDownloadManager transferManager,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager, MareMediator mediator,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
ServerConfigurationManager serverConfigManager,
|
||||
MareConfigService configService, VisibilityService visibilityService,
|
||||
NoSnapService noSnapService) : base(logger, mediator)
|
||||
{
|
||||
Pair = pair;
|
||||
PairAnalyzer = pairAnalyzer;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_downloadManager = transferManager;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_configService = configService;
|
||||
_visibilityService = visibilityService;
|
||||
_noSnapService = noSnapService;
|
||||
|
||||
_visibilityService.StartTracking(Pair.Ident);
|
||||
|
||||
Mediator.SubscribeKeyed<PlayerVisibilityMessage>(this, Pair.Ident, (msg) => UpdateVisibility(msg.IsVisible, msg.Invalidate));
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) =>
|
||||
{
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_charaHandler?.Invalidate();
|
||||
IsVisible = false;
|
||||
});
|
||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
|
||||
{
|
||||
_penumbraCollection = Guid.Empty;
|
||||
if (!IsVisible && _charaHandler != null)
|
||||
{
|
||||
PlayerName = string.Empty;
|
||||
_charaHandler.Dispose();
|
||||
_charaHandler = null;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.GameObjectHandler == _charaHandler)
|
||||
{
|
||||
_redrawOnNextApplication = true;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<CombatOrPerformanceEndMessage>(this, (msg) =>
|
||||
{
|
||||
if (IsVisible && _dataReceivedInDowntime != null)
|
||||
{
|
||||
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
|
||||
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
|
||||
_dataReceivedInDowntime = null;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<CombatOrPerformanceStartMessage>(this, _ =>
|
||||
{
|
||||
if (_configService.Current.HoldCombatApplication)
|
||||
{
|
||||
_dataReceivedInDowntime = null;
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<RecalculatePerformanceMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.UID != null && !msg.UID.Equals(Pair.UserData.UID, StringComparison.Ordinal)) return;
|
||||
Logger.LogDebug("Recalculating performance for {uid}", Pair.UserData.UID);
|
||||
pair.ApplyLastReceivedData(forced: true);
|
||||
});
|
||||
|
||||
LastAppliedDataBytes = -1;
|
||||
}
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
private set
|
||||
{
|
||||
if (_isVisible != value)
|
||||
{
|
||||
_isVisible = value;
|
||||
string text = "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible");
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler),
|
||||
EventSeverity.Informational, text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long LastAppliedDataBytes { get; private set; }
|
||||
public Pair Pair { get; private init; }
|
||||
public PairAnalyzer PairAnalyzer { get; private init; }
|
||||
public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero;
|
||||
public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero
|
||||
? uint.MaxValue
|
||||
: ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_charaHandler!.Address)->EntityId;
|
||||
public string? PlayerName { get; private set; }
|
||||
public string PlayerNameHash => Pair.Ident;
|
||||
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||
{
|
||||
if (_configService.Current.HoldCombatApplication && _dalamudUtil.IsInCombatOrPerforming)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: you are in combat or performing music, deferring application")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase);
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(isUploading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero))
|
||||
{
|
||||
if (_deferred != Guid.Empty)
|
||||
{
|
||||
_isVisible = false;
|
||||
_visibilityService.StopTracking(Pair.Ident);
|
||||
_visibilityService.StartTracking(Pair.Ident);
|
||||
}
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: Receiving Player is in an invalid state, deferring application")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
|
||||
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
|
||||
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
|
||||
this, forceApplyCustomization, forceApplyMods: false)
|
||||
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
|
||||
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
|
||||
_cachedData = characterData;
|
||||
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData));
|
||||
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||
// Ensure that this deferred application actually occurs by forcing visibiltiy to re-proc
|
||||
// Set _deferred as a silencing flag to avoid spamming logs once per frame with failed applications
|
||||
_isVisible = false;
|
||||
_deferred = applicationBase;
|
||||
_visibilityService.StopTracking(Pair.Ident);
|
||||
_visibilityService.StartTracking(Pair.Ident);
|
||||
return;
|
||||
}
|
||||
|
||||
_deferred = Guid.Empty;
|
||||
|
||||
SetUploading(isUploading: false);
|
||||
|
||||
if (Pair.IsDownloadBlocked)
|
||||
{
|
||||
var reasons = string.Join(", ", Pair.HoldDownloadReasons);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
$"Not applying character data: {reasons}")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
|
||||
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
|
||||
this, forceApplyCustomization, forceApplyMods: false)
|
||||
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
|
||||
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
|
||||
_cachedData = characterData;
|
||||
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData));
|
||||
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods);
|
||||
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
|
||||
|
||||
if (string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal) && !forceApplyCustomization) return;
|
||||
|
||||
if (_dalamudUtil.IsInCutscene || _dalamudUtil.IsInGpose || !_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: you are in GPose, a Cutscene or Penumbra/Glamourer is not available")));
|
||||
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while in cutscene/gpose or Penumbra/Glamourer unavailable, returning", applicationBase, this);
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
"Applying Character Data")));
|
||||
|
||||
_forceApplyMods |= forceApplyCustomization;
|
||||
|
||||
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
|
||||
|
||||
if (_charaHandler != null && _forceApplyMods)
|
||||
{
|
||||
_forceApplyMods = false;
|
||||
}
|
||||
|
||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||
{
|
||||
player.Add(PlayerChanges.ForcedRedraw);
|
||||
_redrawOnNextApplication = false;
|
||||
}
|
||||
|
||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||
{
|
||||
_pluginWarningNotificationManager.NotifyForMissingPlugins(Pair.UserData, PlayerName!, playerChanges);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, this);
|
||||
|
||||
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Pair == null
|
||||
? base.ToString() ?? string.Empty
|
||||
: Pair.UserData.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != nint.Zero ? "HasChar" : "NoChar");
|
||||
}
|
||||
|
||||
internal void SetUploading(bool isUploading = true)
|
||||
{
|
||||
Logger.LogTrace("Setting {this} uploading {uploading}", this, isUploading);
|
||||
if (_charaHandler != null)
|
||||
{
|
||||
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing) return;
|
||||
|
||||
_visibilityService.StopTracking(Pair.Ident);
|
||||
|
||||
SetUploading(isUploading: false);
|
||||
var name = PlayerName;
|
||||
Logger.LogDebug("Disposing {name} ({user})", name, Pair);
|
||||
try
|
||||
{
|
||||
Guid applicationId = Guid.NewGuid();
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User")));
|
||||
}
|
||||
|
||||
UndoApplicationAsync(applicationId).GetAwaiter().GetResult();
|
||||
|
||||
_applicationCancellationTokenSource?.Dispose();
|
||||
_applicationCancellationTokenSource = null;
|
||||
_downloadCancellationTokenSource?.Dispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
_charaHandler?.Dispose();
|
||||
_charaHandler = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error on disposal of {name}", name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PlayerName = null;
|
||||
_cachedData = null;
|
||||
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, null));
|
||||
Logger.LogDebug("Disposing {name} complete", name);
|
||||
}
|
||||
}
|
||||
|
||||
public void UndoApplication(Guid applicationId = default)
|
||||
{
|
||||
_ = Task.Run(async () => {
|
||||
await UndoApplicationAsync(applicationId).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterGposeClones()
|
||||
{
|
||||
var name = PlayerName;
|
||||
if (name == null)
|
||||
return;
|
||||
_ = _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var actor in _dalamudUtil.GetGposeCharactersFromObjectTable())
|
||||
{
|
||||
if (actor == null) continue;
|
||||
var gposeName = actor.Name.TextValue;
|
||||
if (!name.Equals(gposeName, StringComparison.Ordinal))
|
||||
continue;
|
||||
_noSnapService.AddGposer(actor.ObjectIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task UndoApplicationAsync(Guid applicationId = default)
|
||||
{
|
||||
Logger.LogDebug($"Undoing application of {Pair.UserPair}");
|
||||
var name = PlayerName;
|
||||
try
|
||||
{
|
||||
if (applicationId == default)
|
||||
applicationId = Guid.NewGuid();
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||
if (_penumbraCollection != Guid.Empty)
|
||||
{
|
||||
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false);
|
||||
_penumbraCollection = Guid.Empty;
|
||||
RegisterGposeClones();
|
||||
}
|
||||
|
||||
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair);
|
||||
if (!IsVisible)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||
await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
||||
|
||||
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}", applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
|
||||
|
||||
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
|
||||
{
|
||||
try
|
||||
{
|
||||
await RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_dalamudUtil.IsInCutscene && !string.IsNullOrEmpty(name))
|
||||
{
|
||||
_noSnapService.AddGposerNamed(name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error on undoing application of {name}", name);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (PlayerCharacter == nint.Zero) return;
|
||||
var ptr = PlayerCharacter;
|
||||
|
||||
var handler = changes.Key switch
|
||||
{
|
||||
ObjectKind.Player => _charaHandler!,
|
||||
ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanion(ptr), isWatched: false).ConfigureAwait(false),
|
||||
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), isWatched: false).ConfigureAwait(false),
|
||||
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), isWatched: false).ConfigureAwait(false),
|
||||
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
|
||||
};
|
||||
|
||||
async Task processApplication(IEnumerable<PlayerChanges> changeList)
|
||||
{
|
||||
foreach (var change in changeList)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}{ft}] Processing {change} for {handler}", applicationId, _dalamudUtil.IsOnFrameworkThread ? "*" : "", change, handler);
|
||||
switch (change)
|
||||
{
|
||||
case PlayerChanges.Customize:
|
||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||
{
|
||||
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||
}
|
||||
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
_customizeIds.Remove(changes.Key);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Heels:
|
||||
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Honorific:
|
||||
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Glamourer:
|
||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||
{
|
||||
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token, allowImmediate: true).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.PetNames:
|
||||
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Moodles:
|
||||
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.ForcedRedraw:
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (handler.Address == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (_configService.Current.SerialApplication)
|
||||
{
|
||||
var serialChangeList = changes.Value.Where(p => p <= PlayerChanges.ForcedRedraw).OrderBy(p => (int)p);
|
||||
var asyncChangeList = changes.Value.Where(p => p > PlayerChanges.ForcedRedraw).OrderBy(p => (int)p);
|
||||
await _dalamudUtil.RunOnFrameworkThread(async () => await processApplication(serialChangeList).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
await Task.Run(async () => await processApplication(asyncChangeList).ConfigureAwait(false), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = processApplication(changes.Value.OrderBy(p => (int)p));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handler != _charaHandler) handler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
||||
{
|
||||
if (!updatedData.Any())
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, this);
|
||||
return;
|
||||
}
|
||||
|
||||
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
|
||||
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip));
|
||||
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var downloadToken = _downloadCancellationTokenSource.Token;
|
||||
|
||||
_ = Task.Run(async () => {
|
||||
await DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
private Task? _pairDownloadTask;
|
||||
|
||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync", applicationBase);
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync > updateModdedPaths", applicationBase);
|
||||
int attempts = 0;
|
||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
|
||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||
{
|
||||
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||
await _pairDownloadTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
$"Starting download for {toDownloadReplacements.Count} files")));
|
||||
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
|
||||
|
||||
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
||||
{
|
||||
Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1);
|
||||
_downloadManager.ClearDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false), downloadToken);
|
||||
|
||||
await _pairDownloadTask.ConfigureAwait(false);
|
||||
|
||||
if (downloadToken.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
|
||||
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Mediator.Publish(new HaltScanMessage(nameof(PlayerPerformanceService.ShrinkTextures)));
|
||||
if (await _playerPerformanceService.ShrinkTextures(this, charaData, downloadToken).ConfigureAwait(false))
|
||||
_ = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(PlayerPerformanceService.ShrinkTextures)));
|
||||
}
|
||||
|
||||
bool exceedsThreshold = !await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false);
|
||||
|
||||
if (exceedsThreshold)
|
||||
Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1);
|
||||
else
|
||||
Pair.UnholdApplication("IndividualPerformanceThreshold");
|
||||
|
||||
if (exceedsThreshold)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Not applying due to performance thresholds", applicationBase);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Pair.IsApplicationBlocked)
|
||||
{
|
||||
var reasons = string.Join(", ", Pair.HoldApplicationReasons);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
$"Not applying character data: {reasons}")));
|
||||
Logger.LogTrace("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadToken.ThrowIfCancellationRequested();
|
||||
|
||||
var appToken = _applicationCancellationTokenSource?.Token;
|
||||
while ((!_applicationTask?.IsCompleted ?? false)
|
||||
&& !downloadToken.IsCancellationRequested
|
||||
&& (!appToken?.IsCancellationRequested ?? false))
|
||||
{
|
||||
// block until current application is done
|
||||
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) return;
|
||||
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _applicationCancellationTokenSource.Token;
|
||||
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||
}
|
||||
|
||||
private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||
{
|
||||
ushort objIndex = ushort.MaxValue;
|
||||
try
|
||||
{
|
||||
_applicationId = Guid.NewGuid();
|
||||
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId);
|
||||
|
||||
if (_penumbraCollection == Guid.Empty)
|
||||
{
|
||||
if (objIndex == ushort.MaxValue)
|
||||
objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
|
||||
_penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, Pair.UserData.UID).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
// ensure collection is set
|
||||
if (objIndex == ushort.MaxValue)
|
||||
objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
|
||||
|
||||
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection,
|
||||
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
LastAppliedDataBytes = -1;
|
||||
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
|
||||
{
|
||||
if (LastAppliedDataBytes == -1) LastAppliedDataBytes = 0;
|
||||
|
||||
LastAppliedDataBytes += path.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateManip)
|
||||
{
|
||||
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, _penumbraCollection, charaData.ManipulationData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kind in updatedData)
|
||||
{
|
||||
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
_cachedData = charaData;
|
||||
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, charaData));
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
||||
{
|
||||
IsVisible = false;
|
||||
_forceApplyMods = true;
|
||||
_cachedData = charaData;
|
||||
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, charaData));
|
||||
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisibility(bool nowVisible, bool invalidate = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(PlayerName))
|
||||
{
|
||||
var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident);
|
||||
if (pc.ObjectId == 0) return;
|
||||
Logger.LogDebug("One-Time Initializing {this}", this);
|
||||
Initialize(pc.Name);
|
||||
Logger.LogDebug("One-Time Initialized {this}", this);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
$"Initializing User For Character {pc.Name}")));
|
||||
}
|
||||
|
||||
// This was triggered by the character becoming handled by Mare, so unapply everything
|
||||
// There seems to be a good chance that this races Mare and then crashes
|
||||
if (!nowVisible && invalidate)
|
||||
{
|
||||
bool wasVisible = IsVisible;
|
||||
IsVisible = false;
|
||||
_charaHandler?.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
if (wasVisible)
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible);
|
||||
Logger.LogDebug("Invalidating {this}", this);
|
||||
UndoApplication();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsVisible && nowVisible)
|
||||
{
|
||||
// This is deferred application attempt, avoid any log output
|
||||
if (_deferred != Guid.Empty)
|
||||
{
|
||||
_isVisible = true;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
ApplyCharacterData(_deferred, _cachedData!, forceApplyCustomization: true);
|
||||
});
|
||||
}
|
||||
|
||||
IsVisible = true;
|
||||
Mediator.Publish(new PairHandlerVisibleMessage(this));
|
||||
if (_cachedData != null)
|
||||
{
|
||||
Guid appData = Guid.NewGuid();
|
||||
Logger.LogTrace("[BASE-{appBase}] {this} visibility changed, now: {visi}, cached data exists", appData, this, IsVisible);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
ApplyCharacterData(appData, _cachedData!, forceApplyCustomization: true);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}, no cached data exists", this, IsVisible);
|
||||
}
|
||||
}
|
||||
else if (IsVisible && !nowVisible)
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler?.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize(string name)
|
||||
{
|
||||
PlayerName = name;
|
||||
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident), isWatched: false).GetAwaiter().GetResult();
|
||||
|
||||
Mediator.Subscribe<HonorificReadyMessage>(this, msg =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
|
||||
Logger.LogTrace("Reapplying Honorific data for {this}", this);
|
||||
_ = Task.Run(async () => await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false), CancellationToken.None);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<PetNamesReadyMessage>(this, msg =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
|
||||
Logger.LogTrace("Reapplying Pet Names data for {this}", this);
|
||||
_ = Task.Run(async () => await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false), CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)
|
||||
{
|
||||
nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident);
|
||||
if (address == nint.Zero) return;
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, Pair.UserData.AliasOrUID, name, objectKind);
|
||||
|
||||
if (_customizeIds.TryGetValue(objectKind, out var customizeId))
|
||||
{
|
||||
_customizeIds.Remove(objectKind);
|
||||
}
|
||||
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
else if (objectKind == ObjectKind.MinionOrMount)
|
||||
{
|
||||
var minionOrMount = await _dalamudUtil.GetMinionOrMountAsync(address).ConfigureAwait(false);
|
||||
if (minionOrMount != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
var pet = await _dalamudUtil.GetPetAsync(address).ConfigureAwait(false);
|
||||
if (pet != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (objectKind == ObjectKind.Companion)
|
||||
{
|
||||
var companion = await _dalamudUtil.GetCompanionAsync(address).ConfigureAwait(false);
|
||||
if (companion != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token)
|
||||
{
|
||||
Stopwatch st = Stopwatch.StartNew();
|
||||
ConcurrentBag<FileReplacementData> missingFiles = [];
|
||||
moddedDictionary = [];
|
||||
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
|
||||
bool hasMigrationChanges = false;
|
||||
|
||||
try
|
||||
{
|
||||
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
|
||||
Parallel.ForEach(replacementList, new ParallelOptions()
|
||||
{
|
||||
CancellationToken = token,
|
||||
MaxDegreeOfParallelism = 4
|
||||
},
|
||||
(item) =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash, preferSubst: true);
|
||||
if (fileCache != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
|
||||
{
|
||||
hasMigrationChanges = true;
|
||||
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
|
||||
}
|
||||
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
outputDict[(gamePath, item.Hash)] = fileCache.ResolvedFilepath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("Missing file: {hash}", item.Hash);
|
||||
missingFiles.Add(item);
|
||||
}
|
||||
});
|
||||
|
||||
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
|
||||
{
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Adding file swap for {path}: {fileSwap}", applicationBase, gamePath, item.FileSwapPath);
|
||||
moddedDictionary[(gamePath, null)] = item.FileSwapPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||
}
|
||||
if (hasMigrationChanges) _fileDbManager.WriteOutFullCsv();
|
||||
st.Stop();
|
||||
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
|
||||
return [.. missingFiles];
|
||||
}
|
||||
}
|
75
MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs
Normal file
75
MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Utils;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Pairs;
|
||||
|
||||
public class OnlinePlayerManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly HashSet<PairHandler> _newVisiblePlayers = [];
|
||||
private readonly PairManager _pairManager;
|
||||
private CharacterData? _lastSentData;
|
||||
|
||||
public OnlinePlayerManager(ILogger<OnlinePlayerManager> logger, ApiController apiController, DalamudUtilService dalamudUtil,
|
||||
PairManager pairManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
|
||||
{
|
||||
_apiController = apiController;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_fileTransferManager = fileTransferManager;
|
||||
Mediator.Subscribe<PlayerChangedMessage>(this, (_) => PlayerManagerOnPlayerHasChanged());
|
||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
|
||||
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||
{
|
||||
var newData = msg.CharacterData;
|
||||
if (_lastSentData == null || (!string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal)))
|
||||
{
|
||||
Logger.LogDebug("Pushing data for visible players");
|
||||
_lastSentData = newData;
|
||||
PushCharacterData(_pairManager.GetVisibleUsers());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Not sending data for {hash}", newData.DataHash.Value);
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (msg) => _newVisiblePlayers.Add(msg.Player));
|
||||
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushCharacterData(_pairManager.GetVisibleUsers()));
|
||||
}
|
||||
|
||||
private void FrameworkOnUpdate()
|
||||
{
|
||||
if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return;
|
||||
|
||||
if (!_newVisiblePlayers.Any()) return;
|
||||
var newVisiblePlayers = _newVisiblePlayers.ToList();
|
||||
_newVisiblePlayers.Clear();
|
||||
Logger.LogTrace("Has new visible players, pushing character data");
|
||||
PushCharacterData(newVisiblePlayers.Select(c => c.Pair.UserData).ToList());
|
||||
}
|
||||
|
||||
private void PlayerManagerOnPlayerHasChanged()
|
||||
{
|
||||
PushCharacterData(_pairManager.GetVisibleUsers());
|
||||
}
|
||||
|
||||
private void PushCharacterData(List<UserData> visiblePlayers)
|
||||
{
|
||||
if (visiblePlayers.Any() && _lastSentData != null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var dataToSend = await _fileTransferManager.UploadFiles(_lastSentData.DeepClone(), visiblePlayers).ConfigureAwait(false);
|
||||
await _apiController.PushCharacterData(dataToSend, visiblePlayers).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
10
MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs
Normal file
10
MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace MareSynchronos.PlayerData.Pairs;
|
||||
|
||||
public record OptionalPluginWarning
|
||||
{
|
||||
public bool ShownHeelsWarning { get; set; } = false;
|
||||
public bool ShownCustomizePlusWarning { get; set; } = false;
|
||||
public bool ShownHonorificWarning { get; set; } = false;
|
||||
public bool ShowPetNicknamesWarning { get; set; } = false;
|
||||
public bool ShownMoodlesWarning { get; set; } = false;
|
||||
}
|
376
MareSynchronos/PlayerData/Pairs/Pair.cs
Normal file
376
MareSynchronos/PlayerData/Pairs/Pair.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Comparer;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Factories;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Pairs;
|
||||
|
||||
public class Pair : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||
private readonly SemaphoreSlim _creationSemaphore = new(1);
|
||||
private readonly ILogger<Pair> _logger;
|
||||
private readonly MareConfigService _mareConfig;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private CancellationTokenSource _applicationCts = new();
|
||||
private OnlineUserIdentDto? _onlineUserIdentDto = null;
|
||||
|
||||
public Pair(ILogger<Pair> logger, UserData userData, PairHandlerFactory cachedPlayerFactory,
|
||||
MareMediator mediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_cachedPlayerFactory = cachedPlayerFactory;
|
||||
_mareConfig = mareConfig;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
|
||||
UserData = userData;
|
||||
|
||||
Mediator.SubscribeKeyed<HoldPairApplicationMessage>(this, UserData.UID, (msg) => HoldApplication(msg.Source));
|
||||
Mediator.SubscribeKeyed<UnholdPairApplicationMessage>(this, UserData.UID, (msg) => UnholdApplication(msg.Source));
|
||||
}
|
||||
|
||||
public Dictionary<GroupFullInfoDto, GroupPairFullInfoDto> GroupPair { get; set; } = new(GroupDtoComparer.Instance);
|
||||
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null;
|
||||
public bool IsOnline => CachedPlayer != null;
|
||||
|
||||
public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused()
|
||||
: GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused());
|
||||
|
||||
// Download locks apply earlier in the process than Application locks
|
||||
private ConcurrentDictionary<string, int> HoldDownloadLocks { get; set; } = new(StringComparer.Ordinal);
|
||||
private ConcurrentDictionary<string, int> HoldApplicationLocks { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
public bool IsDownloadBlocked => HoldDownloadLocks.Any(f => f.Value > 0);
|
||||
public bool IsApplicationBlocked => HoldApplicationLocks.Any(f => f.Value > 0) || IsDownloadBlocked;
|
||||
|
||||
public IEnumerable<string> HoldDownloadReasons => HoldDownloadLocks.Keys;
|
||||
public IEnumerable<string> HoldApplicationReasons => Enumerable.Concat(HoldDownloadLocks.Keys, HoldApplicationLocks.Keys);
|
||||
|
||||
public bool IsVisible => CachedPlayer?.IsVisible ?? false;
|
||||
public CharacterData? LastReceivedCharacterData { get; set; }
|
||||
public string? PlayerName => GetPlayerName();
|
||||
public uint PlayerCharacterId => GetPlayerCharacterId();
|
||||
public long LastAppliedDataBytes => CachedPlayer?.LastAppliedDataBytes ?? -1;
|
||||
public long LastAppliedDataTris { get; set; } = -1;
|
||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
|
||||
public PairAnalyzer? PairAnalyzer => CachedPlayer?.PairAnalyzer;
|
||||
|
||||
public UserData UserData { get; init; }
|
||||
|
||||
public UserPairDto? UserPair { get; set; }
|
||||
|
||||
private PairHandler? CachedPlayer { get; set; }
|
||||
|
||||
public void AddContextMenu(IMenuOpenedArgs args)
|
||||
{
|
||||
if (CachedPlayer == null || (args.Target is not MenuTargetDefault target) || target.TargetObjectId != CachedPlayer.PlayerCharacterId || IsPaused) return;
|
||||
|
||||
void Add(string name, Action<IMenuItemClickedArgs>? action)
|
||||
{
|
||||
args.AddMenuItem(new MenuItem()
|
||||
{
|
||||
Name = name,
|
||||
OnClicked = action,
|
||||
PrefixColor = 559,
|
||||
PrefixChar = 'L'
|
||||
});
|
||||
}
|
||||
|
||||
bool isBlocked = IsApplicationBlocked;
|
||||
bool isBlacklisted = _serverConfigurationManager.IsUidBlacklisted(UserData.UID);
|
||||
bool isWhitelisted = _serverConfigurationManager.IsUidWhitelisted(UserData.UID);
|
||||
|
||||
Add("Open Profile", _ => Mediator.Publish(new ProfileOpenStandaloneMessage(this)));
|
||||
|
||||
if (!isBlocked && !isBlacklisted)
|
||||
Add("Always Block Modded Appearance", _ => {
|
||||
_serverConfigurationManager.AddBlacklistUid(UserData.UID);
|
||||
HoldApplication("Blacklist", maxValue: 1);
|
||||
ApplyLastReceivedData(forced: true);
|
||||
});
|
||||
else if (isBlocked && !isWhitelisted)
|
||||
Add("Always Allow Modded Appearance", _ => {
|
||||
_serverConfigurationManager.AddWhitelistUid(UserData.UID);
|
||||
UnholdApplication("Blacklist", skipApplication: true);
|
||||
ApplyLastReceivedData(forced: true);
|
||||
});
|
||||
|
||||
if (isWhitelisted)
|
||||
Add("Remove from Whitelist", _ => {
|
||||
_serverConfigurationManager.RemoveWhitelistUid(UserData.UID);
|
||||
ApplyLastReceivedData(forced: true);
|
||||
});
|
||||
else if (isBlacklisted)
|
||||
Add("Remove from Blacklist", _ => {
|
||||
_serverConfigurationManager.RemoveBlacklistUid(UserData.UID);
|
||||
UnholdApplication("Blacklist", skipApplication: true);
|
||||
ApplyLastReceivedData(forced: true);
|
||||
});
|
||||
|
||||
Add("Reapply last data", _ => ApplyLastReceivedData(forced: true));
|
||||
|
||||
if (UserPair != null)
|
||||
{
|
||||
Add("Change Permissions", _ => Mediator.Publish(new OpenPermissionWindow(this)));
|
||||
Add("Cycle pause state", _ => Mediator.Publish(new CyclePauseMessage(UserData)));
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyData(OnlineUserCharaDataDto data)
|
||||
{
|
||||
_applicationCts = _applicationCts.CancelRecreate();
|
||||
LastReceivedCharacterData = data.CharaData;
|
||||
|
||||
if (CachedPlayer == null)
|
||||
{
|
||||
_logger.LogDebug("Received Data for {uid} but CachedPlayer does not exist, waiting", data.User.UID);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using var timeoutCts = new CancellationTokenSource();
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(120));
|
||||
var appToken = _applicationCts.Token;
|
||||
using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, appToken);
|
||||
while (CachedPlayer == null && !combined.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(250, combined.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!combined.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug("Applying delayed data for {uid}", data.User.UID);
|
||||
ApplyLastReceivedData();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyLastReceivedData();
|
||||
}
|
||||
|
||||
public void ApplyLastReceivedData(bool forced = false)
|
||||
{
|
||||
if (CachedPlayer == null) return;
|
||||
if (LastReceivedCharacterData == null) return;
|
||||
if (IsDownloadBlocked) return;
|
||||
|
||||
if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID))
|
||||
HoldApplication("Blacklist", maxValue: 1);
|
||||
|
||||
if (NoSnapService.AnyLoaded)
|
||||
HoldApplication("NoSnap", maxValue: 1);
|
||||
else
|
||||
UnholdApplication("NoSnap", skipApplication: true);
|
||||
|
||||
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
|
||||
}
|
||||
|
||||
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_creationSemaphore.Wait();
|
||||
|
||||
if (CachedPlayer != null) return;
|
||||
|
||||
if (dto == null && _onlineUserIdentDto == null)
|
||||
{
|
||||
CachedPlayer?.Dispose();
|
||||
CachedPlayer = null;
|
||||
return;
|
||||
}
|
||||
if (dto != null)
|
||||
{
|
||||
_onlineUserIdentDto = dto;
|
||||
}
|
||||
|
||||
CachedPlayer?.Dispose();
|
||||
CachedPlayer = _cachedPlayerFactory.Create(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_creationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetNote()
|
||||
{
|
||||
return _serverConfigurationManager.GetNoteForUid(UserData.UID);
|
||||
}
|
||||
|
||||
public string? GetPlayerName()
|
||||
{
|
||||
if (CachedPlayer != null && CachedPlayer.PlayerName != null)
|
||||
return CachedPlayer.PlayerName;
|
||||
else
|
||||
return _serverConfigurationManager.GetNameForUid(UserData.UID);
|
||||
}
|
||||
|
||||
public uint GetPlayerCharacterId()
|
||||
{
|
||||
if (CachedPlayer != null)
|
||||
return CachedPlayer.PlayerCharacterId;
|
||||
return uint.MaxValue;
|
||||
}
|
||||
|
||||
public string? GetNoteOrName()
|
||||
{
|
||||
string? note = GetNote();
|
||||
if (_mareConfig.Current.ShowCharacterNames || IsVisible)
|
||||
return note ?? GetPlayerName();
|
||||
else
|
||||
return note;
|
||||
}
|
||||
|
||||
public string GetPairSortKey()
|
||||
{
|
||||
string? noteOrName = GetNoteOrName();
|
||||
|
||||
if (noteOrName != null)
|
||||
return $"0{noteOrName}";
|
||||
else
|
||||
return $"9{UserData.AliasOrUID}";
|
||||
}
|
||||
|
||||
public string GetPlayerNameHash()
|
||||
{
|
||||
return CachedPlayer?.PlayerNameHash ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool HasAnyConnection()
|
||||
{
|
||||
return UserPair != null || GroupPair.Any();
|
||||
}
|
||||
|
||||
public void MarkOffline(bool wait = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (wait)
|
||||
_creationSemaphore.Wait();
|
||||
LastReceivedCharacterData = null;
|
||||
var player = CachedPlayer;
|
||||
CachedPlayer = null;
|
||||
player?.Dispose();
|
||||
_onlineUserIdentDto = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (wait)
|
||||
_creationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetNote(string note)
|
||||
{
|
||||
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
||||
}
|
||||
|
||||
internal void SetIsUploading()
|
||||
{
|
||||
CachedPlayer?.SetUploading();
|
||||
}
|
||||
|
||||
public void HoldApplication(string source, int maxValue = int.MaxValue)
|
||||
{
|
||||
_logger.LogDebug($"Holding {UserData.UID} for reason: {source}");
|
||||
bool wasHeld = IsApplicationBlocked;
|
||||
HoldApplicationLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1));
|
||||
if (!wasHeld)
|
||||
CachedPlayer?.UndoApplication();
|
||||
}
|
||||
|
||||
public void UnholdApplication(string source, bool skipApplication = false)
|
||||
{
|
||||
_logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}");
|
||||
bool wasHeld = IsApplicationBlocked;
|
||||
HoldApplicationLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
|
||||
HoldApplicationLocks.TryRemove(new(source, 0));
|
||||
if (!skipApplication && wasHeld && !IsApplicationBlocked)
|
||||
ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
|
||||
public void HoldDownloads(string source, int maxValue = int.MaxValue)
|
||||
{
|
||||
_logger.LogDebug($"Holding {UserData.UID} for reason: {source}");
|
||||
bool wasHeld = IsApplicationBlocked;
|
||||
HoldDownloadLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1));
|
||||
if (!wasHeld)
|
||||
CachedPlayer?.UndoApplication();
|
||||
}
|
||||
|
||||
public void UnholdDownloads(string source, bool skipApplication = false)
|
||||
{
|
||||
_logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}");
|
||||
bool wasHeld = IsApplicationBlocked;
|
||||
HoldDownloadLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
|
||||
HoldDownloadLocks.TryRemove(new(source, 0));
|
||||
if (!skipApplication && wasHeld && !IsApplicationBlocked)
|
||||
ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
|
||||
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
|
||||
{
|
||||
_logger.LogTrace("Removing not synced files");
|
||||
if (data == null)
|
||||
{
|
||||
_logger.LogTrace("Nothing to remove");
|
||||
return data;
|
||||
}
|
||||
|
||||
var ActiveGroupPairs = GroupPair.Where(p => !p.Value.GroupUserPermissions.IsPaused() && !p.Key.GroupUserPermissions.IsPaused()).ToList();
|
||||
|
||||
bool disableIndividualAnimations = UserPair != null && (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations());
|
||||
bool disableIndividualVFX = UserPair != null && (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX());
|
||||
bool disableGroupAnimations = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations());
|
||||
|
||||
bool disableAnimations = (UserPair != null && disableIndividualAnimations) || (UserPair == null && disableGroupAnimations);
|
||||
|
||||
bool disableIndividualSounds = UserPair != null && (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds());
|
||||
bool disableGroupSounds = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds());
|
||||
bool disableGroupVFX = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableVFX() || pair.Key.GroupPermissions.IsDisableVFX() || pair.Key.GroupUserPermissions.IsDisableVFX());
|
||||
|
||||
bool disableSounds = (UserPair != null && disableIndividualSounds) || (UserPair == null && disableGroupSounds);
|
||||
bool disableVFX = (UserPair != null && disableIndividualVFX) || (UserPair == null && disableGroupVFX);
|
||||
|
||||
_logger.LogTrace("Disable: Sounds: {disableSounds}, Anims: {disableAnimations}, VFX: {disableVFX}",
|
||||
disableSounds, disableAnimations, disableVFX);
|
||||
|
||||
if (disableAnimations || disableSounds || disableVFX)
|
||||
{
|
||||
_logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}",
|
||||
disableAnimations, disableSounds, disableVFX);
|
||||
foreach (var objectKind in data.FileReplacements.Select(k => k.Key))
|
||||
{
|
||||
if (disableSounds)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (disableAnimations)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (disableVFX)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
403
MareSynchronos/PlayerData/Pairs/PairManager.cs
Normal file
403
MareSynchronos/PlayerData/Pairs/PairManager.cs
Normal file
@@ -0,0 +1,403 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Comparer;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Factories;
|
||||
using MareSynchronos.Services.Events;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Pairs;
|
||||
|
||||
public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ConcurrentDictionary<UserData, Pair> _allClientPairs = new(UserDataComparer.Instance);
|
||||
private readonly ConcurrentDictionary<GroupData, GroupFullInfoDto> _allGroups = new(GroupDataComparer.Instance);
|
||||
private readonly MareConfigService _configurationService;
|
||||
private readonly IContextMenu _dalamudContextMenu;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private Lazy<List<Pair>> _directPairsInternal;
|
||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||
|
||||
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
||||
MareConfigService configurationService, MareMediator mediator,
|
||||
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
||||
{
|
||||
_pairFactory = pairFactory;
|
||||
_configurationService = configurationService;
|
||||
_dalamudContextMenu = dalamudContextMenu;
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||
_directPairsInternal = DirectPairsLazy();
|
||||
_groupPairsInternal = GroupPairsLazy();
|
||||
|
||||
_dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||
}
|
||||
|
||||
public List<Pair> DirectPairs => _directPairsInternal.Value;
|
||||
|
||||
public Dictionary<GroupFullInfoDto, List<Pair>> GroupPairs => _groupPairsInternal.Value;
|
||||
public Dictionary<GroupData, GroupFullInfoDto> Groups => _allGroups.ToDictionary(k => k.Key, k => k.Value);
|
||||
public Pair? LastAddedUser { get; internal set; }
|
||||
|
||||
public void AddGroup(GroupFullInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group] = dto;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void AddGroupPair(GroupPairFullInfoDto dto)
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User))
|
||||
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||
|
||||
var group = _allGroups[dto.Group];
|
||||
_allClientPairs[dto.User].GroupPair[group] = dto;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public Pair? GetPairByUID(string uid)
|
||||
{
|
||||
var existingPair = _allClientPairs.FirstOrDefault(f => uid.Equals(f.Key.UID, StringComparison.Ordinal));
|
||||
if (!Equals(existingPair, default(KeyValuePair<UserData, Pair>)))
|
||||
{
|
||||
return existingPair.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true)
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User))
|
||||
{
|
||||
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
addToLastAddedUser = false;
|
||||
}
|
||||
|
||||
_allClientPairs[dto.User].UserPair = dto;
|
||||
if (addToLastAddedUser)
|
||||
LastAddedUser = _allClientPairs[dto.User];
|
||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void ClearPairs()
|
||||
{
|
||||
Logger.LogDebug("Clearing all Pairs");
|
||||
DisposePairs();
|
||||
_allClientPairs.Clear();
|
||||
_allGroups.Clear();
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public List<Pair> GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList();
|
||||
|
||||
public int GetVisibleUserCount() => _allClientPairs.Count(p => p.Value.IsVisible);
|
||||
|
||||
public List<UserData> GetVisibleUsers() => _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key).ToList();
|
||||
|
||||
public void MarkPairOffline(UserData user)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(user, out var pair))
|
||||
{
|
||||
Mediator.Publish(new ClearProfileDataMessage(pair.UserData));
|
||||
pair.MarkOffline();
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true)
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
||||
|
||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||
|
||||
var pair = _allClientPairs[dto.User];
|
||||
if (pair.HasCachedPlayer)
|
||||
{
|
||||
RecreateLazy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendNotif && _configurationService.Current.ShowOnlineNotifications
|
||||
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null
|
||||
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs)
|
||||
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote())
|
||||
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs))
|
||||
{
|
||||
string? note = pair.GetNoteOrName();
|
||||
var msg = !string.IsNullOrEmpty(note)
|
||||
? $"{note} ({pair.UserData.AliasOrUID}) is now online"
|
||||
: $"{pair.UserData.AliasOrUID} is now online";
|
||||
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
pair.CreateCachedPlayer(dto);
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void ReceiveCharaData(OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (!_allClientPairs.TryGetValue(dto.User, out var pair)) throw new InvalidOperationException("No user found for " + dto.User);
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(pair.UserData, nameof(PairManager), EventSeverity.Informational, "Received Character Data")));
|
||||
_allClientPairs[dto.User].ApplyData(dto);
|
||||
}
|
||||
|
||||
public void RemoveGroup(GroupData data)
|
||||
{
|
||||
_allGroups.TryRemove(data, out _);
|
||||
|
||||
foreach (var item in _allClientPairs.ToList())
|
||||
{
|
||||
foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).Where(grpPair => GroupDataComparer.Instance.Equals(grpPair.Group, data)).ToList())
|
||||
{
|
||||
_allClientPairs[item.Key].GroupPair.Remove(grpPair);
|
||||
}
|
||||
|
||||
if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair))
|
||||
{
|
||||
pair.MarkOffline();
|
||||
}
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void RemoveGroupPair(GroupPairDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
var group = _allGroups[dto.Group];
|
||||
pair.GroupPair.Remove(group);
|
||||
|
||||
if (!pair.HasAnyConnection())
|
||||
{
|
||||
pair.MarkOffline();
|
||||
_allClientPairs.TryRemove(dto.User, out _);
|
||||
}
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void RemoveUserPair(UserDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
pair.UserPair = null;
|
||||
|
||||
if (!pair.HasAnyConnection())
|
||||
{
|
||||
pair.MarkOffline();
|
||||
_allClientPairs.TryRemove(dto.User, out _);
|
||||
}
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void SetGroupInfo(GroupInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].Group = dto.Group;
|
||||
_allGroups[dto.Group].Owner = dto.Owner;
|
||||
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void UpdatePairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
throw new InvalidOperationException("No such pair for " + dto);
|
||||
}
|
||||
|
||||
if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto);
|
||||
|
||||
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused()
|
||||
|| pair.UserPair.OtherPermissions.IsPaired() != dto.Permissions.IsPaired())
|
||||
{
|
||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||
}
|
||||
|
||||
pair.UserPair.OtherPermissions = dto.Permissions;
|
||||
|
||||
Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}",
|
||||
pair.UserPair.OtherPermissions.IsPaused(),
|
||||
pair.UserPair.OtherPermissions.IsDisableAnimations(),
|
||||
pair.UserPair.OtherPermissions.IsDisableSounds(),
|
||||
pair.UserPair.OtherPermissions.IsDisableVFX());
|
||||
|
||||
if (!pair.IsPaused)
|
||||
pair.ApplyLastReceivedData();
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void UpdateSelfPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
throw new InvalidOperationException("No such pair for " + dto);
|
||||
}
|
||||
|
||||
if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto);
|
||||
|
||||
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused()
|
||||
|| pair.UserPair.OwnPermissions.IsPaired() != dto.Permissions.IsPaired())
|
||||
{
|
||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||
}
|
||||
|
||||
pair.UserPair.OwnPermissions = dto.Permissions;
|
||||
|
||||
Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}",
|
||||
pair.UserPair.OwnPermissions.IsPaused(),
|
||||
pair.UserPair.OwnPermissions.IsDisableAnimations(),
|
||||
pair.UserPair.OwnPermissions.IsDisableSounds(),
|
||||
pair.UserPair.OwnPermissions.IsDisableVFX());
|
||||
|
||||
if (!pair.IsPaused)
|
||||
pair.ApplyLastReceivedData();
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void ReceiveUploadStatus(UserDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible)
|
||||
{
|
||||
existingPair.SetIsUploading();
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto)
|
||||
{
|
||||
var group = _allGroups[dto.Group];
|
||||
_allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
var group = _allGroups[dto.Group];
|
||||
var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions;
|
||||
_allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions;
|
||||
if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations()
|
||||
|| prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds()
|
||||
|| prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX())
|
||||
{
|
||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||
}
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void SetGroupPermissions(GroupPermissionDto dto)
|
||||
{
|
||||
var prevPermissions = _allGroups[dto.Group].GroupPermissions;
|
||||
_allGroups[dto.Group].GroupPermissions = dto.Permissions;
|
||||
if (prevPermissions.IsDisableAnimations() != dto.Permissions.IsDisableAnimations()
|
||||
|| prevPermissions.IsDisableSounds() != dto.Permissions.IsDisableSounds()
|
||||
|| prevPermissions.IsDisableVFX() != dto.Permissions.IsDisableVFX())
|
||||
{
|
||||
RecreateLazy();
|
||||
var group = _allGroups[dto.Group];
|
||||
GroupPairs[group].ForEach(p => p.ApplyLastReceivedData());
|
||||
}
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void SetGroupStatusInfo(GroupPairUserInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
var prevPermissions = _allGroups[dto.Group].GroupUserPermissions;
|
||||
_allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions;
|
||||
if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations()
|
||||
|| prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds()
|
||||
|| prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX())
|
||||
{
|
||||
RecreateLazy();
|
||||
var group = _allGroups[dto.Group];
|
||||
GroupPairs[group].ForEach(p => p.ApplyLastReceivedData());
|
||||
}
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||
|
||||
DisposePairs();
|
||||
}
|
||||
|
||||
private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args)
|
||||
{
|
||||
if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return;
|
||||
if (!_configurationService.Current.EnableRightClickMenus) return;
|
||||
|
||||
foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible)))
|
||||
{
|
||||
pair.Value.AddContextMenu(args);
|
||||
}
|
||||
}
|
||||
|
||||
private Lazy<List<Pair>> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value)
|
||||
.Where(k => k.UserPair != null).ToList());
|
||||
|
||||
private void DisposePairs()
|
||||
{
|
||||
Logger.LogDebug("Disposing all Pairs");
|
||||
Parallel.ForEach(_allClientPairs, item =>
|
||||
{
|
||||
item.Value.MarkOffline(wait: false);
|
||||
});
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> GroupPairsLazy()
|
||||
{
|
||||
return new Lazy<Dictionary<GroupFullInfoDto, List<Pair>>>(() =>
|
||||
{
|
||||
Dictionary<GroupFullInfoDto, List<Pair>> outDict = new();
|
||||
foreach (var group in _allGroups)
|
||||
{
|
||||
outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList();
|
||||
}
|
||||
return outDict;
|
||||
});
|
||||
}
|
||||
|
||||
private void ReapplyPairData()
|
||||
{
|
||||
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||
{
|
||||
pair.ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecreateLazy()
|
||||
{
|
||||
_directPairsInternal = DirectPairsLazy();
|
||||
_groupPairsInternal = GroupPairsLazy();
|
||||
}
|
||||
}
|
263
MareSynchronos/PlayerData/Services/CacheCreationService.cs
Normal file
263
MareSynchronos/PlayerData/Services/CacheCreationService.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.PlayerData.Data;
|
||||
using MareSynchronos.PlayerData.Factories;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.PlayerData.Services;
|
||||
|
||||
#pragma warning disable MA0040
|
||||
|
||||
public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly SemaphoreSlim _cacheCreateLock = new(1);
|
||||
private readonly Dictionary<ObjectKind, GameObjectHandler> _cachesToCreate = [];
|
||||
private readonly PlayerDataFactory _characterDataFactory;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly CharacterData _playerData = new();
|
||||
private readonly Dictionary<ObjectKind, GameObjectHandler> _playerRelatedObjects = [];
|
||||
private Task? _cacheCreationTask;
|
||||
private CancellationTokenSource _honorificCts = new();
|
||||
private CancellationTokenSource _petNicknamesCts = new();
|
||||
private CancellationTokenSource _moodlesCts = new();
|
||||
private bool _isZoning = false;
|
||||
private bool _haltCharaDataCreation;
|
||||
private readonly Dictionary<ObjectKind, CancellationTokenSource> _glamourerCts = new();
|
||||
|
||||
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||
PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator)
|
||||
{
|
||||
_characterDataFactory = characterDataFactory;
|
||||
|
||||
Mediator.Subscribe<CreateCacheForObjectMessage>(this, (msg) =>
|
||||
{
|
||||
Logger.LogDebug("Received CreateCacheForObject for {handler}, updating", msg.ObjectToCreateFor);
|
||||
_cacheCreateLock.Wait();
|
||||
_cachesToCreate[msg.ObjectToCreateFor.ObjectKind] = msg.ObjectToCreateFor;
|
||||
_cacheCreateLock.Release();
|
||||
});
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (msg) => _isZoning = true);
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false);
|
||||
|
||||
Mediator.Subscribe<HaltCharaDataCreation>(this, (msg) =>
|
||||
{
|
||||
_haltCharaDataCreation = !msg.Resume;
|
||||
});
|
||||
|
||||
_playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true)
|
||||
.GetAwaiter().GetResult();
|
||||
_playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true)
|
||||
.GetAwaiter().GetResult();
|
||||
_playerRelatedObjects[ObjectKind.Pet] = gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPet(), isWatched: true)
|
||||
.GetAwaiter().GetResult();
|
||||
_playerRelatedObjects[ObjectKind.Companion] = gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), isWatched: true)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.GameObjectHandler != _playerRelatedObjects[ObjectKind.Player]) return;
|
||||
|
||||
Logger.LogTrace("Removing pet data for {obj}", msg.GameObjectHandler);
|
||||
_playerData.FileReplacements.Remove(ObjectKind.Pet);
|
||||
_playerData.GlamourerString.Remove(ObjectKind.Pet);
|
||||
_playerData.CustomizePlusScale.Remove(ObjectKind.Pet);
|
||||
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
|
||||
});
|
||||
|
||||
Mediator.Subscribe<ClearCacheForObjectMessage>(this, (msg) =>
|
||||
{
|
||||
// ignore pets
|
||||
if (msg.ObjectToCreateFor == _playerRelatedObjects[ObjectKind.Pet]) return;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
Logger.LogTrace("Clearing cache for {obj}", msg.ObjectToCreateFor);
|
||||
_playerData.FileReplacements.Remove(msg.ObjectToCreateFor.ObjectKind);
|
||||
_playerData.GlamourerString.Remove(msg.ObjectToCreateFor.ObjectKind);
|
||||
_playerData.CustomizePlusScale.Remove(msg.ObjectToCreateFor.ObjectKind);
|
||||
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
|
||||
});
|
||||
});
|
||||
|
||||
Mediator.Subscribe<CustomizePlusMessage>(this, (msg) =>
|
||||
{
|
||||
if (_isZoning) return;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
|
||||
foreach (var item in _playerRelatedObjects
|
||||
.Where(item => msg.Address == null
|
||||
|| item.Value.Address == msg.Address).Select(k => k.Key))
|
||||
{
|
||||
Logger.LogDebug("Received CustomizePlus change, updating {obj}", item);
|
||||
await AddPlayerCacheToCreate(item).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) =>
|
||||
{
|
||||
if (_isZoning) return;
|
||||
Logger.LogDebug("Received Heels Offset change, updating player");
|
||||
_ = AddPlayerCacheToCreate();
|
||||
});
|
||||
Mediator.Subscribe<GlamourerChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (_isZoning) return;
|
||||
var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address);
|
||||
if (changedType.Key != default || changedType.Value != default)
|
||||
{
|
||||
GlamourerChanged(changedType.Key);
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<HonorificMessage>(this, (msg) =>
|
||||
{
|
||||
if (_isZoning) return;
|
||||
if (!string.Equals(msg.NewHonorificTitle, _playerData.HonorificData, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("Received Honorific change, updating player");
|
||||
HonorificChanged();
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<PetNamesMessage>(this, (msg) =>
|
||||
{
|
||||
if (_isZoning) return;
|
||||
if (!string.Equals(msg.PetNicknamesData, _playerData.PetNamesData, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("Received Pet Nicknames change, updating player");
|
||||
PetNicknamesChanged();
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<MoodlesMessage>(this, (msg) =>
|
||||
{
|
||||
if (_isZoning) return;
|
||||
var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address);
|
||||
if (changedType.Key == ObjectKind.Player && changedType.Value != default)
|
||||
{
|
||||
Logger.LogDebug("Received Moodles change, updating player");
|
||||
MoodlesChanged();
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (msg) =>
|
||||
{
|
||||
Logger.LogDebug("Received Penumbra Mod settings change, updating player");
|
||||
AddPlayerCacheToCreate().GetAwaiter().GetResult();
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (msg) => ProcessCacheCreation());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_playerRelatedObjects.Values.ToList().ForEach(p => p.Dispose());
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
private async Task AddPlayerCacheToCreate(ObjectKind kind = ObjectKind.Player)
|
||||
{
|
||||
await _cacheCreateLock.WaitAsync().ConfigureAwait(false);
|
||||
_cachesToCreate[kind] = _playerRelatedObjects[kind];
|
||||
_cacheCreateLock.Release();
|
||||
}
|
||||
|
||||
private void GlamourerChanged(ObjectKind kind)
|
||||
{
|
||||
if (_glamourerCts.TryGetValue(kind, out var cts))
|
||||
{
|
||||
_glamourerCts[kind]?.Cancel();
|
||||
_glamourerCts[kind]?.Dispose();
|
||||
}
|
||||
_glamourerCts[kind] = new();
|
||||
var token = _glamourerCts[kind].Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(500), token).ConfigureAwait(false);
|
||||
await AddPlayerCacheToCreate(kind).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void HonorificChanged()
|
||||
{
|
||||
_honorificCts?.Cancel();
|
||||
_honorificCts?.Dispose();
|
||||
_honorificCts = new();
|
||||
var token = _honorificCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
await AddPlayerCacheToCreate().ConfigureAwait(false);
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void PetNicknamesChanged()
|
||||
{
|
||||
_petNicknamesCts?.Cancel();
|
||||
_petNicknamesCts?.Dispose();
|
||||
_petNicknamesCts = new();
|
||||
var token = _petNicknamesCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
await AddPlayerCacheToCreate().ConfigureAwait(false);
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void MoodlesChanged()
|
||||
{
|
||||
_moodlesCts?.Cancel();
|
||||
_moodlesCts?.Dispose();
|
||||
_moodlesCts = new();
|
||||
var token = _moodlesCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
await AddPlayerCacheToCreate().ConfigureAwait(false);
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void ProcessCacheCreation()
|
||||
{
|
||||
if (_isZoning || _haltCharaDataCreation) return;
|
||||
|
||||
if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true))
|
||||
{
|
||||
_cacheCreateLock.Wait();
|
||||
var toCreate = _cachesToCreate.ToList();
|
||||
_cachesToCreate.Clear();
|
||||
_cacheCreateLock.Release();
|
||||
|
||||
_cacheCreationTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var obj in toCreate)
|
||||
{
|
||||
await _characterDataFactory.BuildCharacterData(_playerData, obj.Value, _cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogCritical(ex, "Error during Cache Creation Processing");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger.LogDebug("Cache Creation complete");
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
else if (_cachesToCreate.Any())
|
||||
{
|
||||
Logger.LogDebug("Cache Creation stored until previous creation finished");
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
Reference in New Issue
Block a user